5.1 函数的定义 Lua 函数的定义详解 1. 函数定义的基本语法 在 Lua 中,定义一个函数的基本语法结构如下: 让我们逐个分解这个语法结构: 关键字: 这是声明函数定义的起始关键字,告诉 Lua 解释器接下来要定义一个函数。 : 这是你为函数选择的名字,用于在代码中调用该函数。函数名必须遵循 Lua 的标识符命名规则: 必须以字母或下划线 开头。 可以包含字母、数字和下划线。 区分大小写。 避免使用 Lua 预留关键字作为函数名(例如 , , 等)。 建议使用具有描述性的函数名,清晰表达函数的功能。 : 这是函数的形式参数列表,用于接收调用函数时传递的实际参数。参数列表是可选的,如果函数不需要接收任何参数,可以留空。参数之间使用逗号 分隔。
1. 函数定义的基本语法
在 Lua 中,定义一个函数的基本语法结构如下:
function 函数名 (参数列表) -- 函数体 -- 执行的代码 [return 返回值列表] end
让我们逐个分解这个语法结构:
function 关键字: 这是声明函数定义的起始关键字,告诉 Lua 解释器接下来要定义一个函数。
函数名: 这是你为函数选择的名字,用于在代码中调用该函数。函数名必须遵循 Lua 的标识符命名规则:
必须以字母或下划线 _ 开头。
可以包含字母、数字和下划线。
区分大小写。
避免使用 Lua 预留关键字作为函数名(例如 function, if, for 等)。
建议使用具有描述性的函数名,清晰表达函数的功能。
(参数列表): 这是函数的形式参数列表,用于接收调用函数时传递的实际参数。参数列表是可选的,如果函数不需要接收任何参数,可以留空。参数之间使用逗号 , 分隔。
-- 函数体 和 end: -- 函数体 到 end 之间是函数的代码块,包含了函数要执行的具体操作。end 关键字标志着函数定义的结束。
[return 返回值列表]: 这是可选的 return 语句,用于指定函数的返回值。函数可以返回零个、一个或多个值。返回值之间使用逗号 , 分隔。如果没有 return 语句,函数默认返回 nil。
代码实践 1:一个简单的问候函数
-- 定义一个名为 greet 的函数,它不接受任何参数, -- 并在控制台打印 "Hello, Lua!" function greet() print("Hello, Lua!") end -- 调用 greet 函数 greet() -- 输出: Hello, Lua!
代码详解 1:
我们使用 function greet() 定义了一个名为 greet 的函数。
函数的参数列表为空 (),表示该函数不接收任何参数。
函数体内部只有一行代码 print("Hello, Lua!"),使用 print 函数将字符串 "Hello, Lua!" 输出到控制台。
通过 greet() 调用该函数,执行函数体内的代码。
2. 函数参数
函数参数允许我们在调用函数时传递数据给函数,使其能够根据不同的输入执行不同的操作。Lua 函数的参数分为形式参数和实际参数。
形式参数 (Parameters): 在函数定义时,参数列表中声明的变量称为形式参数,简称形参。它们就像函数内部的局部变量,用于接收实际参数的值。
实际参数 (Arguments): 在函数调用时,传递给函数的值称为实际参数,简称实参。实参的值会被赋值给形参。
代码实践 2:带参数的加法函数
-- 定义一个名为 add 的函数,它接受两个参数 a 和 b, -- 并返回它们的和 function add(a, b) local sum = a + b return sum end -- 调用 add 函数,传递实际参数 5 和 3 local result = add(5, 3) print("5 + 3 =", result) -- 输出: 5 + 3 = 8 -- 再次调用 add 函数,传递不同的实际参数 result = add(10, -2) print("10 + (-2) =", result) -- 输出: 10 + (-2) = 8
代码详解 2:
function add(a, b) 定义了函数 add,它有两个形参 a 和 b。
在函数体内部,local sum = a + b 计算了 a 和 b 的和,并将结果赋值给局部变量 sum。
return sum 语句将 sum 的值作为函数的返回值。
local result = add(5, 3) 调用 add 函数,传递实参 5 和 3。实参 5 赋值给形参 a,实参 3 赋值给形参 b。函数执行后返回计算结果 8,赋值给变量 result。
add(10, -2) 进行了另一次调用,使用了不同的实参,展示了函数可以处理不同的输入。
2.1. 参数的传递方式
Lua 中函数参数的传递方式是按值传递 (pass-by-value)。这意味着当我们将实参传递给函数时,实际上是将实参值的副本赋值给形参。在函数内部对形参的修改不会影响到函数外部的实参变量。
代码实践 3:参数按值传递示例
function modify_value(x) print("函数内部,修改前 x =", x) x = x + 10 -- 修改形参 x 的值 print("函数内部,修改后 x =", x) end local original_value = 5 print("函数外部,调用前 original_value =", original_value) modify_value(original_value) -- 调用函数,传递 original_value print("函数外部,调用后 original_value =", original_value) -- 输出: -- 函数外部,调用前 original_value = 5 -- 函数内部,修改前 x = 5 -- 函数内部,修改后 x = 15 -- 函数外部,调用后 original_value = 5
代码详解 3:
modify_value(x) 函数接收一个参数 x。
在函数内部,我们修改了形参 x 的值,将其加 10。
modify_value(original_value) 调用函数时,将 original_value (值为 5) 作为实参传递给函数。
函数内部形参 x 的初始值是 5,修改后变为 15。
关键点: 函数调用结束后,函数外部的 original_value 的值仍然是 5,没有受到函数内部对形参 x 修改的影响。这证明了 Lua 函数参数是按值传递的。函数内部操作的是实参值的副本,而不是实参本身。
2.2. 可变参数 (Variadic Functions)
Lua 支持可变参数函数,也称为变长参数函数。这种函数可以接收数量不定的参数。在函数定义时,使用 ... (三个点) 来表示可变参数。
在函数体内部,可以使用 ... 访问传递给函数的所有额外参数。Lua 会将这些参数收集到一个名为 arg 的特殊表中 (在 Lua 5.1 及更早版本中,Lua 5.2 及之后版本推荐使用 {...} 直接创建参数列表)。
代码实践 4:可变参数求和函数
-- 定义一个名为 sum_all 的函数,它可以接收任意数量的数字参数, -- 并返回它们的总和 function sum_all(...) local total = 0 local args = {...} -- 将可变参数收集到一个表中 (Lua 5.2+) for i, v in ipairs(args) do total = total + v end return total end -- 调用 sum_all 函数,传递不同数量的参数 local result1 = sum_all(1, 2, 3) print("sum_all(1, 2, 3) =", result1) -- 输出: sum_all(1, 2, 3) = 6 local result2 = sum_all(10, 20, 30, 40, 50) print("sum_all(10, 20, 30, 40, 50) =", result2) -- 输出: sum_all(10, 20, 30, 40, 50) = 150 local result3 = sum_all() -- 不传递任何参数 print("sum_all() =", result3) -- 输出: sum_all() = 0
代码详解 4:
function sum_all(...) 定义了可变参数函数 sum_all,使用 ... 表示可变参数。
local args = {...} 在函数内部将所有传递给函数的额外参数收集到一个名为 args 的表中。
for i, v in ipairs(args) do ... end 循环遍历 args 表中的每个元素,并将它们累加到 total 变量中。
return total 返回所有参数的总和。
函数可以接收任意数量的参数,包括零个参数。
3. 函数返回值
Lua 函数可以返回零个、一个或多个值。使用 return 语句来指定返回值。
3.1. 返回单个值
当函数只需要返回一个结果时,可以在 return 语句后跟一个表达式,该表达式的值将作为函数的返回值。
代码实践 5:返回最大值的函数
function max(a, b) if a > b then return a else return b end end local larger_number = max(25, 18) print("较大的数是:", larger_number) -- 输出: 较大的数是: 25
代码详解 5:
max(a, b) 函数比较 a 和 b 的大小,并返回较大的那个值。
return a 或 return b 根据条件返回不同的值。
3.2. 返回多个值
Lua 函数可以一次返回多个值。在 return 语句后跟多个表达式,表达式之间用逗号 , 分隔。
代码实践 6:返回商和余数的函数
function divide_and_remainder(dividend, divisor) local quotient = math.floor(dividend / divisor) -- 计算商,向下取整 local remainder = dividend % divisor -- 计算余数 return quotient, remainder -- 返回商和余数 end local q, r = divide_and_remainder(17, 5) print("商 =", q, ", 余数 =", r) -- 输出: 商 = 3 , 余数 = 2 -- 可以只接收部分返回值 local quotient_only = divide_and_remainder(20, 3) print("商 =", quotient_only) -- 输出: 商 = 6 (只接收了第一个返回值)
代码详解 6:
divide_and_remainder(dividend, divisor) 函数计算两个数的商和余数,并使用 return quotient, remainder 返回两个值。
local q, r = divide_and_remainder(17, 5) 调用函数,并使用多重赋值将返回的两个值分别赋值给变量 q 和 r。
local quotient_only = divide_and_remainder(20, 3) 展示了可以只接收函数返回的第一个值,后续的返回值会被忽略。
3.3. 没有 return 语句或 return nil
如果函数中没有 return 语句,或者 return 语句后面没有跟任何表达式,函数默认返回 nil。
代码实践 7:没有 return 语句的函数
function no_return() print("这个函数没有显式返回值") end local result = no_return() print("函数返回值 =", result) -- 输出: 函数返回值 = nil
代码详解 7:
no_return() 函数没有 return 语句。
调用 no_return() 函数后,将返回值赋值给 result,result 的值为 nil,证明函数默认返回 nil。
4. 匿名函数 (Anonymous Functions) 和函数字面量 (Function Literals)
在 Lua 中,函数也可以是匿名的,即没有函数名。匿名函数也被称为函数字面量。我们可以像定义其他字面量值(如数字或字符串)一样定义函数。
匿名函数通常用于以下场景:
作为参数传递给其他函数(高阶函数): 例如,作为 table.sort 的比较函数。
创建闭包 (closures): 匿名函数是创建闭包的关键。
在表 (table) 中存储函数: 方便组织和调用相关功能。
匿名函数的语法与普通函数定义类似,只是省略了函数名:
function (参数列表) -- 函数体 [return 返回值列表] end
代码实践 8:匿名函数示例
-- 将一个匿名函数赋值给变量 my_anonymous_function local my_anonymous_function = function(name) print("Hello, " .. name .. "!") end -- 调用匿名函数 my_anonymous_function("Alice") -- 输出: Hello, Alice! -- 匿名函数作为 table.sort 的比较函数 local numbers = {5, 2, 8, 1, 4} table.sort(numbers, function(a, b) return a < b -- 升序排序 end) print("排序后的 numbers:", table.concat(numbers, ", ")) -- 输出: 排序后的 numbers: 1, 2, 4, 5, 8
代码详解 8:
local my_anonymous_function = function(name) ... end 定义了一个匿名函数,并将其赋值给变量 my_anonymous_function。现在 my_anonymous_function 就相当于函数名,可以像普通函数一样调用。
table.sort(numbers, function(a, b) ... end) 使用了匿名函数作为 table.sort 的第二个参数,即比较函数。这个匿名函数定义了排序的规则(升序)。
5. 函数的作用域 (Scope) 和闭包 (Closures)
5.1. 词法作用域 (Lexical Scoping)
Lua 使用词法作用域,也称为静态作用域。这意味着变量的作用域在代码编写时就已经确定了,而不是在运行时动态确定的。函数可以访问在其定义时所处作用域内的变量。
代码实践 9:词法作用域示例
local outer_variable = "外部变量" function outer_function() local inner_variable = "内部变量" function inner_function() print("访问外部变量:", outer_variable) -- 可以访问 outer_variable print("访问内部变量:", inner_variable) -- 可以访问 inner_variable end inner_function() -- 在 outer_function 内部调用 inner_function end outer_function() -- 输出: -- 访问外部变量: 外部变量 -- 访问内部变量: 内部变量
代码详解 9:
outer_variable 在 outer_function 外部定义,属于全局作用域。
inner_variable 在 outer_function 内部定义,属于 outer_function 的局部作用域。
inner_function 定义在 outer_function 内部。
inner_function 可以访问 outer_variable (外部变量) 和 inner_variable (内部变量)。这是因为 Lua 的词法作用域规则,inner_function 可以访问其定义时所处的作用域(即 outer_function 的作用域)以及更外层的作用域。
5.2. 闭包 (Closures)
闭包是一种特殊的函数,它可以记住并访问在其创建时所处作用域内的变量,即使在其创建作用域之外被调用时仍然可以访问这些变量。闭包是由函数及其相关的环境 (enclosing environment) 组成的。
代码实践 10:闭包示例
function create_counter() local count = 0 -- 计数器变量,在 create_counter 函数内部 -- 返回一个匿名函数,作为闭包 return function() count = count + 1 return count end end -- 创建两个计数器闭包 local counter1 = create_counter() local counter2 = create_counter() print("counter1 第 1 次调用:", counter1()) -- 输出: counter1 第 1 次调用: 1 print("counter1 第 2 次调用:", counter1()) -- 输出: counter1 第 2 次调用: 2 print("counter2 第 1 次调用:", counter2()) -- 输出: counter2 第 1 次调用: 1 print("counter1 第 3 次调用:", counter1()) -- 输出: counter1 第 3 次调用: 3 print("counter2 第 2 次调用:", counter2()) -- 输出: counter2 第 2 次调用: 2
代码详解 10:
create_counter() 函数内部定义了局部变量 count,并返回一个匿名函数。
返回的匿名函数(闭包)可以访问和修改 create_counter() 函数作用域内的 count 变量。
每次调用 create_counter() 都会创建一个新的闭包,并且每个闭包都拥有自己独立的 count 变量副本。
counter1 和 counter2 是两个独立的计数器,它们互不影响,各自维护自己的计数状态。这就是闭包的强大之处:状态保持。
6. 函数的调用和尾调用优化 (Tail Call Optimization)
6.1. 函数调用
要调用一个函数,只需使用函数名后跟一对圆括号 ()。如果函数需要参数,将实际参数放在圆括号内,参数之间用逗号 , 分隔。
代码实践 11:函数调用示例
function multiply(x, y) return x * y end local product = multiply(7, 6) -- 调用 multiply 函数 print("7 * 6 =", product) -- 输出: 7 * 6 = 42
6.2. 尾调用优化 (Tail Call Optimization)
尾调用是指在一个函数的最后一步操作是调用另一个函数,并且对后者的返回值不做任何额外操作,直接作为前者的返回值返回。在满足特定条件下,Lua 解释器可以对尾调用进行优化,避免创建新的栈帧,从而节省内存空间,防止栈溢出。
尾调用优化的条件:
函数调用必须是函数体的最后一步操作。
尾调用返回的结果必须直接作为当前函数的返回值。
尾调用不能涉及任何额外的计算或操作。
代码实践 12:尾调用示例
function factorial_tail_recursive(n, accumulator) if n == 0 then return accumulator -- 尾调用,直接返回 accumulator else return factorial_tail_recursive(n - 1, n * accumulator) -- 尾调用 end end function factorial(n) return factorial_tail_recursive(n, 1) -- 初始调用 end local result = factorial(5) print("5! =", result) -- 输出: 5! = 120
代码详解 12:
factorial_tail_recursive 函数使用尾递归的方式计算阶乘。在 else 分支中,return factorial_tail_recursive(n - 1, n * accumulator) 是一个尾调用,因为这是函数体的最后一步操作,并且返回值直接作为当前函数的返回值。
factorial 函数作为初始入口,调用 factorial_tail_recursive 并设置初始累加器值为 1。
尾调用优化使得递归调用不会无限增长栈空间,可以计算较大的阶乘值而不会栈溢出。
总结
本文详细介绍了 Lua 函数的定义,包括基本语法、参数、返回值、匿名函数、作用域、闭包和尾调用优化等关键概念。理解和熟练运用函数是 Lua 编程的基础,也是构建复杂程序的核心能力。通过本文的学习和代码实践,相信你已经对 Lua 函数有了更深入的理解,并能更好地运用函数来组织和编写高效、可维护的 Lua 代码。记住,函数是 Lua 中“第一类公民”,灵活运用函数,可以让你充分发挥 Lua 语言的强大功能。