Lua 是一门由巴西里约热内卢天主教大学(PUC-Rio)的 Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo 于 1993 年开发的轻量级、高效、可嵌入的脚本语言。它的设计目标是成为一门 "胶水语言",能够轻松嵌入到其他应用程序中,提供灵活的扩展能力。
经过 30 多年的发展,Lua 已经成为全球最流行的嵌入式脚本语言之一,广泛应用于游戏开发(Roblox、魔兽世界、Unity)、Web 开发(OpenResty)、数据库(Redis、Tarantool)、编辑器扩展(Neovim)以及嵌入式设备等领域。Lua 的核心优势在于其极小的体积(编译后仅几百 KB)、极快的执行速度(基于寄存器的虚拟机)、卓越的可移植性以及简单优雅的语法。
本文将系统讲解 Lua 脚本的核心知识,从基础语法到高级特性,再到企业级实战应用,帮助你全面掌握这门强大的语言。
一、Lua 基础语法与核心特性
Lua 是一门动态类型、过程式、函数式的语言,语法简洁优雅,学习曲线平缓。
1.1 变量与数据类型
Lua 有 8 种基本数据类型:nil、boolean、number、string、function、userdata、thread 和 table。
(1)变量声明
Lua 中的变量默认是全局变量,除非用 local 关键字显式声明为局部变量。强烈建议尽量使用局部变量,因为局部变量的访问速度比全局变量快得多,而且可以避免命名空间污染。
Lua
-- 全局变量(不推荐)
global_var = "I am global"
-- 局部变量(推荐)
local local_var = "I am local"
-- 同时声明多个局部变量
local a, b, c = 1, 2, 3
(2)基本数据类型详解
- nil :表示空值,只有一个值
nil。将变量赋值为nil相当于删除该变量。 - boolean :有两个值
true和false。注意:在 Lua 中,只有nil和false被视为假,其他所有值(包括 0、空字符串、空表)都被视为真。 - number:表示数字,Lua 5.3 之前只有双精度浮点数,Lua 5.3 引入了整数类型。
- string:表示字符串,是不可变的。可以用单引号、双引号或双方括号(用于多行字符串)表示。
Lua
local str1 = 'hello'
local str2 = "world"
local str3 = [[
这是一个
多行字符串
]]
-- 字符串拼接用 .. 运算符
local hello_world = str1 .. " " .. str2
print(hello_world) -- 输出:hello world
1.2 运算符
Lua 的运算符包括算术运算符、关系运算符、逻辑运算符、连接运算符和长度运算符。其中,逻辑运算符的行为比较特殊,需要特别注意。
(1)逻辑运算符的短路求值
Lua 的逻辑运算符 and 和 or 不会返回布尔值,而是返回操作数本身:
a and b:如果a为真,返回b;否则返回aa or b:如果a为真,返回a;否则返回b
这个特性非常有用,可以用来实现默认值:
Lua
-- 如果 x 为 nil 或 false,将其设为 10
local x = x or 10
(2)长度运算符
长度运算符 # 用于获取字符串的长度或 table 的数组部分的长度。注意:# 只对连续的数组部分有效,遇到 nil 会停止计数。
Lua
local arr1 = {1, 2, 3, 4, 5}
print(#arr1) -- 输出:5
local arr2 = {1, 2, nil, 4, 5}
print(#arr2) -- 输出:2(遇到 nil 停止)
1.3 控制结构
Lua 提供了 if-elseif-else、while、repeat-until 和 for 四种控制结构。
(1)for 循环
Lua 有两种 for 循环:数值 for 和泛型 for。
数值 for:
Lua
-- 从 1 到 10,步长为 1
for i = 1, 10 do
print(i)
end
-- 从 10 到 1,步长为 -1
for i = 10, 1, -1 do
print(i)
end
泛型 for:
泛型 for 通过迭代器函数遍历集合。Lua 提供了两个常用的迭代器:ipairs 和 pairs。
ipairs:遍历 table 的数组部分,按顺序遍历,遇到nil停止pairs:遍历 table 的所有键值对,顺序不定
Lua
local t = {1, 2, 3, a = 4, b = 5}
-- 用 ipairs 遍历数组部分
print("ipairs:")
for i, v in ipairs(t) do
print(i, v) -- 输出:1 1; 2 2; 3 3
end
-- 用 pairs 遍历所有键值对
print("pairs:")
for k, v in pairs(t) do
print(k, v) -- 输出:1 1; 2 2; 3 3; a 4; b 5(顺序不定)
end
二、Table:Lua 的万能数据结构
Table 是 Lua 中唯一的复杂数据结构,它既可以作为数组使用,也可以作为哈希表使用,还可以用来模拟对象、类、命名空间等。
2.1 Table 的基本使用
Lua
-- 创建一个空 table
local t1 = {}
-- 创建一个数组(数字索引从 1 开始!)
local arr = {10, 20, 30, 40, 50}
print(arr[1]) -- 输出:10(注意不是 0)
-- 创建一个哈希表
local dict = {
name = "Lua",
version = "5.4",
author = "Roberto Ierusalimschy"
}
print(dict.name) -- 输出:Lua
print(dict["version"]) -- 输出:5.4
-- 混合使用数组和哈希表
local mixed = {1, 2, 3, name = "mixed", type = "table"}
2.2 Table 的常用操作
Lua 标准库提供了一些常用的 table 操作函数:
Lua
local t = {1, 2, 3}
-- 在末尾插入元素
table.insert(t, 4)
print(table.concat(t, ", ")) -- 输出:1, 2, 3, 4
-- 在指定位置插入元素
table.insert(t, 2, 1.5)
print(table.concat(t, ", ")) -- 输出:1, 1.5, 2, 3, 4
-- 删除指定位置的元素
table.remove(t, 2)
print(table.concat(t, ", ")) -- 输出:1, 2, 3, 4
-- 排序
local nums = {3, 1, 4, 1, 5, 9, 2, 6}
table.sort(nums)
print(table.concat(nums, ", ")) -- 输出:1, 1, 2, 3, 4, 5, 6, 9
2.3 元表(Metatable)与元方法(Metamethod)
元表是 Lua 最强大的特性之一,它允许我们改变 table 的行为。元表是一个普通的 table,包含了各种元方法,当对 table 执行某些操作时,Lua 会自动调用对应的元方法。
(1)__index 元方法
当访问 table 中不存在的键时,Lua 会调用该 table 的元表的 __index 元方法。__index 可以是一个函数,也可以是另一个 table。
Lua
-- 用 __index 实现默认值
local default_mt = {__index = function(t, k) return "default" end}
local t = setmetatable({a = 1, b = 2}, default_mt)
print(t.a) -- 输出:1
print(t.b) -- 输出:2
print(t.c) -- 输出:default(不存在的键返回默认值)
(2)__add 元方法
当对两个 table 使用 + 运算符时,Lua 会调用第一个 table 的元表的 __add 元方法。
Lua
-- 实现向量相加
local Vector = {}
Vector.__add = function(v1, v2)
return {v1[1] + v2[1], v1[2] + v2[2]}
end
local v1 = setmetatable({1, 2}, Vector)
local v2 = setmetatable({3, 4}, Vector)
local v3 = v1 + v2
print(v3[1], v3[2]) -- 输出:4 6
(3)__tostring 元方法
当将 table 转换为字符串时(比如 print 函数),Lua 会调用元表的 __tostring 元方法。
Lua
local Person = {}
Person.__tostring = function(p)
return string.format("Person{name=%s, age=%d}", p.name, p.age)
end
local p = setmetatable({name = "Alice", age = 25}, Person)
print(p) -- 输出:Person{name=Alice, age=25}
三、函数:一等公民的魅力
在 Lua 中,函数是一等公民,这意味着函数可以作为参数传递给其他函数,可以作为返回值从函数中返回,也可以存储在变量中。
3.1 多返回值
Lua 允许函数返回多个值,这是一个非常方便的特性。
Lua
-- 同时返回最大值和最小值
local function max_min(a, b)
if a > b then
return a, b
else
return b, a
end
end
local max, min = max_min(10, 20)
print(max, min) -- 输出:20 10
3.2 可变参数
用 ... 表示可变参数,可以用**{...}** 将可变参数转换为 table,或者用 select 函数获取指定位置的参数。
Lua
-- 计算任意多个数的和
local function sum(...)
local total = 0
for _, v in ipairs({...}) do
total = total + v
end
return total
end
print(sum(1, 2, 3, 4, 5)) -- 输出:15
3.3 闭包(Closure)
如果一个函数内部定义了另一个函数,并且内部函数访问了外部函数的局部变量,那么内部函数就是一个闭包。闭包会捕获外部函数的局部变量,即使外部函数已经执行完毕,这些变量依然存在。
Lua
-- 实现计数器
local function create_counter()
local count = 0
return function()
count = count + 1
return count
end
end
local counter1 = create_counter()
print(counter1()) -- 输出:1
print(counter1()) -- 输出:2
print(counter1()) -- 输出:3
local counter2 = create_counter()
print(counter2()) -- 输出:1(独立的计数器)
3.4 尾调用优化
当一个函数的最后一个动作是调用另一个函数时,Lua 不会创建新的栈帧,而是复用当前的栈帧,这就是尾调用优化。尾调用优化可以避免递归调用时的栈溢出问题。
Lua
-- 用尾调用实现阶乘
local function factorial(n, acc)
acc = acc or 1
if n <= 1 then
return acc
end
return factorial(n - 1, n * acc) -- 尾调用
end
print(factorial(1000)) -- 不会栈溢出
3.5 常见的工具函数
(1)tonumber:安全的数值类型转换
tonumber 是 Lua 唯一的官方数值转换函数,用于将其他类型(主要是字符串)的值转换为 number 类型。与其他语言的强制转换不同,Lua 的 tonumber 采用失败返回 nil 的设计,而非抛出异常,这让它在处理不确定输入时非常安全。
1.1 基本语法
Lua
tonumber(value [, base])
- 参数 :
value:要转换的值,可以是字符串、数字、布尔值等任意类型base(可选):进制基数,范围 2~36,默认值为 10
- 返回值 :
- 转换成功:返回对应的
number类型值 - 转换失败:返回
nil(不会抛出任何错误)
- 转换成功:返回对应的
1.2 基础用法示例
Lua
-- 字符串转数字(默认十进制)
print(tonumber("123")) -- 123
print(tonumber("-45.67")) -- -45.67
print(tonumber(" 89 ")) -- 89(自动忽略前后空白字符)
-- 布尔值转换
print(tonumber(true)) -- 1
print(tonumber(false)) -- 0
-- nil 转换
print(tonumber(nil)) -- nil
-- 转换失败的情况
print(tonumber("abc123")) -- nil(包含非数字字符)
print(tonumber("123abc")) -- nil(即使开头是数字也不行)
print(tonumber({})) -- nil(table 无法转换)
print(tonumber(function() end)) -- nil(函数无法转换)
1.3 强大的进制转换功能
tonumber 最实用的特性是支持任意进制(2~36)的字符串转十进制数字,这在处理二进制、十六进制数据时非常方便。
Lua
-- 二进制转十进制
print(tonumber("1010", 2)) -- 10
-- 八进制转十进制
print(tonumber("777", 8)) -- 511
-- 十六进制转十进制(大小写不敏感)
print(tonumber("FF", 16)) -- 255
print(tonumber("0xff", 16)) -- 255(支持 0x 前缀)
-- 三十六进制转十进制(包含字母 a-z)
print(tonumber("z", 36)) -- 35
print(tonumber("10", 36)) -- 36
-- 超出进制范围的字符会转换失败
print(tonumber("2", 2)) -- nil(二进制只能包含 0 和 1)
print(tonumber("g", 16)) -- nil(十六进制只能包含 0-9 和 a-f)
1.4 常见坑与最佳实践
-
必须检查返回值是否为 nil 这是最容易犯的错误。如果直接使用
tonumber的结果而不检查 nil,后续代码会因为对 nil 进行算术运算而崩溃。Lua-- 错误写法 local num = tonumber(input) local result = num + 10 -- 如果 input 不是数字,这里会报错:attempt to perform arithmetic on a nil value -- 正确写法 local num = tonumber(input) if not num then error("无效的数字输入: " .. tostring(input)) end local result = num + 10 -
不要用
tonumber判断是否为数字 对于已经是number类型的值,tonumber会直接返回它本身,所以可以用type(x) == "number"来判断类型,而不是tonumber(x) ~= nil。 -
空字符串和空白字符串的区别
Luaprint(tonumber("")) -- nil(空字符串转换失败) print(tonumber(" ")) -- nil(全空白字符串也转换失败)
(2)pcall:保护式函数调用
Lua 是一门动态语言,运行时错误非常常见。如果直接调用一个可能出错的函数,错误会向上传播直到程序崩溃。pcall (protected call)的作用就是在保护模式下调用函数,捕获所有运行时错误,让程序可以继续执行。
2.1 基本语法
Lua
success, result1, result2, ... = pcall(func, arg1, arg2, ...)
- 参数 :
func:要调用的函数(注意:是函数本身,不是函数调用的结果)arg1, arg2, ...:传递给func的参数
- 返回值 :
- 第一个返回值:布尔值,表示函数是否执行成功
- 后续返回值:如果成功,是函数的所有返回值;如果失败,是错误信息
2.2 基础用法示例
Lua
-- 定义一个可能出错的函数
local function divide(a, b)
if b == 0 then
error("除数不能为零") -- 主动抛出错误
end
return a / b
end
-- 成功调用
local ok, result = pcall(divide, 10, 2)
print(ok, result) -- true 5
-- 失败调用
local ok, err = pcall(divide, 10, 0)
print(ok, err) -- false 除数不能为零
2.3 传递多个参数和接收多个返回值
pcall 支持传递任意数量的参数给被调用函数,也能接收函数的多个返回值:
Lua
local function multi_return(a, b)
return a + b, a - b, a * b
end
local ok, sum, diff, product = pcall(multi_return, 10, 5)
if ok then
print(sum, diff, product) -- 15 5 50
end
2.4 常见坑
-
不要把函数调用传给 pcall 这是最致命的错误。如果写成
pcall(func(10, 2)),Lua 会先执行func(10, 2),如果出错,错误会在pcall执行前就抛出,pcall根本无法捕获。Lua-- 错误写法 local ok, err = pcall(divide(10, 0)) -- 这里会直接报错,pcall 不会执行 -- 正确写法 local ok, err = pcall(divide, 10, 0) -
pcall 无法捕获语法错误
pcall只能捕获运行时错误 ,语法错误在编译阶段就会发生,无法被pcall捕获。-- 这个错误无法被捕获,因为语法错误在编译时就发生了 local ok, err = pcall(function() local a = 1 + -- 语法错误:缺少右操作数 end) -
pcall 会清空堆栈信息 这是
pcall最大的缺点:当函数出错时,pcall只会返回错误信息字符串,不会保留堆栈跟踪,这让调试变得非常困难。要解决这个问题,需要使用xpcall。
(3)xpcall:带错误处理的保护调用
xpcall (extended protected call)是 pcall 的增强版,它允许你指定一个错误处理函数,在错误发生时被调用。错误处理函数可以获取完整的堆栈信息,或者执行清理工作(比如关闭文件、释放资源)。
3.1 基本语法
Lua
success, result1, result2, ... = xpcall(func, error_handler, arg1, arg2, ...)
- 参数 :
func:要调用的函数error_handler:错误处理函数,当func出错时会被调用,参数是错误信息arg1, arg2, ...:传递给func的参数
- 返回值 :和
pcall完全相同
3.2 打印堆栈跟踪的标准写法
这是 xpcall 最常用的场景,利用 debug.traceback 函数获取完整的堆栈信息:
Lua
local function risky_function()
local a = nil
return a + 1 -- 这里会出错:attempt to perform arithmetic on a nil value
end
-- 错误处理函数:打印错误信息和堆栈跟踪
local function error_handler(err)
return err .. "\n" .. debug.traceback()
end
local ok, err = xpcall(risky_function, error_handler)
if not ok then
print("发生错误:")
print(err)
end
输出结果会包含完整的调用栈,方便定位错误位置:
bash
发生错误:
attempt to perform arithmetic on a nil value
stack traceback:
[C]: in metamethod '__add'
test.lua:3: in function <test.lua:2>
[C]: in function 'xpcall'
test.lua:10: in main chunk
[C]: in ?
3.3 错误处理函数的注意事项
- 错误处理函数本身如果出错,
xpcall不会再捕获这个错误,而是直接抛出。 - 错误处理函数的返回值会作为
xpcall的第二个返回值(即错误信息)。 - 错误处理函数是在错误发生的上下文环境中执行的,可以访问出错时的局部变量(但不建议这么做)。
四、面向对象编程:用 Table 模拟类与继承
Lua 本身没有内置的类和对象机制,但可以用 table 和元表轻松模拟面向对象编程。
4.1 类的实现
我们可以将类定义为一个包含方法和构造函数的 table,然后通过元表让实例继承类的方法。
Lua
-- 定义 Person 类
local Person = {}
Person.__index = Person
-- 构造函数
function Person.new(name, age)
local self = setmetatable({}, Person)
self.name = name
self.age = age
return self
end
-- 方法
function Person:say_hello()
print(string.format("Hello, my name is %s, I'm %d years old.", self.name, self.age))
end
-- 创建实例
local alice = Person.new("Alice", 25)
local bob = Person.new("Bob", 30)
alice:say_hello() -- 输出:Hello, my name is Alice, I'm 25 years old.
bob:say_hello() -- 输出:Hello, my name is Bob, I'm 30 years old.
4.2 继承的实现
通过将子类的元表的 __index 指向父类,可以实现继承。
Lua
-- 定义 Student 类,继承自 Person
local Student = {}
Student.__index = Student
setmetatable(Student, Person) -- 继承 Person
-- 构造函数
function Student.new(name, age, grade)
local self = Person.new(name, age) -- 调用父类构造函数
setmetatable(self, Student)
self.grade = grade
return self
end
-- 重写方法
function Student:say_hello()
print(string.format("Hello, my name is %s, I'm %d years old, I'm in grade %d.",
self.name, self.age, self.grade))
end
-- 新增方法
function Student:study()
print(string.format("%s is studying in grade %d.", self.name, self.grade))
end
-- 创建实例
local charlie = Student.new("Charlie", 15, 9)
charlie:say_hello() -- 输出:Hello, my name is Charlie, I'm 15 years old, I'm in grade 9.
charlie:study() -- 输出:Charlie is studying in grade 9.
五、协程(Coroutine):轻量级线程
Lua 的协程是一种轻量级的线程,和操作系统线程不同,协程是用户态的,由 Lua 虚拟机调度,开销非常小。一个 Lua 程序可以同时运行成千上万个协程。
5.1 协程的基本操作
协程有四种状态:suspended(挂起) 、running(运行) 、normal(正常) 和 dead(死亡)。
Lua
-- 创建协程
local co = coroutine.create(function(a, b)
print("co: ", a, b)
local c = coroutine.yield(a + b) -- 挂起协程,返回 a+b
print("co: ", c)
return a * b
end)
-- 启动协程
local success, result = coroutine.resume(co, 10, 20)
print("main: ", success, result) -- 输出:co: 10 20; main: true 30
-- 恢复协程
success, result = coroutine.resume(co, 30)
print("main: ", success, result) -- 输出:co: 30; main: true 200
5.2 生产者 - 消费者模型
协程非常适合实现生产者 - 消费者模型,生产者生产数据,然后通过**yield** 交出控制权,消费者消费数据,然后通过 resume 恢复生产者。
Lua
-- 生产者
local function producer()
for i = 1, 5 do
print("Produced: ", i)
coroutine.yield(i) -- 生产数据,挂起
end
end
-- 消费者
local function consumer(prod)
while true do
local success, data = coroutine.resume(prod)
if not success or data == nil then
break
end
print("Consumed: ", data)
end
end
-- 运行
local prod = coroutine.create(producer)
consumer(prod)
六、企业级实战:Lua 在 Redis 中的应用
Redis 是目前最流行的内存数据库,它内置了 Lua 脚本支持。Lua 脚本在 Redis 服务器端原子性执行,不会被其他命令打断,非常适合实现复杂的原子操作。
6.1 分布式锁的实现
用 Lua 脚本可以实现一个原子性的分布式锁,避免多个客户端同时获取锁。
Lua
-- 获取锁
-- KEYS[1]: 锁的 key
-- ARGV[1]: 锁的值(用于释放锁时验证)
-- ARGV[2]: 过期时间(毫秒)
local lock_key = KEYS[1]
local lock_value = ARGV[1]
local expire_time = ARGV[2]
-- 如果锁不存在,设置锁并设置过期时间
if redis.call("SET", lock_key, lock_value, "NX", "PX", expire_time) then
return 1
else
return 0
end
Lua
-- 释放锁
-- KEYS[1]: 锁的 key
-- ARGV[1]: 锁的值(必须和获取锁时的值一致)
local lock_key = KEYS[1]
local lock_value = ARGV[1]
-- 如果锁存在且值匹配,删除锁
if redis.call("GET", lock_key) == lock_value then
redis.call("DEL", lock_key)
return 1
else
return 0
end
6.2 原子计数器
用 Lua 脚本可以实现一个原子性的计数器,避免并发问题。
Lua
-- 原子递增计数器
-- KEYS[1]: 计数器的 key
-- ARGV[1]: 递增的步长(可选,默认 1)
local counter_key = KEYS[1]
local step = tonumber(ARGV[1]) or 1
local new_value = redis.call("INCRBY", counter_key, step)
return new_value
6.3 Redis Lua 脚本中的错误处理
在 Redis 中执行 Lua 脚本时,任何未被捕获的错误都会导致脚本终止,并返回错误给客户端。因此,pcall 和 **xpcall**在 Redis 脚本中非常重要。
Lua
-- Redis 脚本:安全的原子递增操作
local key = KEYS[1]
local step = tonumber(ARGV[1]) or 1
-- 用 pcall 保护 Redis 命令调用
local ok, result = pcall(redis.call, "INCRBY", key, step)
if not ok then
-- 如果出错,返回错误信息
return redis.error_reply("递增失败: " .. result)
end
return result
6.4 配置文件解析中的类型转换
在解析配置文件时,输入通常是字符串,需要用 **tonumber**安全地转换为数值类型:
Lua
local config = {
port = "8080",
timeout = "3000",
max_connections = "100"
}
-- 安全转换配置值
local port = tonumber(config.port)
if not port or port < 1 or port > 65535 then
error("无效的端口号: " .. tostring(config.port))
end
local timeout = tonumber(config.timeout) or 5000 -- 提供默认值
七、性能优化与最佳实践
7.1 性能优化技巧
- 尽量使用局部变量 :局部变量的访问速度比全局变量快 30% 以上,因为全局变量需要在
_G表中进行哈希查找。 - 预分配 table 大小 :当知道 table 的大致大小时,用
table.create(narray, nhash)预分配空间,避免动态扩容的开销。 - 避免频繁的字符串拼接 :Lua 的字符串是不可变的,每次拼接都会创建新的字符串。拼接大量字符串时,应该使用
table.concat。 - 减少函数调用开销:将常用的函数赋值给局部变量,避免每次调用都进行全局查找。
- 合理使用协程:用协程实现异步操作,避免阻塞,提高程序的并发性能。
7.2 常见坑与最佳实践
- 不要用
#计算包含nil的数组长度 :#运算符遇到nil会停止计数,应该自己维护一个长度变量。 - 避免全局变量污染 :所有变量都应该用
local声明,模块只导出必要的接口。 - 注意闭包的内存泄漏:闭包会捕获外部变量,如果闭包被长期持有,外部变量也不会被垃圾回收。
- 正确使用
ipairs和pairs:ipairs只遍历数组部分,pairs遍历所有键值对。 - 错误处理要及时 :用
pcall或xpcall捕获可能出错的操作,避免程序崩溃。
八、总结
Lua 是一门设计优雅、功能强大的轻量级脚本语言。它的核心特性 ------ table、函数、元表、协程,共同构成了一个灵活而强大的编程模型。虽然 Lua 不是一门全能的语言,但在嵌入式、高性能、轻量级的场景下,它有着不可替代的优势。