20.总结
20.1 核心要点速览
分组、注释和打印
| 功能 | 语法/示例 | 说明 |
|---|---|---|
| 编辑器分组 | --#region / --#endregion |
主要给编辑器折叠用,不是 Lua 语言本身的特殊语法 |
| 单行注释 | -- 注释内容 |
最常用,适合解释代码、临时屏蔽一行逻辑 |
| 多行注释 | --[[ 多行注释内容 ]] |
多行注释最常见写法;]]--、--]] --[[ ... ]] |
| 打印输出 | print("Hello Lua") |
可以传多个参数,调试时建议把变量含义一起打出来 |
print("playerId:", playerId)
print("当前状态:", state)
简单变量类型
Lua 常见类型有 8 种:
| 类型 | 说明 | 常见场景 |
|---|---|---|
nil |
空值,未声明变量默认也是 nil |
表示没有值、删除 table 字段 |
boolean |
true / false |
条件判断 |
number |
数值类型,整数和小数都当 number |
数值计算 |
string |
字符串,单引号、双引号都可以 | 文本、日志、配置 key |
function |
函数也是值 | 回调、闭包、模块接口 |
table |
Lua 最核心的复合结构 | 数组、字典、对象、配置 |
userdata |
宿主环境暴露给 Lua 的对象 | C/C++ 扩展、Unity 对象绑定 |
thread |
Lua 协程类型 | coroutine |
真值规则:
只有 nil 和 false 是假。其它都是真。
if 0 then
print("0 也是真")
end
if "" then
print("空字符串也是真")
end
if {} then
print("空表也是真")
end
判断“是否为 0 / 空字符串 / 空表”要写明确条件,别被 C# / C++ 习惯带偏:
if num ~= 0 then
print("num 不是 0")
end
if str ~= "" then
print("str 不是空字符串")
end
表字段赋 nil 一般等于删掉字段:
local t = { id = 1 }
t.id = nil
print(t.id) -- nil
字符串操作
str1 = "双引号字符串"
str2 = '单引号字符串'
str3 = [[
多行字符串
]]
# 是字节长度,不是中文字符数:
local str = "你好"
print(#str) -- UTF-8 下通常是 6
拼接用 ..;number 通常可参与拼接,nil 不行:
print("123" .. "456") -- 123456
local value = nil
-- print("value = " .. value) -- 报错
print("value = " .. tostring(value)) -- value = nil
| 方法 | 作用 | 注意 |
|---|---|---|
string.upper |
转大写 | 主要针对 ASCII 字母 |
string.lower |
转小写 | 主要针对 ASCII 字母 |
string.reverse |
字符串反转 | 按字节反转,中文容易乱码 |
string.find |
查找子串 | 返回起始和结束索引 |
string.sub |
截取字符串 | 索引从 1 开始,支持负数 |
string.rep |
重复字符串 | 适合简单重复拼接 |
string.gsub |
替换字符串 | 返回新字符串和替换次数 |
string.byte |
字符转 ASCII / 字节值 | 多字节字符要小心 |
string.char |
ASCII / 字节值转字符 | 可传多个参数 |
大量拼接、中文长度、富文本截断通常要封装工具函数。
运算符
| 类型 | 运算符 | 说明 |
|---|---|---|
| 算术 | + - * / % ^ |
加减乘除、取余、幂 |
| 比较 | == ~= > < >= <= |
不等于是 ~= |
| 逻辑 | and or not |
短路运算,返回原始值,不一定返回布尔 |
| 拼接 | .. |
字符串拼接 |
| 位运算 | Lua 5.3+:&、|、~、<<、>> |
Lua 5.1 / LuaJIT 项目不要默认可用 |
Lua 没有 a++、a += 1 等写法,要写成 a = a + 1。
and / or 返回值:
a and b:a 为假返回 a,a 为真返回 b。
a or b:a 为真返回 a,a 为假返回 b。
local value = input or defaultValue
false 是合法值时不能这么兜底:
if input == nil then
value = defaultValue
else
value = input
end
条件分支语句
if 条件1 then
-- 分支1
elseif 条件2 then
-- 分支2
else
-- 分支3
end
elseif,不是else if。- 只有
nil和false是假;0、""、{}都是真。 - 多分支从上往下,命中一个就不再往下。
循环语句
| 类型 | 语法 | 执行逻辑 | 注意 |
|---|---|---|---|
while |
while 条件 do ... end |
先判断,再执行 | 条件一开始不成立就一次都不执行 |
repeat |
repeat ... until 条件 |
先执行,再判断 | 条件为真时退出,和 C# do...while 容易混 |
数值 for |
for i = start, stop, step do ... end |
按步长变化 | 步长方向和起止值对不上时,循环不执行 |
local num = 0
while num < 5 do
print(num)
num = num + 1
end
local num = 0
repeat
print(num)
num = num + 1
until num > 5
for i = 1, 5, 1 do
print(i)
end
数值 for:步长为正,变量大于终止值退出;步长为负,变量小于终止值退出;方向不对直接跳过。死循环配合 break。
函数
function Add(a, b)
return a + b
end
local func = function()
print("匿名函数")
end
函数是值,可赋变量、传参、返回。
| 情况 | 结果 |
|---|---|
| 少传参数 | 缺少的位置补 nil |
| 多传参数 | 多出来的参数被丢弃 |
| 参数类型不匹配 | Lua 不会提前检查,运行时逻辑自己负责 |
function Test(a)
print(a)
end
Test() -- nil
Test(1, 2, 3) -- 1
function GetResult()
return true, "ok"
end
local success, message = GetResult()
闭包捕获外部变量并延长其生命周期:
function MakeAdder(x)
return function(y)
return x + y
end
end
local add10 = MakeAdder(10)
print(add10(5)) -- 15
表
数组、字典、对象、配置都靠 table。
local arr = { 1, 2, 3 }
print(arr[1]) -- 1
local player = {
id = 1001,
name = "Tom"
}
print(player.id)
print(player["name"])
表赋值是引用传递:
local a = { name = "A" }
local b = a
b.name = "B"
print(a.name) -- B
| 点 | 结论 |
|---|---|
#t |
只适合连续数组,有洞 table 不要依赖结果 |
pairs |
遍历所有键值对,但顺序不保证 |
ipairs |
从 1 开始遍历,遇到中间 nil 通常会停 |
table.insert(t, value) |
尾插 |
table.insert(t, pos, value) |
中间插入,后面的元素会移动 |
table.remove(t, pos) |
删除指定位置,后面的元素会前移 |
for i = 1, #arr do
print(arr[i])
end
for i, v in ipairs(arr) do
print(i, v)
end
for k, v in pairs(player) do
print(k, v)
end
要固定顺序自己维护 key 列表,别依赖 pairs 顺序。
模块与多脚本
a = 1 -- 全局,进 _G
local a = 1 -- 局部
local TestModule = require("TestModule")
- 第一次加载执行脚本,可
return模块表。 - 结果缓存在
package.loaded。 - 第二次
require直接返回缓存。
package.loaded["TestModule"] = nil
local TestModule = require("TestModule")
新模块写法:
local M = {}
function M.Test()
print("Test")
end
return M
老 5.1 代码可能看到 module("TestModule", package.seeall),要认识但新代码不建议继续用。
特殊语法
a, b, c = 1, 2
print(a) -- 1
print(b) -- 2
print(c) -- nil
a, b = b, a
function Test()
return 1, 2, 3
end
local a, b = Test()
print(a) -- 1
print(b) -- 2
and / or 模拟三目,trueValue 可能是 false 或 nil 时会失效:
local result = condition and trueValue or falseValue
local result
if condition then
result = trueValue
else
result = falseValue
end
协同程序
| 创建方式 | 返回类型 | 执行方式 | 说明 |
|---|---|---|---|
coroutine.create(func) |
thread |
coroutine.resume(co) |
返回值里第一个是是否成功 |
coroutine.wrap(func) |
function |
co() |
调用更简单,但错误处理没 resume 直观 |
local co = coroutine.create(function()
print("协程执行")
end)
coroutine.resume(co)
local co = coroutine.create(function()
print("A")
coroutine.yield()
print("B")
end)
coroutine.resume(co) -- A
coroutine.resume(co) -- B
| 状态 | 含义 |
|---|---|
suspended |
挂起,可以继续执行 |
running |
正在执行 |
dead |
执行结束,或者已经不能继续执行 |
print(coroutine.status(co))
Lua 协程不是系统线程,是“函数执行到一半停住,下次接着跑”。
元表
setmetatable(t, mt)
getmetatable(t)
| 元方法 | 触发场景 |
|---|---|
__tostring |
表被转成字符串,比如 print(t) |
__call |
表被当成函数调用 |
__add / __sub / __mul / __div |
表参与运算 |
__concat |
表参与字符串拼接 |
__eq |
表参与相等比较 |
__index |
访问不存在字段 |
__newindex |
给不存在字段赋值 |
__index 可以是表或函数:
local mt = {
__index = {
name = "default"
}
}
local t = {}
setmetatable(t, mt)
print(t.name) -- default
rawget / rawset 绕过元方法:
rawget(t, "name")
rawset(t, "name", "Tom")
面向对象
table + function + metatable
Object = {}
function Object:new()
local obj = {}
setmetatable(obj, self)
self.__index = self
return obj
end
: 调用自动传 self:
obj:Move()
等价于 obj.Move(obj)。子类调父类方法常写 self.base.Move(self),别把类表当 self。
自带库
| 库 | 常用内容 | 说明 |
|---|---|---|
os |
os.time、os.date |
时间戳、日期表 |
math |
abs、floor、ceil、random、sqrt |
数学计算 |
string |
find、sub、gsub、format |
字符串处理 |
table |
insert、remove、sort、concat |
表处理 |
package |
package.path、package.loaded |
模块搜索路径和加载缓存 |
debug |
debug.traceback |
调试和错误堆栈 |
print(os.time())
local now = os.date("*t")
print(now.year, now.month, now.day)
math.randomseed(os.time())
print(math.random(100))
print(package.path)
debug.traceback 看错误堆栈。
垃圾回收
collectgarbage("count")
collectgarbage("collect")
| 方法 | 作用 |
|---|---|
collectgarbage("count") |
获取当前 Lua 内存占用,单位 KB |
collectgarbage("collect") |
手动触发一次完整 GC |
GC 看可达性,不是引用计数:
local a = {}
local b = {}
a.other = b
b.other = a
a = nil
b = nil
外部访问不到后仍可回收。弱表、__gc、增量/分代 GC 可以后面再仔细学习。热路径别频繁手动 collect。
错误处理
| 方法 | 作用 |
|---|---|
error |
主动抛出错误 |
assert |
条件不成立时抛错 |
pcall |
保护调用,接住错误 |
xpcall |
带错误处理函数的保护调用 |
debug.traceback |
生成错误堆栈 |
error("这里主动抛出一个错误")
assert(playerId ~= nil, "playerId不能为空")
local ok, result = pcall(function()
error("出错了")
end)
print(ok)
print(result)
local ok, err = xpcall(function()
error("出错了")
end, debug.traceback)
print(ok)
print(err)
事件分发、热更入口、UI/网络回调等位置适合统一错误保护。
Lua版本差异
print(_VERSION)
if jit then
print(jit.version)
end
_VERSION 在 LuaJIT 下也可能显示 Lua 5.1,判断 LuaJIT 要看 jit.version。
| 版本 | 常见差异点 | 项目里怎么记 |
|---|---|---|
| LuaJIT | Lua 5.1 语义为主,内置 bit,带部分 Lua 5.2 / 5.3 扩展 |
不要当成完整 Lua 5.3 / Lua 5.4 |
| Lua 5.1 | unpack、setfenv/getfenv、module |
老项目、Unity Lua 热更、LuaJIT 体系常见 |
| Lua 5.2 | _ENV、goto、__pairs / __ipairs |
环境机制从 setfenv 转向 _ENV |
| Lua 5.3 | 整数 / 浮点、原生位运算、table.move、utf8 |
新语法不要直接搬进 Lua 5.1 项目 |
| Lua 5.4 | incremental / generational GC、<close>、__close、coroutine.close |
资源关闭和协程关闭语义要注意 |
跨版本通用坑:
#对有洞 table 的结果不要依赖。pairs遍历顺序不要依赖。ipairs遇到中间nil通常会停。table.insert(t, pos, value)中间插入会移动后面的元素。
最终以项目嵌入的运行时为准。
20.2 面试题精选
基础题
1. Lua 里哪些值会被当成 false?
题目
Lua 的真值规则是什么?0、空字符串 ""、空表 {} 分别算真还是假?
深入解析
- Lua 只有
nil和false会被当成“假”,其余一律为“真”。 0、空字符串、空表在 Lua 里都是真。- 会影响
if判断、and/or的返回值逻辑,以及模拟三目写法。
答题示例
Lua 里只有
nil和false是假,其它都是真。0、空字符串、空表都是真。实际写条件判断时,不能用if x then来判断“是否为 0 / 空字符串 / 空表”,要写成更明确的比较条件。
参考文章
- 4.简单变量类型
- 7.条件分支语句
- 12.特殊语法
2. and / or 的短路和返回值规则是什么?
题目
Lua 的 and / or 除了短路,还有什么“非布尔返回值”特性?a and b / a or b 分别返回什么?
深入解析
and:左操作数为假,返回左值;左操作数为真,返回右值。or:左操作数为真,返回左值;左操作数为假,返回右值。- 它们返回的是参与运算的原始值,不一定是
true / false。 x = x or default很常见,但如果false是合法值,就不能这么写。
答题示例
and是“左假返回左,左真返回右”;or是“左真返回左,左假返回右”,并且都会短路。
所以 Lua 里经常用x = x or default做默认值,但如果false是合法值,就要显式判断nil,不能直接用or兜底。
参考文章
- 6.运算符
- 12.特殊语法
3. .. 拼接字符串有哪些坑?怎么写更好?
题目
.. 拼接非字符串时会怎样?遇到 nil 会怎样?如何避免拼接时报错?
深入解析
..用来做字符串拼接。number通常可以转成字符串参与拼接。nil不能直接拼接,会报错。- 日志拼接里,变量可能为空时最好先
tostring。
答题示例
..是字符串拼接。number 通常可以参与拼接,但nil会报错。
项目里写日志时,我一般会用tostring包一下,比如"playerId = " .. tostring(playerId),避免某个值为空时日志代码自己先炸掉。
参考文章
- 5.字符串操作
- 9.函数
4. Lua 函数参数个数不匹配会怎样?
题目
Lua 调函数时,多传参数、少传参数会不会报错?
深入解析
- Lua 调函数时参数个数比较宽松。
- 少传的参数会补
nil。 - 多传的参数会被丢弃。
- 关键函数的参数是否合法,需要自己写检查。
答题示例
Lua 调函数时,少传参数会补
nil,多传参数会丢弃,不会像 C# 那样直接编译期报错。
所以项目里的关键函数一般要自己做参数检查,比如用assert或普通if判断。
参考文章
- 9.函数
- 18.错误处理
进阶题
1. #t 为什么“不可靠”?什么时候能用?
题目
Lua 里 #table 的长度规则是什么?为什么带 nil 的数组 # 结果可能不稳定?项目里该怎么处理?
深入解析
#t适合连续数组,也就是从 1 开始,中间没有nil。- 一旦中间出现
nil,就变成有洞 table,#t的结果不要依赖。 - 字典、稀疏数组、混合表,不要用
#当数量。 - 真要统计数量,可以自己维护
count,或者按业务规则遍历统计。
答题示例
#t只适合连续数组。只要中间有 nil,结果就不要依赖,不同版本、不同构造方式下都可能和直觉不一致。
连续数组可以用#,字典和稀疏表不要用#当数量,项目里好的方式是自己维护 count 或按规则遍历统计。
参考文章
- 10.表
- 19.Lua版本差异
2. pairs 和 ipairs 的区别是什么?
题目
pairs 和 ipairs 分别适合遍历什么表?各自有什么坑?
深入解析
ipairs一般用于连续数组,从 1 开始往后遍历,遇到第一个nil通常会停。pairs可以遍历表里的所有键值对,但遍历顺序不要依赖。- 需要固定顺序时,不要依赖
pairs,自己维护 key 列表。
答题示例
ipairs适合连续数组,从 1 开始遍历,遇到 nil 通常会停。pairs适合遍历所有键值对,包括字符串 key、数字 key,但顺序不保证。
如果业务需要固定顺序,就自己维护 key 列表。
参考文章
- 10.表
- 19.Lua版本差异
3. require 的缓存机制是什么?怎么强制重新加载?
题目
require 会不会重复执行脚本?它的缓存在哪里?怎么卸载并重新加载一个模块?
深入解析
require首次加载会执行目标脚本。- 脚本返回值会缓存到
package.loaded[name]。 - 后续
require(name)直接返回缓存,不会重复执行。 - 强制重载可以先把
package.loaded[name]置空,再重新require。
答题示例
require默认只执行一次,结果会缓存在package.loaded。
要重载就把package.loaded["xxx"] = nil,然后再require("xxx")触发重新执行。这个适合测试或工具场景,正式业务里不要随便清模块缓存。
参考文章
- 11.模块与多脚本
4. setmetatable / __index / rawget 分别解决什么问题?
题目
Lua 元表的 __index 查找链是什么?rawget 为什么能绕过元表?常见用法有哪些?
深入解析
setmetatable(t, mt)给表设置元表。- 当访问
t[k]找不到键时,会看元表的__index。 __index可以是一张表,也可以是一个函数。rawget(t, k)只查t自己,不触发__index。
答题示例
__index是“缺键时的兜底查找”:可以是表,也可以是函数。rawget只查原表本身,不走元表链,常用于绕过代理表、调试元表行为,或者判断字段到底是不是表自己持有的。
参考文章
- 14.元表
5. pcall 和 xpcall 怎么选?
题目
Lua 里 pcall 和 xpcall 都能保护调用,它们有什么区别?项目里什么时候用 xpcall?
深入解析
pcall可以保护一次函数调用,报错时不会直接中断外层流程。pcall返回是否成功,以及结果或错误信息。xpcall可以指定错误处理函数,常见搭配是debug.traceback。- 框架入口、事件派发、脚本回调这些位置更适合用
xpcall打完整堆栈。
答题示例
pcall适合简单保护调用,能拿到是否成功和错误信息。xpcall可以指定错误处理函数,所以项目里经常配合debug.traceback打完整堆栈。
普通局部保护用pcall,框架入口和回调分发更适合xpcall。
参考文章
- 18.错误处理
深度题
1. 用 Lua 实现“类 / 继承 / 多态”时,: 和 . 的差异会引发什么 bug?
题目
Lua 里 obj:func() 和 obj.func(obj) 等价吗?在调用父类方法时为什么有时必须用 . 并显式传 self?
深入解析
:调用会把调用者作为第一个参数,也就是self,隐式传入。obj:func(x)等价于obj.func(obj, x)。- 子类调用父类方法时,如果传错
self,可能把类表当成实例,导致字段写到类表上。 - 常见写法是
self.base.Move(self)。
答题示例
:本质是帮忙隐式传self。obj:func(x)等价于obj.func(obj, x)。
子类调用父类方法时,要保证传进去的是实例,不然容易把类表当 self,改到共享字段上。常见写法是self.base.Move(self)。
参考文章
- 15.面向对象
2. Lua GC 回收的是什么?是不是引用计数?
题目
Lua GC 是不是简单的引用计数?两个表互相引用会不会永远回收不了?
深入解析
- Lua GC 主要看对象是否可达,不是简单引用计数。
- 对象如果还能从全局变量、局部变量、调用栈、表字段等地方访问到,就可以先理解为可达。
- 对象如果已经没有地方能访问到,就可以先理解为不可达,后续可能被 GC 回收。
- 两个表互相引用,不代表一定泄漏。只要整个引用环从外部不可达,GC 仍然可以回收。
答题示例
Lua GC 不是简单引用计数。基础理解是:对象如果从外部已经访问不到了,就可以认为不可达,后续可能被 GC 回收。
两个表互相引用本身不代表一定泄漏,只要整个引用环从外部不可达,GC 仍然能回收。
参考文章
- 17.垃圾回收
3. LuaJIT 和 Lua 5.3 / 5.4 是什么关系?
题目
LuaJIT 是不是 Lua 的最新版?为什么 Unity Lua 热更项目里不能直接写 Lua 5.3 / Lua 5.4 的新语法?
深入解析
- LuaJIT 不是 Lua 官方主线的 5.3 / 5.4。
- LuaJIT 整体更接近 Lua 5.1。
- LuaJIT 内置
bit,也带部分 Lua 5.2 / 5.3 扩展。 - 但 LuaJIT 不是完整 Lua 5.3 / Lua 5.4。
- Unity Lua 热更项目里,最终以项目实际嵌入的 Lua 运行时为准。
答题示例
LuaJIT 不是 Lua 最新版,它主要按 Lua 5.1 语义来跑,同时做了 JIT 优化,内置 bit,也带部分扩展。
但不能把它当完整 Lua 5.3 / 5.4 用。比如 Lua 5.3 的原生位运算符、Lua 5.4 的<close>,在 LuaJIT 项目里都不要默认可用。实际写热更代码时要先确认项目运行时。
参考文章
- 2.开发环境搭建
- 19.Lua版本差异
4. Lua 5.1 的 setfenv 和 Lua 5.2 的 _ENV 有什么关系?
题目
Lua 5.1 里常见的 setfenv / getfenv 和 Lua 5.2 之后的 _ENV 分别解决什么问题?
深入解析
- Lua 5.1 常见
setfenv / getfenv,用来处理函数环境。 - 可以简单理解成:函数查全局变量时,用哪张表去查。
- Lua 5.2 之后,环境机制更多基于
_ENV理解。 - 非
local、非 upvalue 的名字,会通过当前可见的_ENV去查。 - 这类机制常用于沙盒、配置脚本、限制全局变量访问。
答题示例
Lua 5.1 常用
setfenv / getfenv控制函数环境,换掉函数查全局变量时用的那张表。
Lua 5.2 之后更多通过_ENV理解环境,非 local 的变量会走当前_ENV查找。
项目里这类机制多用于沙盒和配置加载,普通业务代码里不建议随便改环境。
参考文章
- 11.模块与多脚本
- 19.Lua版本差异
5. Lua 5.4 的 __close 和 __gc 有什么区别?
题目
Lua 5.4 的 to-be-closed 变量、__close 和传统 __gc 有什么区别?为什么还要有 coroutine.close?
深入解析
local x <close> = value表示变量离开作用域时需要关闭。__close配合<close>使用,偏及时释放资源。__gc偏 GC 阶段收尾,触发时机由 GC 决定。coroutine.close和挂起协程里的 to-be-closed 变量有关。- 协程
yield后如果不再恢复,可以用coroutine.close主动关闭协程,并处理里面还没关闭的 to-be-closed 变量。
答题示例
__close更像作用域结束时的资源关闭,适合文件句柄、连接、C 层资源这类需要及时释放的东西。__gc是 GC 阶段的收尾,触发时机不确定。coroutine.close主要是为协程场景补关闭语义:协程 yield 后如果不再恢复,可以主动 close,让里面挂着的 to-be-closed 变量走关闭逻辑。
参考文章
- 13.协同程序
- 14.元表
- 17.垃圾回收
- 19.Lua版本差异
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com