函数教程

lua-users home
wiki

函数允许您将一段代码存储在一个值中,这对于从多个地方运行相同的代码片段非常有用,而无需重复它。此外,它们还允许您通过向代码的不同部分提供不同的函数来在运行时更改程序的行为。

定义函数

函数使用function关键字创建,如下所示

function ( args ) body end

以下示例展示了一个简单的函数,它接收一个参数并返回其值的兩倍

> foo = function (n) return n*2 end
> = foo(7)
14

参数(也称为参数)在( )部分中指定,使用return关键字从函数返回的值。如果没有return,函数将不返回值。


函数是值

请注意,在上面的示例中,我们实际上并没有“命名”函数,我们只是将其分配给了一个变量。这是因为在 Lua 中,函数是普通值(如数字、字符串、表等),您可以对它们执行任何对其他值的操作。这与您可能知道的许多其他语言(如 C)非常不同,在这些语言中,函数具有在编译时固定的永久名称,并且不能像值一样被操作。

function块是一个表达式(与“1 + 2”是表达式一样),它计算为一个新的函数值。函数值可以通过使用( )运算符来调用,该运算符运行函数中的代码。( )对位于函数表达式之后,并可选地包含一个用逗号分隔的参数列表。

这意味着 Lua 函数被认为是匿名(没有预设名称)和一等(与其他值没有区别)。

要记住的另一件事是,与表一样,函数是按引用传递的。例如,当您将包含函数的变量分配给另一个变量时,您只是创建了对同一个函数的新“句柄”。


函数参数

函数可以接受 0 个或多个参数。这些是在调用函数时传递给函数的值,函数中存储的代码可以使用这些值。在函数内部,参数看起来像变量,只是它们只存在于函数内部。

一个演示参数如何工作以及如何将它们传递给函数的示例

> f = function (op, a, b)
>>  if op == 'add' then
>>   return a + b
>>  elseif op == 'sub' then
>>   return a - b
>>  end
>>  error("invalid operation")
>> end
> g = function (value)
>>  print(value)
>> end
> = f('add', 1, 2) -- args are given inside (), separated by commas.
3
> = f('add', 1, 2, 123) -- extra args are ignored
3
> = f('add', 1) -- missing args aren't an error, instead they will be filled with nil, which might cause an error in the function's code
stdin:1: attempt to perform arithmetic on local 'b' (a nil value)
> = g() -- to call a function with no args, use ()
nil
> = g "example" -- the () can be omitted if you have one quoted string arg
example
> = g {} -- same with one table constructor
table: 0x820ee0

函数返回值

函数可以使用 `return` 关键字将值返回给调用它们的代码。该值将成为函数调用表达式的值。Lua 的一个独特功能是函数可以返回任意数量的值。在大多数语言中,函数总是返回一个值。要使用此功能,请在 `return` 关键字后放置用逗号分隔的值。

> f = function ()
>>  return "x", "y", "z" -- return 3 values
>> end
> a, b, c, d = f() -- assign the 3 values to 4 variables. the 4th variable will be filled with nil
> = a, b, c, d
x y z nil
> a, b = (f()) -- wrapping a function call in () discards multiple return values
> = a, b
x, nil
> = "w"..f() -- using a function call as a sub-expression discards multiple returns
wx
> print(f(), "w") -- same when used as the arg for another function call...
x w
> print("w", f()) -- ...except when it's the last arg
w x y z
> print("w", (f())) -- wrapping in () also works here like it does with =
w x
> t = {f()} -- multiple returns can be stored in a table
> = t[1], t[2], t[3]
x y z

关于最后一个例子 ( `{f()} `) 需要记住的一点是,如果函数返回 nil,由于表中的 `nil` 被认为是“无值”,`#` 运算符无法可靠地用于获取值的个数,因为它无法确定数组是否包含“空洞”。

如果您习惯于使用返回多个值的语言(如 Python),这些值存储在“元组”类型中,那么 Lua 的工作方式并非如此。Lua 函数实际上返回单独的值,而不是单个容器。


Return 跳过其他代码

function f(switch)

	if not switch then --if switch is nil, function f() will not complete anything else below Return
		return
	end

	print("Hello") 
end

f()--doesn't print anything

function f(switch)

	if not switch then --switch is no longer nil but is instead "1"
		return
	end

	print("Hello") 
end

f(1)--prints "hello"

将函数用作参数和返回值

将函数用作参数或返回值是一个有用的功能,因为它允许您将自己的行为插入现有代码中。一个很好的例子是 `table.sort`,它可以选择接受一个自定义的“小于”函数。

> list = {{3}, {5}, {2}, {-1}}
> table.sort(list)
attempt to compare two table values
stack traceback:
        [C]: in function 'sort'
        stdin:1: in main chunk
        [C]: in ?
> table.sort(list, function (a, b) return a[1] < b[1] end)
> for i,v in ipairs(list) do print(v[1]) end
-1
2
3
5

可变数量的参数

函数可以在其参数列表的末尾使用 `...`。这将捕获在命名参数之后传递的任何剩余参数。然后您可以在函数体内部使用 `...`,它将评估为多个值(与具有多个返回值的函数调用具有相同的规则)。

例如,一个将它的额外参数不变地传递给另一个函数的函数

> f = function (x, ...)
>>  x(...)
>> end
> f(print, "1 2 3")
1 2 3

这也是一个函数将另一个函数作为参数的例子。

要从 `...` 中获取特定项目,请使用 `select` 函数,该函数接受一个数字和可变数量的参数,并返回从该索引开始的参数。它也可以接受 `"#"` 作为索引并返回参数的数量。

> f=function(...) print(select("#", ...)) print(select(3, ...)) end
> f(1, 2, 3, 4, 5)
5
3 4 5

`...` 也可以打包到一个表中

> f=function(...) tbl={...} print(tbl[2]) end
> f("a", "b", "c")
b

具有数组项目的表也可以“解包”到参数列表中

> f=function(...) tbl={...} print(table.unpack(tbl)) end -- it's just "unpack" (without the table.) in 5.1
> f("a", "b", "c")
a b c
> f("a", nil, "c") -- undefined result, may or may not be what you expect
但在第二个例子中,我们看到了一个问题:表不能存储 nil,这意味着 `#` 运算符(`table.unpack` 在内部使用)无法使用,因为它无法确定数组是否包含 nil “空洞”。即使循环遍历表以查找具有最大键的项目,如果 nil 是传递给函数的最后一个参数,也无法获得真实长度。

Lua 5.2 添加了一个table.pack函数来帮助解决这个问题,它的作用类似于{...},但它还添加了一个包含项目数量的“n”字段。

> f=function(...) tbl=table.pack(...) print(tbl.n, table.unpack(tbl, 1, tbl.n)) end
> f("a", "b", "c")
3 a b c
> f("a", nil, "c")
3 a nil c
在这里,我们还使用了table.unpack的起始和结束索引参数,如果给出这些参数,它将使用这些参数,而不是从 1 开始到#tbl结束。

“命名”函数的语法快捷方式

虽然 Lua 允许我们像使用任何其他值一样自由地使用函数,但我们通常只想给它们一个名称(通过将它们存储在变量中)并通过该名称使用它们。Lua 有些语法糖可以使将函数存储在变量中看起来更漂亮。

function f(...)
end

-- is equivalent to:

f = function (...)
end

这种语法可以在本教程中的示例中使用,但 = 使得函数是值的这一点更加清晰。通常建议在实际脚本中使用快捷语法,除非没有理由给你的函数命名。

此外,还有类似的语法用于将函数存储在表中。

function a.b.f(...)
end

-- is equivalent to:

a.b.f = function (...)
end

递归和尾调用

递归函数是指调用自身的函数。例如

function factorial(x)
  if x == 1 then
    return 1
  end
  return x * factorial(x-1)
end

还可能存在相互递归函数,例如a调用bb又调用a,如此反复。

这类函数的问题在于,每次调用函数时,Lua 都需要记住它是从哪里调用的,以便知道返回到哪里。此信息存储在一个称为调用栈的数据结构中,它在每次调用函数时都会增长,并在函数返回时缩小。因此,当你编写一个可以调用自身数千次的函数时,它会导致堆栈变得非常大。

尾调用是解决这个问题的方法:如果一个函数返回另一个函数调用的完全未修改的结果,Lua 就会知道它不必返回到你的函数,它可以使用当前的堆栈槽,并让调用的函数直接返回到调用你当前函数的函数。

如果以上内容令人困惑,另一种理解方式是,尾调用只是一个跳转,而不是真正的函数调用。

以下是上述函数的尾递归示例

function factorial_helper(i, acc)
  if i == 0 then
    return acc
  end
  return factorial_helper(i-1, acc*i)
end

function factorial(x)
  return factorial_helper(x, 1)
end

一些关于什么是尾调用和什么不是尾调用的例子

return f(arg) -- tail call
return t.f(a+b, t.x) -- tail call
return 1, f() -- not a tail call, the function's results are not the only thing returned
return f(), 1 -- not a tail call, the function's results are not the only thing returned
return (f()) -- not a tail call, the function's possible multiple return values need to be cut down to 1 after it returns
return f() + 5 -- not a tail call, the function's return value needs to be added to 5 after it returns
return f().x -- not a tail call, the function's return value needs to be used in a table index expression after it returns

并非所有非尾递归函数都是不好的,如果调用深度不会很深,它通常是最自然的解决方案。但是,如果你预期数百次或更多次的迭代,你可能应该考虑使用尾递归或循环。

最后,尾调用与递归一起引入的唯一原因是,它们在递归中最为有用。但这并不意味着尾调用只在调用同一个函数时才有效,它在调用不同函数时仍然以相同的方式工作。


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2016 年 6 月 23 日下午 6:16 GMT (差异)