Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用

一、函数的定义

  • Lua中函数定义的常见语法格式为:
function 函数名(函数参数)
    -- 函数体
end
  • 例如,下面是一个对序列'a'的元素进行求和的函数
function add(a)
    local sum = 0

    for i = 1, #a do
        sum = sum + a[i]
    end

    return sum
end

二、函数调用的注意事项

是否需要带"圆括号"

  • 一般情况下,函数调用时需要带上圆括号(不论函数是否带有参数)。录入:
add(10, 20)

b = "String"
func(b)
  • 有一个特殊情况:当函数只有一个参数,且参数是字符串常量或者表构造器时,可以不使用圆括号。例如:
-- 等价于print("Hello World")
print "Hello World"

-- 等价于dofile('a.lua')
dofile 'a.lua'

-- 等价于print([[a multi-line
-- message]])
print [[a multi-line
 message]]


-- 等价于f({x = 10, y = 20})
f{x = 10, y = 20}

-- 等价于type({})
type{}

面向对象风格的调用

  • Lua语言也为面向对象风格的调用提供了一种特殊的语法,即冒号调用操作符
  • 例如:
-- 其中o是对象, foo是o的方法
o::foo(x)
  • 我们在后面介绍“面向对象”编程时再详细介绍这种调用方式

调用参数个数与定义参数个数不一致

  • 调用函数时传递的参数个数可以与定义函数时定义的参数个数不一致,Lua会通过抛弃多余参数和将不足的参数设为nil的方式来调整参数的格式
  • 例如:下面定义了一个函数,用来打印传递的参数
function f(a, b) print(a, b) end
  • 那么下面的调用都是正确的
-- 等价于f(nil, nil)
f()

-- 等价于f(3, nil)
f(3)

-- 等价于f(3, 4)
f(3, 4)

-- 等价于f(3, 4), 抛弃5
f(3, 4, 5)

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第1张图片

  • 这种行为可能导致编程错误(在单元测试中容易发现),但同样又是有用的,尤其是对于默认参数的情况。例如:
    • 该函数以1作为默认实参
    • 当调用无参数的inCount()时,将globalCounter+1;当调用有参数的inCount(n)时,会把globalCounter+n
function inCount(n)
    n = n or 1
    globalCounter = globalCounter + n
end

三、多返回值

  • Lua函数允许一个函数返回多个结果
  • 例如在前面我们学习过的stirng.find()函数,其就会返回2个返回值,分别为匹配模式在字符串中其实字符和结尾字符的索引
s, e = string.find("hello Lua users", "Lua")

print(s, 3)

多返回值的语法

  • 只需要在return关键字后面列出所有要返回的值即可
  • 例如:下面查找序列中的最大元素,同时返回其值及其位置
function maximum(a)
    local mi = 1
    local m = a[mi]
    
    for i = 1, #a do
        if a[i] >m then
            mi = i; m = a[i]
        end
    end
    
    return m , mi
end

print(maximum({8, 10, 23, 12, 5}))

  • 函数多返回值的使用会根据函数被调用的方式来返回:
    • ①当函数被作为一条单独语句调用时,其所有返回值都会被丢弃
    • ②当函数被作为表达式(例如,加法的操作数)调用时,将只保留函数的第一个返回值
    • ③只有当函数调用是一系列表达式中的最后一个表达式(或者唯一一个表达式)时,其所有的返回值才能被获取到。这里的“一系列表达式”在Lua中表现为4种情况:多重赋值、函数调用时传入的实参列表、表构造器、return语句
  • 下面主要讲解“多重赋值、函数调用时传入的实参列表、表构造器、return语句”的4种情况

多重赋值

  • 现在定义了3个函数
-- 不返回结果
function foo0() end

-- 返回1个结果
function foo1() return "a" end

-- 返回2个结果
function foo2() return "a", "b" end

  • 如果一个函数调用是一系列表达式中的最后(或者唯一)一个表达式,则该函数调用将产生尽可能多的返回值以匹配待赋值变量。例如:
-- x="a", y="b"
x, y = foo2()

-- x="a", 返回值"b"被丢弃
x = foo2()

-- x=10, y="a", z="b"
x, y, z = 10, foo2()
  • 如果一个函数没有返回值或者返回值个数不够多,那么Lua会用nil来补充缺失的值。例如:
-- x=nil, y=nil
x, y = foo0()

-- x="a", y=nil
x, y = foo1()

-- x="a", y="b", z=nil
x, y, z = foo2()
  • 只有当函数调用是一系列表达式中的最后(或者是唯一)一个表达式时才能返回多值结果,否则只能返回一个结果。 例如:
-- x="a", y=20 ,其中foo2()的返回值"b"被丢弃了
x, y = foo2(), 20

-- x=nil, y=20, 最后的30被丢弃了
x, y = foo1(), 20, 30

函数调用时传入的实参列表

  • 当函数作为其他函数的参数传递时,有如下的规则:
    • 当一个函数调用是另一个函数调用的最后一个(或者唯一)实参时,函数的所有返回值都会被作为实参传给第二个函数(见下面演示案例1)
    • 当一个函数调用是另一个函数调用的实参时,但是如果这个函数调用后面还有参数,那么这个函数只返回第一个返回值给第二个函数(见下面演示案例2)
  • 其实这与上面的“多重赋值”的原理是一模一样的
  • 演示案例1:
print(foo0())
print(1, foo0())

print(foo1())
print(2, foo1())

print(foo2())
print(3, foo2())

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第2张图片

  • 演示案例2:
print(foo2())
print(foo2(), 3)
print(foo2() .. "x")

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第3张图片

表构造器

  • 当函数调用被用在表构造器中时,有如下的规则:
    • 当这个函数是表构造器中的最后一个(或者唯一)实参时,函数的所有返回值都会返回给表构造器(见下面演示案例1)
    • 当这个函数用来表构造器中,但是函数后面还有元素时,那么函数只返回第一个返回值(见下面演示案例2)
  • 其实这与上面的“多重赋值”等原理都是相同的
  • 演示案例1:
-- t1 = {}
t1 = {foo0()}

-- t2 = {"a"}
t2 = {foo1()}

-- t3 = {"a", "b"}
t3 = {foo2()}
  • 演示案例2:
-- t1 = {"a", 4}
t1 = {foo2(), 4}

-- t2 = {nil, "a", 4}
t2 = {foo0(), foo2(), 4}

return语句

  • 函数可以作为其他函数的return返回值进行返回,原理与上面的都是相同的:
    • 当这个函数作为其他函数return的返回值返回时,如果是return的最后一个(或者唯一)实参时,函数的所有返回值都会返回
    • 当这个函数作为其他函数return的返回值返回时,如果其后面还有别的返回值,那么只返回其第一个返回值
  • 例如,下面是一个函数的定义和调用
function foo(i)
    if i == 0 then return foo0()
    elseif i == 1 then return foo1()
    elseif i == 2 then return foo2()
    elseif i == 3 then return foo2(), 10
    end
end

print(foo(0))

print(foo(1))

print(foo(2))

print(foo(3))

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第4张图片

  • 强制返回一个返回值
print(foo(0))

print(foo(1))

print(foo(2))

print(foo(3))

强制返回一个返回值

  • 如果一个函数返回多个返回值,我们可以在调用该函数时,在外面加一层圆括号,这样该函数只会返回一个返回值
  • 例如:
print(foo2())

-- 在foo2()外面加上圆括号, 其只返回一个返回值
print((foo2()))

  • 例如:
-- x="a", y="b"
x, y = foo2()
print(x, y)

-- x="a", y=nil
x, y = (foo2())
print(x, y)

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第5张图片

  • 例如:
function foo(i)
    if i == 1 then return foo2()
    elseif i ==2 then return (foo2())
    end
end

print(foo(1))

-- 下面都只返回1个值
print(foo(2))
print((foo(1)))

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第6张图片

四、可变长参数

  • Lua支持可变长参数函数,“可变长参数”是使用三个点(...)组成的可变长参数表达式
  • 例如,下面是一个简单的演示案例,该函数返回所有参数的总和
function add(...)
    local s = 0
    for _, v in ipairs{...} do
        s = s + v
    end
    return s
end

print(add(1))
print(add(1, 2, 3))
print(add(3, 4, 10, 25, 12))

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第7张图片

  • 例如,下面的函数在内部将变长参数的前2个参数赋值给局部变量a和b
function add(...)
    local a,b = ...
end
  • 例如,下面的两个函数都是等同的
function func1(a, b, c)
    
end

-- Perl编程人员可能更喜欢这种形式
function func2(...)
    local a, b, c = ...
end
  • 例如,还可以打印所有的参数,或者将可变长参数再传递给其它函数
function foo1(...)
    print("calling foo1: ", ...)
    return foo2(...)
end
  • 再来看另一个有用的示例:
    • Lua提供了用于格式化输出的string.format()函数和输出文本的io.write()函数,我们可以把这两个函数合并为一个具有可变长参数的函数
    • 注意,在三个点前有一个固定的参数fmt。具有可变长参数的函数也可以具有任意数量的固定参数,但固定参数必须放在变长参数之前。Lua语言会先将前面的参数赋值给固定参数,然后将剩余的参数(如果有)作为可变长参数
function fwrite(fmt, ...)
    return io.write(string.format(fmt, ...))
end

可变长参数的遍历方式1(表遍历)

  • 要遍历可变长参数,可以使用表达式{...}将可变长参数放在一个表中
function foo(...)
    a = {...}
    for index = 1, #a do
        print(a[index])
    end
end

foo("a", "b", 10)
foo("a", "b", {"HelloWorld", 666}, 10)

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第8张图片

  • 不过,如果可变长参数中包含无效的nil,那么{...}获得的表可能不再是一个有效的序列。此时,就没有办法在表中判断原始参数究竟是不是以nil结尾的

可变长参数的遍历方式2(table.pack()函数)

  • 在Lua 5.2中引入了table.pack()函数,该函数像表达式{...}一样保存了所有参数,然后将其放在一个表中返回,但是这个表还有一个保存了参数个数的额外字段"n"
  • 例如:下面使用函数table.pack()来检测参数中是否有nil
function nonils(...)
    local arg = table.pack(...)
    for i = 1, arg.n do
        if arg[i] == nil then return false end
    end
    return true
end

print(nonils(2, 3, nil))
print(nonils(2, 3))
print(nonils())
print(nonils(nil))

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第9张图片

可变长参数的遍历方式3(select()函数)

  • 另一种遍历可变长参数的方式是使用select()函数
  • select()函数的参数1决定了该函数的行为:
    • 如果参数1是一个数值n,则select()返回第n个参数后的所有参数
    • 如果参数1是#,则select()返回参数1后面所有参数的总数
  • 例如:
print(select(1, "a", "b", "c"))

print(select(2, "a", "b", "c"))

print(select(3, "a", "b", "c"))

print(select("#", "a", "b", "c"))

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第10张图片

  • 例如,下面我们改写上面的add()函数,所有参数的总和
    • 因为select()是作为+号表达式调用的,因此select()每次都只会返回第1个值
    • 这个版本的add()相比于上面的那个add()效率更好,因为避免了每次调用时创建一个新的表
    • 不过如果参数较多,多次带有很多参数调用函数select会超过创建表的开销,因此此时上面那个版本的add()函数更好(特别地,由于迭代的次数和每次迭代时传入参数的个数会随着参数的个数增长,因此第二个版本的时间开销是二次代价的)
function add(...)
    local s = 0
    for i = 1, select('#', ...) do
        s = s + select(i, ...)
    end
    return s
end

print(add(1))
print(add(1, 2, 3))
print(add(3, 4, 10, 25, 12))

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第11张图片

五、table.unpack()

  • 多重返回值还涉及一个特殊的函数table.unpack(),该函数的参数是一个数组,返回值为数组内的所有元素
  • 例如:
print(table.unpack{10, 20, 30})

a, b = table.unpack{10, 20, 30}
print(a, b)

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第12张图片

  • 可以看到,table.unpack()与table.pack()的功能相反:
    • table.pack():把参数列表转换为Lua语言中的一个列表(表)
    • table.unpack():把列表(表)转换为一组值,进而可以作为另一个函数的参数被使用

泛型调用机制

  • unpack()函数的重要用途之一体现在泛型调用机制中泛型调用机制允许我们动态地调用具有任意参数的任意函数
  • 例如,在ISO C中,我们无法编写泛型调用的代码,只能声明可变长参数的函数(使用stdarg.h)或使用函数指针来调用不同的函数;但是,我们仍然不能调用具有可变数量参数的函数,因为C语言中的每一个函数调用的实参个数是固定的,并且每个实参的类型也是固定的
  • 而在Lua中,却可以做到这一点,如果我们想通过数组a传入可变参数来调用函数f,那么写成下面的代码,table.unpack()函数会返回数组a的所有元素,然后作为f的参数
f(table.unpack(a))
  • 例如下面的代码:
print(string.find("hello", "ll"))
  •  上面的代码可以改写为下面的样子:
f = string.find
a = {"hello", "ll"}

print(f(table.unpack(a)))
  • table.unpack()使用长度操作符#来获取返回值的个数,因此该函数只能用于序列
  • 我们还可以使用圆括号限制返回元素的范围。例如:
print(table.unpack({"Sun", "Mon", "Tue", "Wed"}, 2, 3))

自己实现一个table.unpack()函数

  • table.unpack()是使用C语言编写的,但是我们也可以自己使用Lua实现一个
  • 实现代码如下:
    • 第1次调用时,只传入一个参数,此时i=1,n为序列长度
    • 然后函数返回t[1]及unpack(t, 2, n)返回的所有结果,而unpack(t, 2, n)又会返回t[2]及unpack(t, 3, n)返回的所有结果
    • 以此类推...直到处理完n个元素位置
function unpack(t, i, n)
    i = i or 1
    n = n or #t
    if i <= n then
        return t[i], unpack(t, i + 1, n)
    end
end

print(unpack{10, 20, 30})

Lua:07---Lua函数:函数语法、多返回值、可变长参数(table.pack()、select())、table.unpack()、尾调用_第13张图片

六、正确的尾调用

  • Lua语言中有关函数的另一个有趣的特性是,Lua原因是支持尾调用消除的。这意味着Lua可以正确地尾递归,虽然尾递归调用消除的概念并没有直接涉及递归
  • 尾调用也就是递归

尾调用

  • 尾调用是被当作函数调用使用的跳转
  • 当一个函数的最后一个动作是调用另一个函数而没有进行其它工作时,就形成了尾调用
  • 例如,下面的代码中对函数g()的调用就是尾调用
function f(x)
    x = x + 1
    return g(x)
end

尾调用消除

  • 在上面的代码中,当函数f()调用完g()之后,f()不再需要进行其它的工作。这样,当被调用的函数执行结束后,程序就不再需要返回最初的调用者。因此,在尾调用之后,程序也就不需要在调用栈中保存有关调用函数的任何信息。当g()返回时,程序的执行路径会直接返回到调用f()的位置
  • 在一些语言的实现中,例如Lua语言解释器中,就利用了这个特点,使得在进行尾调用时不使用任何额外的栈空间,我们将这种实现称为“尾调用消除”
  • 由于尾调用不会使用栈空间,所以一个程序中能够嵌套的尾调用的数量是无限的。例如,下面的函数永远不会发生栈溢出
function foo(n)
    if n > 0 then
        return foo(n - 1)
    end
end

如何判断一个调用是“尾调用”

  • 关于“尾调用消除”的一个重点就是如何判断一个调用是“尾调用”
  • 例如下面的调用就不是尾调用,因为当调用完g(x)之后,f在返回前还不得不丢弃g()返回的所有结果
function f(x)
    g(x)
end
  • 类似的,下面的调用都不符合尾调用的定义
-- 必须进行加法
return g(x) + 1

-- 必须把返回值限制为1个
return x or g(x)

-- 必须把返回值限制为1个
reutn (g(x))
  • 在Lua中,只有形如"return func(args)"的调用才是尾调用。不过,由于Lua语言会在调用前对func()及其参数求值,所以func()及其参数都可以是复杂的表达式。例如,下面的例子就是尾调用
return x[i].foo(x[j] + a * b, i + j)

你可能感兴趣的:(Lua,函数语法,多返回值,可变长参数,尾调用)