19.Lua版本差异
19.1 知识点
判断Lua运行时
Lua 不是只有一个版本。
不同项目里使用的 Lua 运行时可能不一样。学习基础语法时,用 Lua 5.1 跑通大部分内容没问题。但真正进项目后,不能只看自己本机装了什么版本,而是要看项目实际嵌进去的 Lua 运行时。
常见情况大概有这些:
- LuaJIT
- Lua 5.1
- Lua 5.2
- Lua 5.3
- Lua 5.4
- 项目魔改 Lua
- xLua / toLua / SLua 内部集成的 Lua 运行时
版本差异这类问题,第一步先确认:当前脚本到底跑在哪个 Lua 运行时上。
最直接的方式是打印 _VERSION:
print(_VERSION)
可能输出:
Lua 5.1
也可能输出:
Lua 5.3
如果是 LuaJIT 环境,通常还可以看 jit.version:
if jit then
print(jit.version)
end
这里有个容易误判的点:LuaJIT 环境下,_VERSION 也可能显示 Lua 5.1。所以只看 _VERSION,不一定能判断当前是不是 LuaJIT。
实际项目里还要看:
- 项目接入文档。
- Lua DLL / so / bundle 版本。
- 框架封装的运行时说明。
- 是否支持某些标准库函数。
- 是否支持某些新语法。
- xLua / toLua / SLua 内部到底集成的是哪套 Lua。
最稳的办法还是看项目真正跑起来的那个 Lua 运行时。
版本差异可以先按下面这张表记:
| 版本 | 常见差异点 | 项目里怎么记 |
|---|---|---|
| LuaJIT | Lua 5.1 语义为主,内置 bit,带部分 Lua 5.2 / 5.3 扩展 |
不要当成完整 Lua 5.3 / 5.4;部分扩展要看 LuaJIT 版本和编译开关 |
| 跨版本通用坑 | #、pairs、ipairs、table.insert |
不只和版本有关,也和 table 的使用方式有关 |
| 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 |
资源关闭和协程关闭语义要注意 |
最终某个语法、某个库函数能不能用,还是以项目运行时为准。
LuaJIT
LuaJIT 可以理解成:一个更偏性能的 Lua 运行时。
普通 Lua 大致是先把 Lua 源码编译成字节码,再由虚拟机解释执行。LuaJIT 里的 JIT 是 Just-In-Time,也就是运行时编译。它会在程序运行过程中,把一些热点 Lua 代码进一步编译成更接近机器能直接执行的代码,所以很多场景下会比标准 Lua 解释器更快。
先不研究 JIT 编译细节,先记几个项目里常用的判断:
- LuaJIT 也是用来跑 Lua 脚本的。
- LuaJIT 不是 Lua 官方主线的 5.3 / 5.4 版本。
- LuaJIT 整体更接近 Lua 5.1。
- LuaJIT 内置
bit库,位运算常用bit.band / bit.bor / bit.bxor这套写法。 - LuaJIT 2.1 带了一部分 Lua 5.2 / Lua 5.3 扩展,但不是完整 Lua 5.3 / 5.4。
- Unity Lua 热更项目里经常能看到 LuaJIT。
可以用这几行先区分清楚:
Lua 5.1 :一套 Lua 语言版本和标准解释器。
LuaJIT :主要按 Lua 5.1 语义来跑,但内部做了 JIT 优化,也带一些扩展。
Lua 5.3/5.4 :Lua 官方后续版本,有一些新语法和新标准库。
在项目里看到 LuaJIT,不要直接理解成“最新版的 Lua”。它更像是:性能更强、生态很常见、但语法基线偏 Lua 5.1 的 Lua 运行环境。
Unity 热更项目里选择 LuaJIT,一般不是因为它语法最新,而是因为它在这些方面比较合适:
- 执行速度通常比标准 Lua 解释器更好。
- Lua 5.1 生态老项目多,历史代码和资料也多。
- 很多 Lua 热更方案早期就是围绕 Lua 5.1 / LuaJIT 做的。
- 对客户端来说,脚本性能、启动速度、运行时开销都比较敏感。
但这也带来一个问题:不能把 LuaJIT 当成完整 Lua 5.3 或 Lua 5.4 来写。
比如下面这些 Lua 5.3 / Lua 5.4 写法,就不要默认在 LuaJIT 项目里能跑:
-- Lua 5.3 原生位运算
-- LuaJIT / Lua 5.1 项目里不要默认能这么写
local a = 1 & 2
-- Lua 5.4 to-be-closed 变量
-- LuaJIT / Lua 5.1 项目里也不要默认能这么写
local f <close> = xxx
在 LuaJIT 里如果要做位运算,常见的是用 bit 库,而不是直接写 Lua 5.3 的 &、| 这些原生位运算符:
local bit = require("bit")
-- bit.band:按位与
-- 这里可以理解成 LuaJIT 里的 1 & 3
local value = bit.band(1, 3)
print(value) -- 1
-- 因为:
-- 01
-- & 11
-- ----
-- 01
这里不展开 bit 库,先知道有这个差异即可。
还有一个细节要注意:LuaJIT 2.1 这类版本会带一些 Lua 5.2 / Lua 5.3 的扩展,比如 table.move。有些 Lua 5.2 兼容特性,比如 pairs / ipairs 检查 __pairs / __ipairs,还要看 LuaJIT 编译时是否打开了 LUAJIT_ENABLE_LUA52COMPAT。所以不能只听到“LuaJIT 支持部分新特性”,就把它当成完整 Lua 5.2 / 5.3 来写。
写 LuaJIT 项目代码时,一般按这个习惯来:
优先按 Lua 5.1 的习惯写。
看到 Lua 5.3 / Lua 5.4 的新语法,不要直接搬进 LuaJIT 项目。
LuaJIT 有些扩展能不能用,要看项目接入的具体版本和编译开关。
实际项目里还要看接入的是哪个 LuaJIT 版本,以及项目有没有做兼容扩展。比如有些项目可能自己补了某些库函数,有些项目可能封装了自己的位运算工具,有些项目可能根本不允许直接访问部分标准库。
所以不要因为本机某个 Lua 版本能跑,就直接把语法写进项目热更层。热更代码最终能不能跑,要看项目真正嵌进去的那个 Lua 运行时。
小结:
- LuaJIT 是一个用来运行 Lua 的高性能运行时,不是 Lua 官方 5.3 / 5.4 主线版本。
JIT可以先理解成“运行时把热点代码编译得更快”。- LuaJIT 的语法基线更接近 Lua 5.1。
- LuaJIT 内置
bit库,也带部分 Lua 5.2 / 5.3 扩展。 - 部分 Lua 5.2 兼容行为要看 LuaJIT 版本和编译开关,不能默认所有项目都有。
_VERSION可能只显示Lua 5.1,判断 LuaJIT 时可以再看jit.version。- 写热更代码时,最终以项目实际接入的 Lua 运行时为准。
跨版本通用坑
有些坑不是某一个版本才有,而是写 Lua 表时一直容易遇到。
不展开 table 底层结构,先记几个结论:
#只适合连续数组。pairs不保证遍历顺序。ipairs遇到中间的nil通常会停。table.insert中间插入会移动后面的元素。
这些点看起来都很基础,但实际项目里很多奇怪 bug 都和它们有关。
# 对有洞table的结果不要依赖
如果数组中间有 nil,就叫有洞 table,也可以理解成“不连续数组”。
local t = { 1, nil, 3, 4 }
print(#t)
这段代码的结果不要拿来写关键逻辑。
原因很简单:# 对连续数组很好理解,但对中间有洞的 table,Lua 找到的“边界”不一定符合肉眼直觉。
比如:
local t1 = { 1, 2, 3, 4 }
print(#t1) -- 4,连续数组,这种情况比较稳定
但下面这种:
local t2 = { 1, nil, 3, 4 }
print(#t2) -- 结果不要依赖
t2[2] 是 nil,这张表已经不是严格连续数组了。不同 Lua 版本、不同构造方式、不同运行时实现下,#t2 的结果都不要当成业务规则。
比较稳的习惯是:
- 连续数组可以用
#。 - 字典不要用
#当数量。 - 有空洞的数组不要依赖
#。 - 真要统计数量,自己维护
count,或者按项目规则遍历统计。
local count = 0
for k, v in pairs(t2) do
count = count + 1
end
print(count)
这里也要注意,pairs 统计的是表里实际存在的键值对,不等于“连续数组长度”。所以到底用 #、count,还是自己维护数量,要看这张表在项目里到底被当成数组、字典,还是集合来用。
pairs遍历顺序不要依赖
pairs 可以遍历表里的键值对,但遍历顺序不要依赖。
local t = {
id = 1,
name = "Tom",
age = 18
}
for k, v in pairs(t) do
print(k, v)
end
这段代码会把 id / name / age 都遍历出来,但顺序不一定是:
id
name
age
也可能是别的顺序。
所以不要写这种逻辑:
-- 错误习惯:假设 pairs 第一个遍历到的一定是某个字段
for k, v in pairs(t) do
print(k, v)
break
end
这种代码表面上能跑,但逻辑是不稳的。如果业务需要固定顺序,就自己维护一个 key 列表。
local keys = { "id", "name", "age" }
for i = 1, #keys do
local key = keys[i]
print(key, t[key])
end
这段输出顺序就很清楚:
id 1
name Tom
age 18
所以这里可以简单记:
pairs 负责遍历所有键值对。
固定顺序要自己控制。
ipairs遇到nil会停,元方法和魔改库另看项目版本
ipairs 一般用于连续数组,从索引 1 开始往后遍历,遇到第一个 nil 通常就停。
local t = { 1, 2, nil, 4 }
for i, v in ipairs(t) do
print(i, v)
end
通常只会输出:
1 1
2 2
因为第三个位置是 nil,ipairs 走到这里就停了,后面的 4 通常遍历不到。
所以 ipairs 适合这种表:
local arr = { "A", "B", "C" }
for i, v in ipairs(arr) do
print(i, v)
end
不适合这种中间可能缺元素的表:
local arr = {}
arr[1] = "A"
arr[3] = "C"
for i, v in ipairs(arr) do
print(i, v)
end
这里第二个位置是 nil,遍历很可能到 1 就停。如果项目里的表可能是稀疏数组,就不要用 ipairs 假装它是连续数组。
如果项目用了 Lua 5.2 的 __ipairs、LuaJIT 的兼容开关,或者项目魔改过基础库,那就按项目运行时来确认。基础语法里不要依赖太复杂的行为。
table.insert中间插入会移动元素
table.insert(t, value) 尾插比较直观:
local t = { 1, 2, 3 }
table.insert(t, 4)
print(t[1]) -- 1
print(t[2]) -- 2
print(t[3]) -- 3
print(t[4]) -- 4
这相当于在数组最后追加一个元素。
但是中间插入就不一样了:
local t = { 1, 2, 3 }
table.insert(t, 2, 99)
print(t[1]) -- 1
print(t[2]) -- 99
print(t[3]) -- 2
print(t[4]) -- 3
这段代码的意思是:
把 99 插入到第 2 个位置。
原来的 t[2] 和 t[3] 会整体往后挪。
也就是:
插入前:
1, 2, 3
插入后:
1, 99, 2, 3
所以 table.insert(t, value) 和 table.insert(t, pos, value) 要分清楚:
table.insert(t, value) -- 尾插
table.insert(t, pos, value) -- 插到指定位置,后面的元素会移动
这篇先知道这个现象。后面写表结构和性能优化时,要再强调一次:不要在热路径里频繁做大量中间插入和删除。尤其是战斗列表、UI 列表、排行榜这类频繁更新的数据结构,中间插入和删除要多留个心眼。
小结:
#适合连续数组,不适合有洞 table。- 字典、稀疏表、有空洞数组,不要盲信
#。 pairs能遍历键值对,但顺序不要依赖。- 需要固定顺序时,自己维护 key 列表。
ipairs一般从 1 开始,遇到中间nil通常会停。table.insert尾插很直观,中间插入会移动后面的元素。- 表到底当数组、字典、集合还是队列用,要在写代码前先想清楚。
Lua 5.1
Lua 5.1 是很多老项目、Unity Lua 热更项目、LuaJIT 体系里常见的基础版本。
之前安装的 Lua for Windows 默认就是:
Lua 5.1.5
所以前面这些基础语法文章,大部分都是按 Lua 5.1 能跑通的思路来写的。
Lua 5.1 里旧项目常见写法主要有:
unpacksetfenvgetfenvmodule
这里不用把它们都背下来,只要知道:这些写法在 Lua 5.1 里比较常见,但到了后续版本,很多写法已经不推荐继续当成新项目标准写法了。
unpack
Lua 5.1 里常见写法:
local t = { 1, 2, 3 }
print(unpack(t)) -- 1 2 3
后续版本里更常见的是:
table.unpack(t)
所以看老代码时,如果看到 unpack(t),不要觉得奇怪。它就是把表里的连续数组元素拆出来,作为多个返回值返回。
setfenv和getfenv
Lua 5.1 里可以用 setfenv / getfenv 处理函数环境。
这里的“环境”可以理解成:函数查全局变量时用的那张表。
正常情况下,函数里如果访问一个没有 local 的变量,会去默认全局表里找。比如:
value = 100
function TestEnv()
print(value)
end
TestEnv() -- 100
上面这个 value 没有写 local,所以它是全局变量。TestEnv 执行时,自己内部找不到 value,就去默认全局环境里找,于是能打印出 100。
setfenv 做的事情,就是把这个函数的“查找环境”换成另一张表。
local env = {
print = print,
value = 123
}
function TestEnv()
print(value)
end
setfenv(TestEnv, env)
TestEnv() -- 123
这段代码可以分开看:
env是一张普通表。env.print = print,表示这张表里也提供print函数。env.value = 123,表示这张表里有一个value。setfenv(TestEnv, env),表示以后TestEnv找全局变量时,不去默认全局表里找,而是去env这张表里找。
所以 TestEnv() 执行时:
print(value)
实际查找过程可以理解成:
1. 函数内部没有 local value
2. 去 TestEnv 的环境表 env 里找 value
3. 找到 env.value = 123
4. 去 env 里找 print
5. 找到 env.print
6. 调用 print(123)
这里有个小细节:print 本身也要放进 env 里。因为换了环境以后,TestEnv 里面访问的 print 也会去 env 里找。如果 env 里没有 print,这段代码就会报错。
local env = {
value = 123
}
function TestEnv()
print(value)
end
setfenv(TestEnv, env)
-- TestEnv() -- 报错,因为 env 里没有 print
getfenv 则是反过来,把函数当前使用的环境表拿出来。
local env = {
print = print,
value = 123
}
function TestEnv()
print(value)
end
setfenv(TestEnv, env)
local currentEnv = getfenv(TestEnv)
print(currentEnv == env) -- true
print(currentEnv.value) -- 123
所以基础阶段先记住:
setfenv(函数, 表):把函数查找全局变量的环境换成指定表。getfenv(函数):拿到函数当前使用的环境表。- 老项目、老框架、沙盒脚本里可能会看到它们。
Lua 5.2 之后,不再按 Lua 5.1 那套 setfenv / getfenv 作为常规环境方案来写。理解新版本环境机制时,重点转到 _ENV:非 local、非 upvalue 的名字,本质上会通过当前可见的 _ENV 去查。
这个机制常见用途是做脚本沙盒、限制全局变量访问、给某段脚本提供一套独立的运行环境。比如只给脚本暴露 print 和配置数据,不让它随便访问外部全局变量。
这里先理解现象即可。真正的环境隔离、全局变量拦截、沙盒加载,后面放到 Lua 进阶知识里再讲。
module
老 Lua 代码里还可能看到:
module("TestModule", package.seeall)
这个属于 Lua 5.1 时代比较常见的旧模块写法。
这里最好不要只把它理解成“调用一个函数”。它真正做的事情更像是:把当前这个 Lua 文件切换成一个模块文件。
可以拆成三件事:
- 创建或找到一张叫
TestModule的模块表。 - 把当前文件后面写的非
local变量和函数,放到TestModule这张表里。 - 因为用了
package.seeall,所以模块表里找不到的变量,还可以去_G全局表里找。
也就是说,package.seeall 主要解决的是“读不到全局函数”的问题,比如 print。它不是说“后面所有变量都继续写到全局表”。
这一点很容易混。
新建模块文件 TestModule.lua
这个例子建议单独新建一个文件:
TestModule.lua
内容如下:
module("TestModule", package.seeall)
name = "测试模块"
function PrintName()
print(name)
end
表面上看,name 和 PrintName 像是普通全局变量。但因为文件开头写了:
module("TestModule", package.seeall)
所以后面的:
name = "测试模块"
function PrintName()
print(name)
end
实际可以理解成写到了 TestModule 这张表里:
TestModule.name = "测试模块"
function TestModule.PrintName()
print(TestModule.name)
end
所以这段模块文件加载后,大概等价于准备出了一张表:
TestModule = {
name = "测试模块",
PrintName = function()
print(name)
end
}
当然真实执行细节不是简单文本替换,这里只是为了理解现象。
新建使用模块的文件 Main.lua
再新建一个文件:
Main.lua
内容如下:
require("TestModule")
TestModule.PrintName() -- 测试模块
print(TestModule.name) -- 测试模块
这里要拆成两个文件看:
TestModule.lua:负责定义模块。Main.lua:负责require这个模块,并使用模块里的内容。
如果把这两段都塞在同一个 Lua 文件里,反而不容易理解 require 和模块的关系。模块本来就是为“多脚本拆分”准备的。
package.seeall到底做了什么
package.seeall 最容易误解。
它不是让变量继续写到全局表 _G。它主要是让模块表“看得见”全局表里的东西。
比如在模块里写:
function PrintName()
print(name)
end
这里面有两个名字:
print
name
name 是模块自己定义的,所以会在 TestModule 表里找到。
但 print 是 Lua 自带的全局函数,不是 TestModule 里定义的。如果模块表完全看不到全局表,那么这里调用 print 就会出问题。
用了 package.seeall 后,可以理解成模块表多了一条兜底规则:
setmetatable(TestModule, { __index = _G })
也就是:
模块表自己找不到的变量,就去 _G 里找。
所以:
print(name)
查找过程大概是:
1. 找 print
2. TestModule 表里没有 print
3. 因为 package.seeall,去 _G 里找 print
4. 找到了 Lua 全局 print 函数
5. 找 name
6. TestModule 表里有 name
7. 直接拿 TestModule.name
所以 package.seeall 影响的是“找变量”的兜底规则,不是“写变量”的位置。
module后普通变量会写到哪里
在 module("TestModule", package.seeall) 后面写:
a = 1
它默认不是写到:
_G.a = 1
而是更接近写到:
TestModule.a = 1
所以在外部文件里可以这样访问:
require("TestModule")
print(TestModule.a)
如果在外部直接写:
print(a)
通常是拿不到的,因为 a 不在全局表里,而是在 TestModule 模块表里。
如果想创建真正的全局变量
如果在这种旧模块写法里,确实想写到全局表,需要显式写 _G:
module("TestModule", package.seeall)
name = "测试模块" -- 写到 TestModule.name
_G.globalName = "全局变量" -- 写到真正的全局表
外部使用:
require("TestModule")
print(TestModule.name) -- 测试模块
print(globalName) -- 全局变量
print(_G.globalName) -- 全局变量
不过实际项目里,一般不建议在模块里乱写全局变量。如果一个模块偷偷往 _G 里塞东西,后面脚本多了,很容易出现命名冲突,也不好排查是谁写进去的。
如果想只在模块内部使用
如果某个变量只想在当前模块内部用,不想暴露给外部,就写 local:
module("TestModule", package.seeall)
local localName = "模块内部变量"
name = "对外暴露的变量"
function PrintName()
print(localName)
print(name)
end
外部:
require("TestModule")
print(TestModule.name) -- 对外暴露的变量
print(TestModule.localName) -- nil
localName 只是模块文件内部的局部变量,不会挂到 TestModule 表上。
所以这三种写法要分清楚:
local a = 1 -- 当前文件局部变量,外部访问不到
a = 1 -- module 后,写到模块表里,大概是 TestModule.a
_G.a = 1 -- 明确写到全局表里,外部可以直接 a 访问
新代码更推荐返回表
老代码里看到 module 要认识,但新代码一般不建议继续这么写。
现在更常见、更清楚的模块写法是返回一张表。
新建文件:
TestModule.lua
内容:
local M = {}
local localName = "模块内部变量"
M.name = "测试模块"
function M.PrintName()
print(localName)
print(M.name)
end
return M
再新建使用文件:
Main.lua
内容:
local TestModule = require("TestModule")
TestModule.PrintName()
print(TestModule.name)
这种写法更直观:
local的就是模块内部用。M.xxx的就是模块对外暴露。return M表示这个模块最终返回的就是这张表。- 外部
local TestModule = require("TestModule"),拿到的就是这张模块表。
小结
module("TestModule", package.seeall)是 Lua 5.1 时代的旧模块写法。- 这段代码一般写在模块文件
TestModule.lua的开头。 - 示例最好拆成两个文件:
TestModule.lua定义模块,Main.lua使用模块。 module后面普通的非local变量和函数,默认会写到模块表里,比如TestModule.name。package.seeall让模块找不到变量时可以去_G里找,主要解决读全局函数、全局库的问题。package.seeall不代表写变量时写回_G。- 如果想明确写全局变量,用
_G.xxx = xxx。 - 如果只想模块内部使用,用
local xxx。 - 新代码更推荐
local M = {}; return M这种模块写法,边界更清楚。
Lua 5.2
Lua 5.2 开始,很多版本差异会集中在环境、模块、控制流这些地方。
先记几个点:
_ENVgoto__pairs / __ipairssetfenv / getfenv不再像 Lua 5.1 那样作为常规写法使用。
_ENV
Lua 5.2 开始,环境相关的写法更多会围绕 _ENV 来理解。
这里不要把 _ENV 想复杂。它可以理解成:当前代码查全局变量时用的那张表。
正常情况下,代码里访问一个没有 local 的变量,会去默认全局环境里找。
value = 100
function Test()
print(value)
end
Test() -- 100
这里的 value 没有写 local,所以它是全局变量。Test 里找不到局部变量 value,就会去默认全局环境里找,于是打印出 100。
Lua 5.2 之后,可以通过 _ENV 改变这段代码“查全局变量”的位置。
local env = {
print = print,
value = 123
}
local function Test()
local _ENV = env
print(value)
end
Test() -- 123
这段可以分开看:
env是一张普通表。env.print = print,表示这张表里也放了print函数。env.value = 123,表示这张表里有一个value。local _ENV = env,表示当前作用域里,查全局变量时先去env这张表里找。
所以 Test() 执行时:
print(value)
可以理解成:
1. Test 函数内部定义了 local _ENV = env
2. print 不是 local 变量,于是去 _ENV 里找
3. 找到 env.print
4. value 也不是 local 变量,于是去 _ENV 里找
5. 找到 env.value = 123
6. 最后执行 print(123)
所以它最终打印:
123
这里有个容易踩的点:既然 _ENV 被换成了 env,那 print 也会去 env 里找。所以 env 里必须放 print = print,否则下面这段会报错:
local env = {
value = 123
}
local function Test()
local _ENV = env
print(value)
end
-- Test() -- 报错,因为 env 里没有 print
可以把 _ENV 和 Lua 5.1 的 setfenv 放在一起理解:
Lua 5.1:常用 setfenv 改函数环境。
Lua 5.2:更多通过 _ENV 控制变量查找环境。
这里不要展开太深,记住一句话就够了:
_ENV 决定了当前代码访问“非 local 变量”时,去哪里找。
实际项目里,不建议在普通业务代码里随便改 _ENV。因为一旦改了以后,变量到底来自局部、模块表、全局表,排查起来会很绕。环境隔离、全局变量拦截、沙盒加载这些内容,后面放到 Lua 进阶知识里再整理。
goto
Lua 5.2 有 goto 语法。
goto 可以理解成:让代码跳到某个提前标记好的位置继续执行。
这个“标记好的位置”叫标签,写法是:
::begin::
跳过去的写法是:
goto begin
完整示例:
local i = 1
::begin::
print(i)
i = i + 1
if i <= 3 then
goto begin
end
执行过程大概是:
1. i = 1
2. 运行到 ::begin:: 标签
3. 打印 i,输出 1
4. i 变成 2
5. i <= 3,goto begin,跳回 ::begin::
6. 打印 i,输出 2
7. i 变成 3
8. i <= 3,goto begin,继续跳回 ::begin::
9. 打印 i,输出 3
10. i 变成 4
11. i <= 3 不成立,不再跳转
12. 代码继续往后执行
输出:
1
2
3
这段代码其实和下面的 while 差不多:
local i = 1
while i <= 3 do
print(i)
i = i + 1
end
所以 goto 本身不是不能用,但普通业务代码里一般不鼓励滥用。如果只是正常循环、条件分支,用 if / while / for 通常更清楚。
goto 更适合少数特殊场景,比如快速跳出多层逻辑、做一些底层流程控制。但写业务逻辑时如果到处都是 goto,代码会变成一堆跳转线,后面排查问题会很难受。
这里记住:
::name::是标签。goto name会跳到这个标签位置继续执行。- Lua 5.1 不支持
goto,Lua 5.2+ 才有。 - 普通业务逻辑优先用
if / while / for,不要为了炫技写goto。
__pairs和__ipairs
Lua 5.2 里要注意 __pairs / __ipairs。
这两个东西和表遍历有关。简单说,就是允许表通过元方法影响 pairs / ipairs 的遍历行为。这里不用展开元表细节,先知道有这个版本差异即可。
真正容易踩坑的是 __ipairs 的版本变化:__ipairs 是 Lua 5.2 引入的机制,但 Lua 5.3 里已经废弃。后续版本、LuaJIT 兼容开关、项目魔改基础库,行为都要按项目实际运行时确认。
所以实际项目里不要默认写这种强依赖:
-- 不建议在基础业务代码里默认依赖 __ipairs 行为
-- 具体能不能用,要看当前 Lua 运行时
这里先这样记:
__pairs / __ipairs和表遍历元方法有关。- Lua 5.2 要知道它们存在。
__ipairs到 Lua 5.3 已经废弃,后续版本不要默认依赖。- LuaJIT 是否支持,还要看版本和编译开关。
- 普通业务表遍历,优先写清楚数据结构,不要靠这种版本差异行为兜底。
Lua 5.3
Lua 5.3 开始,比较明显的变化主要集中在数值、位运算、数组搬移和 UTF-8 辅助库上。
这里先记这几个点:
- 整数 / 浮点开始有区分。
- 支持原生位运算。
table.move可以批量搬移数组区间。- 新增
utf8库,用来做一些 UTF-8 字符串辅助处理。
这里还是只做速查,不展开源码和底层实现。真正写项目时,尤其是 Unity Lua 热更项目,要先确认运行时到底是不是 Lua 5.3。很多 LuaJIT / Lua 5.1 项目不能直接用这些新语法。
整数和浮点
Lua 5.3 之前,学习时经常会简单说:
Lua 里所有数字都是 number。
这句话在基础阶段问题不大,但如果项目用的是 Lua 5.3+,就要知道:Lua 5.3 的 number 内部已经分成了整数和浮点两种数值子类型。
print(type(1)) -- number
print(type(1.0)) -- number
从 type 看,它们都还是 number。
但在 Lua 5.3 里,可以用 math.type 看得更细一点:
print(math.type(1)) -- integer
print(math.type(1.0)) -- float
可以这么记:
type 看大类:都是 number。
math.type 看 number 里面更细的类型:integer / float。
普通业务脚本里,大部分时候不用刻意纠结这个。但涉及网络协议、存档、二进制数据、跨端一致性、位运算时,就不能完全当成“所有数字都一样”来写了。
比如:
print(1 == 1.0) -- true
值比较是相等的。但底层表示和某些运算结果可能会有差异,所以真正进项目后还是要按项目运行时确认。
原生位运算
Lua 5.3 开始支持原生位运算符。
位运算可以理解成:直接操作一个整数的二进制位。项目里常见用途是状态标记、权限标记、协议字段、二进制数据处理等。
比如:
print(1 & 3) -- 按位与,输出 1
print(1 | 2) -- 按位或,输出 3
print(1 ~ 3) -- 按位异或,输出 2
print(1 << 2) -- 左移,输出 4
print(8 >> 1) -- 右移,输出 4
这里拿 1 & 3 举例:
1 的二进制可以简单看成:01
3 的二进制可以简单看成:11
01
& 11
----
01
所以结果是 1。
这些符号在 Lua 5.3 里可以直接用。但 Lua 5.1 / LuaJIT 项目里不能默认这么写。
老项目如果要做位运算,可能会用:
bitbit32- 项目自己封装的位运算工具
所以写热更代码时要先看运行时版本。不要直接把 Lua 5.3 的 &、|、~、<<、>> 写进 Lua 5.1 / LuaJIT 项目里。
table.move
Lua 5.3 有 table.move,可以批量搬移数组区间。
语法大概是:
table.move(源表, 起始索引, 结束索引, 目标起始索引, 目标表)
比如:
local source = { 1, 2, 3, 4, 5 }
local target = {}
table.move(source, 2, 4, 1, target)
print(target[1]) -- 2
print(target[2]) -- 3
print(target[3]) -- 4
这段代码的意思是:
把 source[2] 到 source[4] 这一段内容,
搬到 target[1] 开始的位置。
也就是:
source[2] = 2 -> target[1]
source[3] = 3 -> target[2]
source[4] = 4 -> target[3]
这里有个容易误解的点:table.move 这个名字叫 move,但不要理解成“剪切后删除原位置”。更准确地说,它是把一段连续数组元素搬到指定位置,原表里的值不会因为这个示例自动清空。
print(source[2]) -- 2,原来的 source 里还在
如果最后一个目标表参数不传,它会在同一张表里搬移:
local list = { 1, 2, 3, 4, 5 }
table.move(list, 2, 4, 3)
print(list[1]) -- 1
print(list[2]) -- 2
print(list[3]) -- 2
print(list[4]) -- 3
print(list[5]) -- 4
同一张表里搬移时,容易涉及覆盖和区间重叠。这篇先知道有这个函数即可,真正的使用场景和性能细节后面放到表结构、性能优化里再讲。
utf8库
Lua 5.3 里还新增了 utf8 库。
它可以用来做一些 UTF-8 字符串辅助处理,比如遍历 UTF-8 字符、计算 UTF-8 字符数量等。
这里先记住一个点:之前讲字符串时说过,Lua 字符串本质上更像字节序列,# 拿到的通常是字节长度,不是肉眼看到的中文字符数量。
Lua 5.3 的 utf8 库,就是为 UTF-8 字符串处理补了一些工具。
local str = "你好"
print(#str) -- 通常输出 6,因为 UTF-8 中文一般一个字 3 字节
print(utf8.len(str)) -- 输出 2,表示 2 个 UTF-8 字符
不过这也要看项目运行时。Lua 5.1 / LuaJIT 项目里不要默认有 utf8 标准库,很多项目会用自己封装的字符串工具来处理中文。
小结:
- Lua 5.3 里
number内部开始区分整数和浮点。 type(1)和type(1.0)都是number,但math.type可以看到integer / float。- Lua 5.3 支持原生位运算符,比如
&、|、~、<<、>>。 - Lua 5.1 / LuaJIT 项目不要默认能直接使用 Lua 5.3 位运算符。
table.move可以批量搬移一段连续数组区间。table.move不要简单理解成“剪切删除”,它更像是把一段值复制 / 搬到指定位置。- Lua 5.3 新增
utf8库,但 Unity LuaJIT 项目里不要默认可用。
Lua 5.4
Lua 5.4 的差异点里,比较容易在项目里被问到的是:
- 分代 GC。
- to-be-closed 变量。
__close元方法。- coroutine 关闭行为。
这些内容都偏进阶,这里只做速查。先知道它们大概是为了解决什么问题,不需要一上来钻实现细节。
分代GC
Lua 本身有自动垃圾回收。前面第 17 篇已经讲过,基础阶段会用:
collectgarbage("count")
collectgarbage("collect")
就够了。
Lua 5.4 不是只有分代 GC,而是 GC 模式上同时要知道 incremental 和 generational。这里是基础篇,所以先点到分代 GC。
分代 GC 可以简单理解成:
新创建的对象,通常更可能很快变成垃圾。
活得比较久的对象,通常不需要每次都重点扫描。
所以分代 GC 会把对象按“新对象 / 老对象”这类思路区分处理。这样做的目标是减少一些没必要的扫描,让垃圾回收在某些场景下更合适。
这里不要展开算法。真正涉及 GC 参数、增量式 GC、分代 GC、卡顿、内存峰值、泄漏排查时,放到 Lua 进阶知识里再讲。
这里记住:
- Lua 有自动垃圾回收。
- Lua 5.4 的 GC 可以工作在 incremental 和 generational 两种模式。
- 本篇只点到分代 GC,不展开 GC 参数。
- 不要把 Lua GC 简单说成引用计数。
- 不要在游戏热路径里频繁手动
collectgarbage("collect")。
to-be-closed变量和__close
Lua 5.4 里有一种资源释放语义,常见写法是:
local x <close> = xxx
to-be-closed 这个名字看着很绕,可以直接翻成:离开作用域时需要被关闭的变量。
它一般不是用来管理普通数字、字符串、表的。它更适合管理“用完必须关掉”的资源,比如文件句柄、网络连接、数据库结果、C 层资源句柄这类东西。
可以看一个伪代码:
local file <close> = io.open("test.txt", "r")
-- 中间读取文件
-- ...
-- 当前作用域结束时,Lua 会尝试调用 file 对应的 __close 逻辑
这里的重点不是 io.open,而是这个语义:
变量离开作用域时,Lua 会尝试帮它走关闭逻辑。
__close 就是和 <close> 配套用的元方法。如果一个值要支持 <close>,它对应的元表里需要有 __close。
可以这么理解:
local x <close> = xxx -- 标记 x 离开作用域时要关闭
__close -- 真正负责关闭时执行的函数
这里还有个细节:local x <close> = value 时,value 必须有 __close 元方法,或者是 nil / false。nil 和 false 会被忽略,不会真的执行关闭逻辑。如果是普通 table、number、string 这类没有 __close 的值,不能硬塞进去。
这里先记住:
local x <close>是 Lua 5.4 的写法。- 它不是 Lua 5.1 / LuaJIT 项目里的默认可用语法。
- 它偏资源释放,不是普通变量声明的常规写法。
- to-be-closed 变量本身类似常量局部变量,声明后不要再重新赋值。
__close是离开作用域时的关闭逻辑。__close和__gc不是一回事。
__close和__gc的区别
这两个名字很像,但不要混在一起。
可以这样记:
__close:变量离开作用域时触发,偏“及时关闭资源”。
__gc :对象被垃圾回收时触发,时机由 GC 决定。
比如一个文件句柄,用完后最好立刻关掉。这类场景更希望“作用域结束就关闭”,而不是等哪天 GC 扫到了再说。
所以:
__close 更偏确定性的资源释放。
__gc 更偏垃圾回收阶段的收尾处理。
基础篇只讲到这里,不展开元方法细节。真正要写可关闭对象、C# / C 资源绑定、宿主对象生命周期时,放到后面的进阶或绑定系列里再看。
coroutine关闭行为
Lua 5.4 里还要注意 coroutine 的关闭行为。
前面协程篇里讲过,协程可以 yield 挂起。如果协程里有 to-be-closed 变量,而这个协程挂起后再也不恢复,就会有一个问题:
这个变量什么时候关闭?
Lua 5.4 提供了 coroutine.close,可以关闭一个挂起或已结束的协程,并处理里面还没关闭的 to-be-closed 变量。
可以看成:
coroutine.close(co)
它的作用是:
关闭协程 co,并把协程里挂着的 to-be-closed 变量也处理掉。
比如一个协程里打开了资源,然后在中途 yield 了:
-- Lua 5.4 示例,Lua 5.1 / LuaJIT 里不要直接执行
local mt = {
__close = function(resource, err)
print("关闭资源:", resource.name)
print("关闭时的错误:", err)
end
}
local co = coroutine.create(function()
local resource <close> = setmetatable({ name = "协程里的资源" }, mt)
print("协程开始执行")
coroutine.yield()
print("协程恢复后继续执行")
end)
coroutine.resume(co) -- 输出:协程开始执行
local ok, err = coroutine.close(co)
print(ok, err)
-- 输出:true nil
-- 同时会触发 resource 的 __close
这段代码的关键点不在打印,而在生命周期:
1. 协程创建资源 resource。
2. resource 是 to-be-closed 变量。
3. 协程 yield 后停在半路。
4. 如果后面不再 resume,这个作用域不会自然结束。
5. 调用 coroutine.close(co),Lua 会关闭协程,并触发还没关闭的 __close。
如果协程里没有 <close> 变量,coroutine.close 也可以把挂起协程变成 dead,但最有价值的场景还是配合 to-be-closed 变量处理资源收尾。
另外,coroutine.close(co) 不是随便什么协程都能关。它要求协程处于挂起或者已经结束状态。正常关闭成功时返回 true;如果关闭过程中出现错误,会返回 false 和错误对象。这个行为和 pcall 有点像,项目里不要只写一行 coroutine.close(co) 就不看返回值。
这块不展开。只要知道:如果项目用的是 Lua 5.4,协程生命周期、<close>、__close、资源释放这些点是绑在一起看的,不能完全按 Lua 5.1 / LuaJIT 的习惯理解。
Unity Lua 热更项目里,不要默认能直接用:
local x <close> = xxx
coroutine.close(co)
真要用,必须先确认项目运行时版本支持。
小结:
- Lua 5.4 的 GC 可以工作在 incremental 和 generational 两种模式。
- 分代 GC可以理解成按“新对象 / 老对象”的思路优化回收过程。
local x <close>表示变量离开作用域时需要走关闭逻辑。local x <close> = value里的value必须有__close,或者是nil / false。__close是配合<close>使用的关闭元方法。__close和__gc不一样:前者偏及时关闭资源,后者偏 GC 阶段收尾。coroutine.close和挂起协程里的 to-be-closed 变量有关。- Lua 5.1 / LuaJIT 项目不要默认能用 Lua 5.4 的
<close>、__close、coroutine.close。
总结
- 写 Lua 版本差异,第一步先判断当前运行时。
_VERSION可以看基础 Lua 版本,但 LuaJIT 环境下也可能显示Lua 5.1。- 判断 LuaJIT 时,可以顺手看
jit.version。 - LuaJIT 更接近 Lua 5.1,但也带一些扩展,不要简单当成完整 Lua 5.3 / 5.4。
- LuaJIT 的部分 Lua 5.2 兼容行为还要看版本和编译开关。
#对有洞 table 的结果不要依赖。pairs遍历顺序不要依赖。ipairs遇到中间nil通常会停,元方法和魔改库另看项目版本。- Lua 5.1 是很多老项目和 Unity Lua 热更项目常见基础版本。
- Lua 5.2 开始要注意
_ENV、goto、__pairs / __ipairs等差异。 - Lua 5.3 开始要注意整数 / 浮点、原生位运算、
table.move、utf8。 - Lua 5.4 开始要注意 GC 模式、
local x <close>、__close、coroutine.close,to-be-closed 变量声明后也不要再重新赋值。 - 实际项目永远以当前运行时为准,不要把本机能跑当成项目一定能跑。
19.2 知识点代码
Lesson19_Lua版本差异.lua
print("**********Lua版本差异************")
print("**********知识点一 判断Lua运行时************")
-- _VERSION 只能看当前 Lua 的基础版本。
-- 普通 Lua 5.1 通常打印 Lua 5.1。
-- 普通 Lua 5.3 通常打印 Lua 5.3。
-- 普通 Lua 5.4 通常打印 Lua 5.4。
-- LuaJIT 环境下,_VERSION 也可能打印 Lua 5.1。
-- 所以项目里判断运行时,不能只看 _VERSION。
print("当前 _VERSION:", _VERSION)
-- LuaJIT 环境里通常会有 jit 这张表。
-- jit.version 可以看到 LuaJIT 的具体版本。
-- 如果当前不是 LuaJIT,jit 通常是 nil。
if jit then
print("当前运行环境:LuaJIT")
print("LuaJIT 版本:", jit.version)
else
print("当前运行环境:不是 LuaJIT,或者项目没有暴露 jit 表")
end
-- 实际项目里还要看:
-- 1. 项目接入文档。
-- 2. Lua DLL / so / bundle 版本。
-- 3. xLua / toLua / SLua 内部集成的运行时。
-- 4. 项目有没有自己魔改 Lua 或封装标准库。
print("**********知识点二 LuaJIT************")
-- LuaJIT 更接近 Lua 5.1,不是完整 Lua 5.3 / Lua 5.4。
-- LuaJIT 内置 bit 库,位运算一般用 bit.band / bit.bor / bit.bxor。
-- 不要默认可以直接写 Lua 5.3 的 &、|、~、<<、>> 运算符。
if jit then
local bit = require("bit")
-- bit.band:按位与。
-- 这里可以理解成 LuaJIT 里的 1 & 3。
-- 二进制角度看:
-- 01
-- & 11
-- ----
-- 01
local value = bit.band(1, 3)
print("bit.band(1, 3) 的结果:", value) -- 输出:1
else
print("当前不是 LuaJIT,跳过 LuaJIT bit 库示例")
end
-- 注意:
-- LuaJIT 2.1 可能带部分 Lua 5.2 / Lua 5.3 扩展。
-- 但有些兼容行为还要看编译开关,比如 LUAJIT_ENABLE_LUA52COMPAT。
-- 项目里最终还是以实际接入的 LuaJIT 版本为准。
print("**********知识点三 跨版本通用坑************")
-- 1. # 对有洞 table 的结果不要依赖。
local holeTable = { 1, nil, 3, 4 }
-- 这张表中间有 nil,不是严格连续数组。
-- 这里打印出来的长度,不要拿来写关键业务逻辑。
print("有洞 table 的 # 结果,不要依赖:", #holeTable)
-- 如果要统计实际存在的键值对,可以用 pairs 遍历。
-- 但这个 count 表示“实际存在的键值对数量”,不是连续数组长度。
local count = 0
for k, v in pairs(holeTable) do
count = count + 1
end
print("有洞 table 中实际存在的键值对数量:", count)
-- 2. pairs 遍历顺序不要依赖。
local player = {
id = 1,
name = "Tom",
age = 18
}
print("pairs 遍历 player,输出顺序不要依赖:")
for k, v in pairs(player) do
print(k, v)
end
-- 如果业务需要固定顺序,自己维护 key 列表。
local keys = { "id", "name", "age" }
print("按照指定 key 顺序访问 player:")
for i = 1, #keys do
local key = keys[i]
print(key, player[key])
end
-- 3. ipairs 遇到中间 nil 通常会停。
local arr = { 1, 2, nil, 4 }
print("ipairs 遍历 { 1, 2, nil, 4 }:")
for i, v in ipairs(arr) do
print(i, v)
end
-- 一般只会输出:
-- 1 1
-- 2 2
-- 因为第三个位置是 nil,后面的 4 通常遍历不到。
-- 4. table.insert 中间插入会移动元素。
local list = { 1, 2, 3 }
table.insert(list, 2, 99)
print("table.insert(list, 2, 99) 后的 list:")
print("list[1]:", list[1]) -- 输出:1
print("list[2]:", list[2]) -- 输出:99
print("list[3]:", list[3]) -- 输出:2
print("list[4]:", list[4]) -- 输出:3
-- table.insert(list, 2, 99) 的意思是:
-- 把 99 插入到第 2 个位置。
-- 原来的 list[2] 和 list[3] 会整体往后挪。
print("**********知识点四 Lua 5.1************")
-- 1. unpack 是 Lua 5.1 里常见写法。
-- Lua 5.2+ 更常见的是 table.unpack。
-- 这里做一个兼容写法,避免换版本后直接报错。
local unpackFunc = unpack or table.unpack
if unpackFunc then
local t = { 1, 2, 3 }
print("unpack / table.unpack 拆开数组后的输出:")
print(unpackFunc(t)) -- 输出:1 2 3
else
print("当前运行时没有 unpack / table.unpack")
end
-- 2. setfenv / getfenv 是 Lua 5.1 里常见的环境相关写法。
-- 可以理解成:
-- 让 TestEnv 查全局变量时,不去默认全局表找,而是去 env 这张表找。
-- Lua 5.2+ 默认不再按这套方式写,所以这里先判断函数是否存在。
if setfenv and getfenv then
local env = {
print = print,
value = 123
}
function TestEnv()
print("TestEnv 里访问 value,实际取到的是 env.value:", value)
end
setfenv(TestEnv, env)
TestEnv() -- 输出:TestEnv 里访问 value,实际取到的是 env.value: 123
local currentEnv = getfenv(TestEnv)
print("getfenv(TestEnv) == env:", currentEnv == env) -- 输出:true
print("TestEnv 当前环境表里的 value:", currentEnv.value) -- 输出:123
else
print("当前运行时没有 setfenv / getfenv,Lua 5.2+ 更常见的是 _ENV")
end
-- 3. module 是 Lua 5.1 时代的旧模块写法。
-- 完整示例放在 TestModule.lua / Main.lua 里。
-- 新代码更推荐 local M = {}; return M 这种返回表的写法。
print("**********知识点五 Lua 5.2************")
-- 1. _ENV 是 Lua 5.2+ 的环境相关写法。
-- 如果当前运行环境是 Lua 5.1,这段不能直接运行。
-- 所以这里只保留示例,不在 Lua 5.1 文件里直接打开执行。
-- 这段逻辑和 Lua 5.1 的 setfenv 有点像,都是在影响“变量去哪里找”。
--[[
local env = {
print = print,
value = 123
}
local function Test()
local _ENV = env
print("通过 _ENV 找到的 value:", value)
end
Test() -- 输出:通过 _ENV 找到的 value: 123
]]
-- 2. goto 是 Lua 5.2+ 的语法。
-- 如果当前运行环境是 Lua 5.1,这段不能直接运行。
-- 业务代码里一般不建议滥用 goto,普通循环用 for / while 更清楚。
--[[
local i = 1
::begin::
print("goto 示例,当前 i:", i)
i = i + 1
if i <= 3 then
goto begin
end
-- 输出:
-- goto 示例,当前 i:1
-- goto 示例,当前 i:2
-- goto 示例,当前 i:3
]]
-- 3. Lua 5.2 引入了 __pairs / __ipairs。
-- __ipairs 到 Lua 5.3 已经废弃,后续版本不要默认依赖。
-- LuaJIT 是否支持 pairs / ipairs 检查 __pairs / __ipairs,还要看版本和编译开关。
-- 基础业务代码里,最好先把表结构写清楚,不要靠这些版本差异行为兜底。
print("Lua 5.2 的 _ENV / goto / __pairs / __ipairs 示例只做版本提示,具体以项目运行时为准")
print("**********知识点六 Lua 5.3************")
-- 1. Lua 5.3 开始,number 内部区分 integer / float。
-- type 只能看到大类 number,math.type 可以看得更细。
print("type(1) 的结果:", type(1))
print("type(1.0) 的结果:", type(1.0))
if math.type then
print("math.type(1) 的结果:", math.type(1))
print("math.type(1.0) 的结果:", math.type(1.0))
print("1 == 1.0 的比较结果:", 1 == 1.0)
else
print("当前运行时没有 math.type,跳过 integer / float 子类型示例")
end
-- 2. Lua 5.3+ 支持原生位运算。
-- 但是 Lua 5.1 / LuaJIT 文件里不能直接写 1 & 3 这种语法,否则解析阶段就会报错。
-- 所以这里只保留示例,不在 Lua 5.1 文件里直接执行。
--[[
print("1 & 3 的结果:", 1 & 3) -- 按位与,输出:1
print("1 | 2 的结果:", 1 | 2) -- 按位或,输出:3
print("1 ~ 3 的结果:", 1 ~ 3) -- 按位异或,输出:2
print("1 << 2 的结果:", 1 << 2) -- 左移,输出:4
print("8 >> 1 的结果:", 8 >> 1) -- 右移,输出:4
]]
-- 3. table.move 是 Lua 5.3+ 的表库函数。
-- LuaJIT 2.1 也可能带这个扩展,但不要默认所有 LuaJIT 项目都有。
if table.move then
local source = { 1, 2, 3, 4, 5 }
local target = {}
-- 从 source 的 2 到 4 号位置,搬到 target 的 1 号位置开始。
table.move(source, 2, 4, 1, target)
print("table.move(source, 2, 4, 1, target) 后的 target:")
print("target[1]:", target[1]) -- 输出:2
print("target[2]:", target[2]) -- 输出:3
print("target[3]:", target[3]) -- 输出:4
-- 注意:table.move 不是“剪切删除”。
-- 这个示例里,source 原来的位置不会自动清空。
print("source[2] 仍然存在:", source[2]) -- 输出:2
else
print("当前运行时没有 table.move,跳过 table.move 示例")
end
-- 4. utf8 是 Lua 5.3+ 的标准库。
-- Lua 5.1 / LuaJIT 项目里不要默认存在这个库。
if utf8 and utf8.len then
local str = "你好"
print("字符串内容:", str)
print("#str 得到的是字节长度:", #str)
print("utf8.len(str) 得到的是 UTF-8 字符数量:", utf8.len(str))
else
print("当前运行时没有 utf8.len,跳过 utf8 库示例")
end
print("**********知识点七 Lua 5.4************")
-- 1. Lua 5.4 的 GC 可以工作在 incremental 和 generational 两种模式。
-- 这里先知道分代 GC 这个概念即可,不展开 GC 参数。
print("Lua 5.4 需要注意 incremental / generational 两种 GC 模式")
-- 2. local x <close> 是 Lua 5.4 的写法。
-- 它和 __close 元方法相关,常用于资源离开作用域时自动关闭。
-- Lua 5.1 / LuaJIT 文件里不能直接写这种语法,否则解析阶段就会报错。
-- 所以这里只保留示例,不在 Lua 5.1 文件里直接打开执行。
--[[
local resource <close> = xxx
]]
-- 这里要注意:
-- local x <close> = value 时,value 必须有 __close 元方法,或者是 nil / false。
-- nil 和 false 会被忽略,不会真的执行关闭逻辑。
-- 普通 table、number、string 没有 __close,不能硬塞进去。
-- to-be-closed 变量本身类似常量局部变量,声明后不要再重新赋值。
-- 3. coroutine.close 是 Lua 5.4 里和协程关闭相关的函数。
-- 如果当前运行时没有 coroutine.close,就跳过。
if coroutine.close then
print("当前运行时支持 coroutine.close")
else
print("当前运行时没有 coroutine.close,Lua 5.1 / LuaJIT 项目里不要默认可用")
end
-- coroutine.close 最值得关注的场景:
-- 协程里有 to-be-closed 变量,协程 yield 后不再恢复。
-- 这时可以用 coroutine.close 主动关闭协程,并触发挂起作用域里的 __close。
-- 下面这段包含 Lua 5.4 的 <close> 语法,Lua 5.1 / LuaJIT 不能直接解析,所以只保留为注释示例。
--[[
local mt = {
__close = function(resource, err)
print("关闭资源:", resource.name)
print("关闭时的错误:", err)
end
}
local co = coroutine.create(function()
local resource <close> = setmetatable({ name = "协程里的资源" }, mt)
print("协程开始执行")
coroutine.yield()
print("协程恢复后继续执行")
end)
coroutine.resume(co) -- 输出:协程开始执行
local ok, err = coroutine.close(co)
print("coroutine.close(co) 的返回值:", ok, err) -- 输出:true nil
-- 同时会触发 __close:
-- 关闭资源:协程里的资源
-- 关闭时的错误:nil
]]
-- coroutine.close(co) 要求 co 是挂起或者已经结束的协程。
-- 正常关闭成功时返回 true。
-- 如果关闭过程中出错,会返回 false 和错误对象。
-- 所以实际项目里最好接一下返回值,不要完全忽略。
print("**********总结************")
print("1. 写 Lua 版本差异,第一步先判断当前运行时。")
print("2. _VERSION 可以看基础版本,但 LuaJIT 下也可能显示 Lua 5.1。")
print("3. LuaJIT 更接近 Lua 5.1,不要当成完整 Lua 5.3 / Lua 5.4。")
print("4. #、pairs、ipairs、table.insert 这些坑,不只和版本有关,也和 table 的使用方式有关。")
print("5. Lua 5.1 常见 unpack、setfenv / getfenv、module。")
print("6. Lua 5.2 开始重点看 _ENV、goto、__pairs / __ipairs。")
print("7. Lua 5.3 开始重点看整数 / 浮点、原生位运算、table.move、utf8。")
print("8. Lua 5.4 开始重点看 GC 模式、<close>、__close、coroutine.close。")
print("9. 项目里最终以实际嵌入的 Lua 运行时为准,不要把本机能跑当成项目一定能跑。")
TestModule.lua
module("TestModule", package.seeall)
-- module 之后,普通非 local 变量会写到模块表里。
-- 这里可以理解成 TestModule.name = "测试模块"。
name = "测试模块"
-- module 之后,普通非 local 函数也会写到模块表里。
-- 这里可以理解成 TestModule.PrintName = function() ... end。
function PrintName()
print(name) -- 输出:测试模块
end
-- local 变量只在当前文件内部有效,不会挂到 TestModule 表上。
local localName = "模块内部变量"
function PrintLocalName()
print(localName) -- 输出:模块内部变量
end
-- 如果真的要写到全局表,必须显式写 _G。
-- 实际项目里一般不建议模块偷偷写全局变量,这里只是为了演示。
_G.globalName = "全局变量"
Main.lua
-- 加载 TestModule.lua。
-- Lua 5.1 的 module 写法会把内容挂到全局 TestModule 表上。
require("TestModule")
TestModule.PrintName() -- 输出:测试模块
print(TestModule.name) -- 输出:测试模块
TestModule.PrintLocalName() -- 输出:模块内部变量
-- localName 是 TestModule.lua 内部的局部变量,没有挂到 TestModule 表上。
print(TestModule.localName) -- 输出:nil
-- globalName 是 TestModule.lua 里显式写到 _G 的全局变量。
print(globalName) -- 输出:全局变量
print(_G.globalName) -- 输出:全局变量
TestModuleNew.lua
local M = {}
-- local 变量只在当前模块内部用,外部拿不到。
local localName = "模块内部变量"
-- M.xxx 表示这个字段要暴露给外部。
M.name = "测试模块"
function M.PrintName()
print(localName) -- 输出:模块内部变量
print(M.name) -- 输出:测试模块
end
-- 新模块写法一般显式 return 一张表。
return M
MainNew.lua
-- require 会拿到 TestModuleNew.lua 最后 return 出来的 M 表。
local TestModuleNew = require("TestModuleNew")
TestModuleNew.PrintName()
print(TestModuleNew.name) -- 输出:测试模块
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com