c语言 unpack函数,6 函数

  • Post author:
  • Post category:其他


在 Lua 语言中,函数是对语句和表达式进行抽象的主要方式。函数既可以用于完成某种特定任务或子例程,也可以只是进行一些计算然后返回计算结果。在前一种情况下,我们将一句函数调用视为一条语句;而在后一种情况下,我们则将函数调用视为表达式:

print(8 * 9)

a = math.sin(3) + math.cos(10)

print(os.date())

无论哪种情况,函数调用时都需要使用一对圆括号把参数列表括起来。即使被调用的函数不需要参数,也需要一对空括号 ()。对于这个规则,唯一的例外就是,当函数有且只有一个参数,且该参数是字符串常量或表构造器时,括号是可选的:

print “hello world” print(“hello world”)

dofile ‘a.lua’ dofile(‘a.lua’)

print [[amulti-line print([[amulti-line

message]] message]])

f{x = 10, y =20} f({x = 10, y = 20})

type{} type({})

我不确定其他语言是否有这样的语法,但我使用 Lua 主要用于 Unity 中的热更新,所以,即使有这样可选的括号,我还是建议把括号带上,毕竟写代码不是指写给自己看,多为别人着想!

Lua 语言也为面向对象风格的调用提供了一种特殊的语法,即冒号操作符。形如 o:foo(x)的表达式意为调用对象 o 的 foo 方法。在第 21 章中,我们会继续学习这种调用方式及面向对象编程。

一个 Lua 程序既可以调用 Lua 语言编写的 函数,也可以调用 C 语言编写的函数。一般来说,我们选择使用 C 语言编写的函数来实现对性能要求更高,或不容易直接通过 Lua 语言进行操作的操作系统机制等。例如,Lua 语言标准库中所有的函数都是使用 C 语言编写的。不过,无论一个函数是用 Lua 语言编写的还是用 C 语言编写的,在调用它们时都没有任何区别。

正如我们已经在其他示例中所看到的,Lua 语言中的函数定义的常见语法格式形如:

function add(a)

local sum = 0

for i = 1, #a do

sum = sum + a[i]

end

return sum

end

在这种语法中,一个函数定义具有一个函数名、一个参数组成的列表和由一组语句组成的函数体。参数的行为与局部变量的行为完全一致,相当于一个用函数调用时传入的值进行初始化的局部变量。

调用函数时使用的参数个数可以与定义函数时使用的参数个数不一致。Lua 语言会通过抛弃多余参数和将不足的参数设为 nil 的方式来调整参数的个数。例如,考虑如下函数:

function f(a, b) print(a, b) end

其行为如下:

f() –> nil nil

f(3) –> 3 nil

f(3, 4) –> 3 4

f(3, 4, 5) –> 3 4

虽然这种行为可能导致编程错误,但同样又是有用的,尤其是对于默认参数的情况。例如,考虑如下递增全局计数器的函数:

function incCount(n)

n = n or 1

globalCounter = globalCounter + n

end

该函数以 1 作为默认参数,当调用无参数的incCount()时,将 globalCounter 加 1。在调用incCount()时,Lua 语言首先把参数 n 初始化为 nil,接下来的 or 表达式又返回了其第二个操作数,最终把 n 赋成了默认值 1。

6.1 多返回值

Lua 语言中一种与众不同但又非常有用的特性是允许函数返回多个结果。Lua 语言中几个预定义函数就会返回多个值。我们已经接触过函数string.find,该函数用于在字符串中定位模式。当找到了对应的模式时,该函数会返回两个索引值:所匹配模式在字符串中起始字符和结尾字符的索引。使用多重赋值可以同时获取到这两个结果:

s, e = string.find(“hello Lua users”,”Lua”)

print(s, e) –> 7 9

请记住,字符串的第一个字符的索引值为 1。

Lua 语言编写的函数同样可以返回多个结果,只需在 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})) –> 23 3

Lua 语言根据函数的被调用情况调整返回值的数量。当函数被作为一条单独的语句调用时,其所有返回值都会被丢弃;当函数被作为表达式调用时,将只保留第一个返回值。只有当函数调用是一系列表达式中的最后一个表达式(或唯一一个表达式)时,其所有的返回值才能被获取到。这里所谓的“一系列表达式”在 Lua 中表现为 4 种情况:多重赋值、函数调用时传入的实参列表、表构造器和 return 语句。为了分别展示这几种情况,接下来举几个例子:

function foo0() end

function foo1() return “a” end

function foo2() return “a”, “b” end

在多重赋值中,如果一个函数调用是一系列表达式中的最后一个(或是唯一一个)表达式,则该函数调用将产生尽可能多的返回值以匹配待赋值变量:

x, y = foo2() –> x = “a”, y = “b”

x = foo2() –> x = “a”, “b”被丢弃

x, y, z = 10, foo2() –> x = 10, y = “a”, z = “b”

在多重赋值中,如果一个函数没有返回值或者返回值个数不够多,那么 Lua 语言会用 nil 来补充缺失的值:

x, y = foo0() –> x = nil, y = nil

x, y = foo1() –> x = “a”, y = nil

x, y, z = foo2() –> x = “a”, y = “b”, z = nil

请注意,只有当函数调用是一系列表达式中的最后一个(或唯一一个)表达式时才能返回多值结果,否则只能返回一个结果:

x, y = foo2(), 20 –> x = “a”,y = 20

x, y = foo0(), 20, 30 –> x = nil, y = 20

x = foo0(), 20 –> x = nil (我自己补充一个)

当一个函数调用是另一个函数调用的最后一个(或者唯一一个)实参时,第一个函数的所有返回值都会被作为实参传给第二个函数。我们已经见到过很多这样的代码结构,例如函数print能够接受可变数量的参数,所以print(g())会打印出 g 返回的所有结果。

print(foo0()) –> (无结果)

print(foo1()) –> a

print(foo2()) –> a b

print(foo2(), 1) –> a 1

print(foo2() .. “x”) –> ax

print(“x” .. foo2()) –> xa (我自己补充一个)

当在表达式中调用foo2时,Lua 语言会把其返回值的个数调整为 1。因此,在上例的最后一行,只有第一个返回值 “a” 参与了字符串连接操作。

当我们调用f(g())时,如果 f 的参数是固定的,那么 Lua 语言会把 g 返回值的个数调整成与 f 的参数个数一致。这并非巧合,实际上这正是多重赋值的逻辑。

表构造器会完整地接收函数调用的返回值,而不会调整返回值的个数:

t = {foo0()} –> t = {}

t = {foo1()} –> t = {“a”}

t = {foo2()} –> t = {“a”, “b”}

不过,这种行为只有当函数调用是表达式列表中的最后一个时才有效,在其他位置上的函数调用总是只返回一个结果:

t = {foo0(), foo2(), 4} –> t[1] = nil, t[2] = “a”, t[3] = 4

最后,形如return f()的语句会返回 f 返回的所有结果:

function foo(i)

if i == 0 then return foo0()

elseif i == 1 then return foo1()

elseif i == 2 then return foo2()

end

end

print(foo(1))

print(foo(2))

print(foo(0))

print(foo(3))

将函数调用用一对圆括号括起来也可以强制其只返回一个结果:

print((foo0())) –> nil

print((foo1())) –> a

print((foo2())) –> a

应该意识到,return 语句后面的内容是不需要加括号的,如果加了括号会导致程序出现额外的行为。因此,无论f究竟返回了几个值,形如return (f(x))的语句只返回一个值。有时这可能是我们所希望出现的情况,但有时又可能不是。

6.2 可变长参数函数

Lua 语言中的函数可以是可变长参数函数,即可以支持数量可变的参数。例如,我们已经使用一个、两个或更多个参数调用过函数 print。虽然函数 print 是在 C 语言中定义的,但也可以在 Lua 语言中定义可变长参数函数。

下面是一个简单的示例,该函数返回所有参数的总和:

function add(…)

local s = 0

for _, v in ipairs{…} do

s = s + v

end

return s

end

print(add(3, 4, 10, 25, 12)) –> 54

参数列表中的三个点…表示该函数的参数是可变长的。当这个函数被调用时,Lua 内部会把它所有参数收集起来,我们把这些收集起来的参数称为函数的额外参数。当函数要访问这些参数时仍需用到三个点,但不同的是此时这三个点是作为一个表达式来使用的。在上例中,表达式 {…} 的结果是由一个由所有可变长参数组成的列表,该函数会遍历该列表来累加其中的元素。

我们将三个点组成的表达式称为可变长参数表达式,其行为类似于一个具有多尔返回值的函数,返回的是当前函数的所有可变长参数。例如,print(…)会打印出该函数的所有参数。又如,如下代码创建了两个局部变量,其值为前两个可选的参数(如果参数不存在则为 nil):

local a, b = …

实际上,可以通过变长参数来模拟 Lua 语言中普通的参数传递机制,例如:

function foo(a, b, c)

可以写成:

function foo(…)

local a, b, c = …

喜欢 Perl 参数传递机制的人可能会更喜欢第二种形式。

形如下例的函数只是将调用它时传入的所有参数简单地返回:

function id(…) return … end

该函数是一个多值恒等式函数。下列函数的行为则类似于直接调用函数 foo,唯一不同之处是在调用函数 foo 之前先打印出传递给函数 foo 的所有参数:

function foo1(…)

print(“calling foo: “, …)

return foo(…)

end

当跟踪对某个特定的函数调用时,这个技巧很有用。

接下来再让我们看另外一个很有用的示例。Lua 语言提供了专门用于格式化输出的函数string.format和输出文本的函数io.write。我们会很自然的想到把这两个函数合并为一个具有可变长参数的函数:

function fwrite(fmt, …)

return io.write(string.format(fmt, …))

end

注意,在三个点前有一个固定的参数 fmt。具有可变长参数的函数也可以具有任意数量的固定参数,但固定参数必须放在变长参数之前。Lua 语言会先将前面的参数赋给固定参数,然后将剩余的参数(如果有)作为可变长参数。

要遍历可变长数组,函数可以使用表达式 {…} 将可变长参数放在一个表中,就像 add 示例中所做的那样。不过,在某些罕见的情况下,如果可变长参数中包含无效的 nil,那么 {…} 获得的表可能不再是一个有效的序列。此时,就没有办法再表中判断原始参数究竟是不是以 nil 结尾的。对于这种情况,Lua 语言提供了函数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))

另一种遍历函数的可变长参数的方法是使用函数 select。函数 select 总是具有一个固定额参数 selector,以及数量可变的参数。如果 selector 是数值 n,那么函数 select 则返回第 n 个参数后的所有参数;否则,selector 应该是字符串 “#”,以便函数 select 返回额外参数的总数。

print(select(1, “a”, “b”, “c”)) –> a, b, c

print(select(2, “a”, “b”, “c”)) –> b, c

print(select(3, “a”, “b”, “c”)) –> c

print(select(“#”, “a”, “b”, “c”)) –> 3

通常,我们在需要把返回值个数调整为 1 的地方使用函数 select,因此可以把select(n, …)认为是返回第 n 个额外参数的表达式。

来看一个使用函数 select 的典型示例,下面是使用该函数的 add 函数:

function add(…)

local s = 0

for i = 1, select(“#”, …) do

s = s + select(i, …)

end

return s

end

对于参数较少的情况,第二个版本额 add 更快,因为该版本避免了每次调用时创建一个新表。不过,对于参数较多的情况,多次带有很多参数调用函数 select 会超过创建表的开销,因此第一个版本会更好(特别的,由于迭代的次数和每次迭代时传入参数的个数会随着参数的个数增长,因此第二个版本的时间开销是二次代价的)。

6.3 函数 table.unpack

多重返回值还涉及一个特殊的函数table.unpack。该函数的参数是一个数组,返回值为数组的所有元素:

print(table.unpack({10, 20, 30})) –> 10 20 30

a, b = table.unpack({10, 20, 30}) –> a = 10, b = 20, 30被丢弃

顾名思义,函数table.unpack()与函数table.pack()的功能相反。pack 会把参数列表转换成 Lua 语言中一个真实的列表,而 unpack 则把 Lua 语言中真实的列表转换成一组返回值,进而可以作为一个函数的参数被使用。

unpack 函数的重要用途之一体现在泛型调用机制中。泛型调用机制允许我们动态地调用具有任意参数的任意函数。例如,在 ISO C 中,我们无法编写泛型调用的代码,只能声明可变长参数的函数或者使用函数指针来调用不同的函数。但是,我们仍然不能调用具有可变数量参数的函数,因为 C 语言中的每一个函数调用的实参个数是固定的,并且每个实参的类型也是固定的。而在 Lua 语言中,却可以做到这一点。如果我们想通过数组 a 传入可变的参数来调用函数 f,那么可以写成:

f(table.unpack(a))

unpack 会返回 a 中所有的元素,而这些元素又被用作 f 的参数。例如,考虑如下代码:

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)) –> “Mon” “Tue”

虽然预定义的函数 unpack 是用 C 语言编写的,但是也可以利用递归在 Lua 语言中实现:

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

在第一次调用该函数时, 只传入一个参数,此时 i 为 1,n 为序列长度;然后,函数返回 t[1] 及 unpack(t, 2, n) 返回的所有结果,而 unpack(t, 2, n) 又会返回 t[2] 及 unpack(t, 3, n) 返回的所有结果,依此类推,直到处理完 n 个元素为止。

6.4 正确的尾调用

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

尾调用是被当作函数调用使用的跳转。当一个函数的最后一个动作是调用另一个函数而没有再进行其他工作时,就形成了尾调用。例如,下面代码中对函数 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 就不是尾调用:

function f(x)

g(x)

end

这个示例的问题在于,当调用完 g 后,f 在返回前还不得不丢弃 g 返回的所有结果。类似的,以下所有调用也都不符合尾调用的定义:

return g(x) + 1

return x or g(x)

return (g(x))

在 Lua 语言中,只有形如return func(arg)的调用才是尾调用。不过,由于 Lua 语言会在调用前对 func 及其参数求值,所以 func 及其参数都可以是复杂的表达式。例如,下面的例子就是尾调用:

return x[i].foo(x[j] + a * b, i + j)

6.5 练习

练习 6.1:请编写一个函数,该函数的参数为一个数组,打印出该数组的所有元素。

function PrintArr(arr)

for _,v in pairs(arr) do

print(v)

end

end

练习 6.2:请编写一个函数,该函数的参数为可变数量的一组值,返回值为除第一个元素之外的其他所有值。

function PrintArr2(…)

return table.unpack({…}, 2, select(“#”, …))

end

print(PrintArr2(1,2,3,4,5))

练习 6.3:请编写一个函数,该函数的参数为可变数量的一组值,返回值为除了最有一个元素之外的所有值。

function PrintArr3(…)

return table.unpack({…}, 1, select(“#”, …) – 1)

end

print(PrintArr3(1,2,3,4,5,6))

练习 6.4:请编写一个函数,该函数用于打乱一个指定的数组,请保证所有的排列都是等概率的。

—打乱输入进来的数组

—@return table

—@param arr table

function Shuffle(arr)

local t = {}

math.randomseed(os.time())

for i = #arr, 1, -1 do

table.insert(t,table.remove(arr, math.random(i)))

end

return t

end

print(table.unpack(Shuffle({1,2,3,4,5})))

练习 6.5:请编写一个函数,其参数为一个数组,返回值为数组中元素的所有组合。提示:可以使用组合的递推公式 C(n, m) = C(n – 1, m – 1) + C(n – 1, m)。要计算从 n 个元素中选出 m 个组成的组合 C(n, m),可以先将第一个元素加到结果集中,然后计算所有的其他元素的C(n – 1, m – 1);然后,从结果中删掉第一个元素,再计算其他所有剩余元素的 C(n – 1, m)。当 n 小于 m 时,组合不存在;当 m 为 0 时,只有一种组合(一个元素也没有)。

这题比较难 以后再写

练习 6.6:有时,具有正确尾调用的语句被称为 正确的尾递归,争论在于这种正确性只与递归调用有关(如果没有递归调用,那么一个程序的最大调用深度是静态固定的)。

请证明上述争论的观点像 Lua 语言一样的动态语言中不成立:不使用递归,编写一个能够实现支持无限调用链的程序。

想要实现无限调用链,就要在完成函数功能以后再返回自身,即是像 C++ 运算符重载那样,可以无限地使用 << 来对 cout 进行输出

—可以链式打印

—@return function

—@param n number

function MyPrint(n)

print(n)

return MyPrint

end

MyPrint(1)(2)(3)(4)