programming in lua (3)

5-6 章。

函数

函数是完成某一特定功能的语句集合。函数使用括号标识,括号内是传递给函数的参数。如果函数只有一个参数,且这个参数是一个字符串或者 table 的 constructor,那么括号是可选的,例如:

print("Hello")
print "hello"
print (type({x = 10, y = 20}))
print (type {x = 10, y = 20})

函数定义的语法是:

function func_name (parameter)
   body
end

如果括号中的参数多于 1 个用逗号隔开,如果没有参数就不用写。

给函数传递参数时,如果参数的个数比函数定义的个数少,则靠右的参数被赋值为 nil;如果传递的参数比函数定义的个数多,则多出来的值被忽略:

function func(a, b, c)
   print("a =", a)
   print("b =", b)
   print("c =", c)
end

func(5)
print("--------------")
func(5, 20)
print("--------------")
func(5, 30, 60)
print("--------------")
func(1, 2, 3, 4)

lua 的函数可以返回多个值,保存返回值的规则和前面的等号赋值一样从左往右依次赋值,如果左边的变量比函数返回值个数多,那么多出来的变量会被赋值为 nil;如果左边变量比返回值个数少,那么多出来的函数返回值会被忽略:

function add1(a, b)
   a = a + 1
   b = b + 1
   return a, b
end

x, y = add1(5, 7)
print(x, y) -- 6     8

如果函数作为一个表达式的一部分,lua 会根据表达式的上下文决定使用返回值的个数:

function func()
   return "a", "b"
end

print(func() .. "x") -- ax

因为连接运算符“..”要求一个参数(也就是相当于接收 func() 返回值的变量只有一个),所以只取返回值的第一个“a”,忽略“b”。

如果接收函数返回值的是一个接收不定参数的 constructor,并且函数是 constructor 的最后一个元素时,函数的返回值会被全部接收;但是如果函数不是最后一个值时,只有第一个返回值会被保留,其它的都被忽略:

function func()
   return "a", "b", "c"
end

function print_table(t)
   local i = 1
   while t[i] do
      print(t[i])
      i = i + 1
   end
end

a = {func()}
print_table(a) -- a b c
b = {"k", func(), "y"}
print_table(b) -- k a y

如果一个函数不返回任何值,那么接收它返回值的变量值为 nil:

function func()
end

a = func()
print(a) -- nil
print(func())
print((func())) -- nil

函数可以接收不定个数的参数,与指定参数个数不一样的是括号中使用三个点“...”来代替不定参数:

function my_print(...)
   for k, v in pairs{...} do
      print(v)
   end
end

my_print("a", "b", "c")
my_print("a", "b", "c", 3, 5)

如果还有其它参数,“...”必须放在最后。“...”可以认为是一个特殊的变量,保存传递给函数的所有参数。使用“...”可以把值赋给变量:

function func(...)
   local a, b, c = ...
   print("a =", a, "b =", b, "c =", c)
end

func(5, 1, 3, 0)

可以使用一个 table 把参数都保存起来。但是如果参数中含有 nil,要确定参数个数可以使用函数 select()。select() 函数的第一个固定参数叫 selector,后面有不定数量个参数。如果第一个参数是个数值 n,select() 返回后面的第 n 个参数;如果第一个参数不是数值,则应该是字符串“#”,这时 select() 返回参数的个数:

function func(selector, ...)
   local n = select(selector, ...)
   print("n is ", n)
end

func("#", 1, nil, 3)
func(3, 1, nil, 3)

从 lua 5.0 开始,每个不定参数的函数都有一个隐含变量 arg,指向变长参数的 table,并且 arg 有一个属性 n 保存变长参数的个数:

function func(a, ...)
   print("argn = ", arg.n)
end

func(1, 2, 3)
func(1, 2, 3, 4, 5)

一般来说,函数的参数传递都是按顺序进行的,即第一个值传递给第一个参数,等等。但是我们可能需要根据参数的名称来指定它的值(如果参数太多并且有些是默认参数),不过目前 lua 还没有提供使用变量名来指定参数值的功能。

在 lua 中,函数是第一类值(first-class value),可以保存在变量中,可以作为参数传递,可以作为其它函数的返回值返回。当我们说到某个函数,例如 print,是指 print 指向的函数,而不是把实现这个功能的函数和“print”这个名字绑定起来,其它名称也可以指向这个函数:

func = math.sin

math.sin = print -- now math.sin is used to print something
math.sin(10, 20, 30) -- 10   20   30

print = func -- print points to the original math.sin
math.sin(print(1)) -- 0.8414709

之前说过的定义函数的方式实际像是一种提供方便的“语法糖”:

function foo (x)
   return (2 * x)
end

print(foo(5)) -- 10

与其等价的另一种创建函数的方式是:

foo = function (x) return (2 * x) end

print(foo(5)) -- 10

我们可以使用“function (x) body end”这样的形式作为函数的 constructor,就像 table 使用“{}”一样。在 lua 中函数和普通变量没有区别,当一个函数需要另一个函数作为参数时,我们可以直接构造一个匿名函数传递进去:

function my_func(var, double_var)
   print(double_var(var))
end

my_func(5, function (x) return (2 * x) end) -- 10

一个以其它函数作为参数的函数称为高阶函数。

闭包

当一个函数定义于另一个函数内时,内层函数可以访问外层函数的局部变量:

function foo ()
   local i = 5
   return function () print(i) end
end

func = foo()
func() -- 5

foo() 中的局部变量 i 对于内层的匿名函数来说,既不是全局变量又不是局部变量,叫做“非局部变量”(non-local variable)。像下面创建一个匿名函数:

function counter ()
   local i = 0;
   return   function ()
               i = i + 1
               return i
            end
end

c1 = counter()
print(c1()) -- 1
print(c1()) -- 2

c2 = counter()
print(c2()) -- 1
print(c1()) -- 3

c1 接收 counter() 返回的一个匿名函数。i 是 counter() 的局部变量,当在 counter() 的外部调用 c1 时,在 c1 中使用的 i(也就是 counter() 中的 i)其实已经超出了它的作用域范围,但是当连续调用 c1 时,lua 仍然保留了 i 在上一次 c1 被调用时的值,就像 i 是 c1 中的变量一样,这就是闭包。简单地说,闭包就是一个函数加上在这个函数中所使用的非局部变量(non-local variable)。

在连续调用了两次 c1 后 c2 接收了 counter() 返回的一个新的匿名函数,但是 c2 和 c1 不一样,因为它们指向的不是同一个闭包。

使用闭包的好处是,我们可以对一些函数进行重新定义。前面提过函数的名称只是指向函数的一个引用,我们随时可以改变变量的指向让它变成另一个函数。例如我们想让每次调用 print() 之前都先打印出一行提示,我们可以使用闭包来重新定义 print() 函数:

do
   local old_print = print
   print = function (...)
      old_print("print arg:", ...)
   end
end

print("a")
print("5 + 3 =", 5 + 3)

在“do...end”界定的块中,old_print 是这个块里的局部变量,也是新 print() 函数里的非局部变量(non-local variable)。在“do...end”块之后使用的 print() 函数只能是经过修改的 print() 函数,而原来的 print() 函数已经不能直接访问了。通过这样的方法,我们可以对 lua 提供的一些函数进行包装,提供更好的错误检测机制或用在 debug 中,而不用去修改原来的代码。

本地函数

如果函数由局部变量引用,那么这个函数就是局部函数(和局部变量一样),定义方法和一般的局部变量相似:

local add = function (x, y)
   return x + y
end

或者另一种定义函数的语法糖:

local function add (x, y)
   return x + y
end

这样定义的函数作用域和局部变量的作用域一样,就是函数只在定义它的块内有意义。

定义一个递归的局部函数要注意的地方,就是在定义之前需要先声明局部函数的变量名称。如果像下面这样定义一个计算阶乘的局部函数:

local fact = function (n)
   if n == 0 then
      return 1
   else
      return n * fact(n - 1)
   end
end

fact(3) -- error

那么在调用 fact() 的时候会报错,因为在第 5 行调用 fact() 的时候实际的 fact() 函数还没定义完整,所以 lua 会尝试查找一个全局函数 fact(),因为没定义这样的一个全局函数,所以会报错。正确的做法是先声明一个局部变量 fact,再进行实际的函数定义(相当于 c/c++ 中的前置声明):

local fact
fact = function (n)
   if n == 0 then
      return 1
   else
      return n * fact(n - 1)
   end
end

print(fact(3)) -- ok

因为 lua 中函数和普通变量一样都保存在变量中,所以第一行 local 声明的变量 fact 初始值为 nil,在接下来的赋值中,fact 指向一个计算阶乘的函数。

又因为 lua 提供了另外一种定义函数的语法,所以上述的递归函数定义可以写成下面的形式而不需要提前声明变量(真晕……):

local function fact (n)
   if n == 0 then
      return 1
   else
      return n * fact(n - 1)
   end
end

print(fact(3)) -- ok

再次例外,如果定义的是间接递归函数(也就是 a 调用 b,b 中又调用了 a),上述第二种方法又不能用了,只能用第一种方法,也就是先声明“local a, b”再进行定义。总的来说,第一种方法是通用方法。

尾调用(tail call)

对于形如

function f (x) return g(x) end

这样的形式,因为函数调用 g(x) 是 f(x) 的最后一条语句,g(x) 执行完后直接把结果返回给 f(x) 的调用者,这样在调用 g(x) 后就不需要保留调用的信息。一般来说,尾调用就是一条 goto 语句,而且 goto 到别的函数后不需要回到调用点。但是像下面那样的却不是尾调用:

function f (x) g(x) end

因为调用完 g(x) 后 f(x) 还需要丢弃 g(x) 的返回值。总的来说,尾调用可能是这样的形式:

return func(args)

func 和 args 可能是复杂的表达式。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注