19.Lua版本差异

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 版本和编译开关
跨版本通用坑 #pairsipairstable.insert 不只和版本有关,也和 table 的使用方式有关
Lua 5.1 unpacksetfenv/getfenvmodule 老项目、Unity Lua 热更、LuaJIT 体系里常见
Lua 5.2 _ENVgoto__pairs / __ipairs 环境机制从 setfenv 转向 _ENV,迭代元方法也要注意版本差异
Lua 5.3 整数 / 浮点、原生位运算、table.moveutf8 新语法不要直接搬进 Lua 5.1 项目
Lua 5.4 incremental / generational GC、<close>__closecoroutine.close 资源关闭和协程关闭语义要注意

最终某个语法、某个库函数能不能用,还是以项目运行时为准。

LuaJIT

LuaJIT 可以理解成:一个更偏性能的 Lua 运行时

普通 Lua 大致是先把 Lua 源码编译成字节码,再由虚拟机解释执行。LuaJIT 里的 JITJust-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

因为第三个位置是 nilipairs 走到这里就停了,后面的 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 里旧项目常见写法主要有:

  • unpack
  • setfenv
  • getfenv
  • module

这里不用把它们都背下来,只要知道:这些写法在 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

表面上看,namePrintName 像是普通全局变量。但因为文件开头写了:

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 开始,很多版本差异会集中在环境、模块、控制流这些地方。

先记几个点:

  • _ENV
  • goto
  • __pairs / __ipairs
  • setfenv / 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 项目里不能默认这么写。

老项目如果要做位运算,可能会用:

  • bit
  • bit32
  • 项目自己封装的位运算工具

所以写热更代码时要先看运行时版本。不要直接把 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 / falsenilfalse 会被忽略,不会真的执行关闭逻辑。如果是普通 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>__closecoroutine.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 开始要注意 _ENVgoto__pairs / __ipairs 等差异。
  • Lua 5.3 开始要注意整数 / 浮点、原生位运算、table.moveutf8
  • Lua 5.4 开始要注意 GC 模式、local x <close>__closecoroutine.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

×

喜欢就点赞,疼爱就打赏