20.总结
20.1 核心要点速览
分组、注释和打印
| 功能 | 语法/示例 | 说明 |
|---|---|---|
| 编辑器分组 | --#region / --#endregion |
主要给编辑器折叠用,不是 Lua 语言本身的特殊语法 |
| 单行注释 | -- 注释内容 |
最常用,适合解释代码、临时屏蔽一行逻辑 |
| 多行注释 | --[[ 多行注释内容 ]] |
多行注释最常见写法;]]--、--]] 本质上还是围绕 --[[ ... ]] 展开 |
| 打印输出 | print("Hello Lua") |
可以传多个参数,调试时建议把变量含义一起打出来 |
print("playerId:", playerId)
print("当前状态:", state)
实际写脚本时,print 不要只打一堆裸值。日志里带上字段名,后面查问题会舒服很多。
简单变量类型
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
这里很容易被 C# / C++ 习惯带偏。Lua 里判断“数字是不是 0”“字符串是不是空”,都要写明确条件。
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
字符串拼接用 ..:
print("123" .. "456") -- 123456
number 通常可以参与拼接,但 nil 不行。
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--
-- a += 1
-- 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
条件分支语句
Lua 条件分支:
if 条件1 then
-- 分支1
elseif 条件2 then
-- 分支2
else
-- 分支3
end
重点:
- Lua 里是
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:
while true do
if condition then
break
end
end
函数
函数声明:
function Add(a, b)
return a + b
end
匿名函数:
local func = function()
print("匿名函数")
end
Lua 函数本身也是值,可以赋给变量、作为参数、作为返回值。
参数数量不匹配时:
| 情况 | 结果 |
|---|---|
| 少传参数 | 缺少的位置补 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
闭包会捕获外部变量,并延长这个变量的生命周期。后面写回调、事件、异步逻辑时,这个点很常见。
表
表是 Lua 里最核心的复合数据结构,可以当数组、字典、对象、配置表使用。
数组:
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
局部变量:
local a = 1
Lua 里不写 local,默认就是全局变量,会进入 _G 表。脚本一多,全局变量很容易污染环境,所以项目里能写 local 就写 local。
require 用来加载脚本:
local TestModule = require("TestModule")
require 的几个点:
- 第一次加载时会执行脚本。
- 脚本最后可以
return一个值,常见是返回模块表。 - 加载结果会缓存到
package.loaded。 - 第二次
require同一个模块,通常直接返回缓存,不会重复执行。
强制重载:
package.loaded["TestModule"] = nil
local TestModule = require("TestModule")
新模块写法更推荐:
local M = {}
function M.Test()
print("Test")
end
return M
老 Lua 5.1 代码里可能看到:
module("TestModule", package.seeall)
这种写法要认识,但新代码一般不建议继续这么写。它会改变当前文件后面非 local 变量和函数的归属,读起来不如 local M = {}; return M 清楚。
特殊语法
多变量赋值:
a, b, c = 1, 2
print(a) -- 1
print(b) -- 2
print(c) -- nil
值多了会丢弃,值少了会补 nil。
交换变量:
a, b = b, a
多返回值:
function Test()
return 1, 2, 3
end
local a, b = Test()
print(a) -- 1
print(b) -- 2
and / or 模拟三目:
local result = condition and trueValue or falseValue
注意:如果 trueValue 可能是 false 或 nil,这个写法会失效,要改成显式 if / else。
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)
协程可以用 yield 挂起:
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 协程不是系统线程,同一时间还是一段 Lua 代码在跑。它更像是“函数执行到一半先停住,下次再接着跑”。
元表
元表可以理解成一张规则表。普通表遇到某些特殊操作时,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")
这个在调试代理表、对象系统、继承链时很有用。
面向对象
Lua 没有 C# 那种原生 class,常见做法是:
table + function + metatable
最基础的对象模型:
Object = {}
function Object:new()
local obj = {}
setmetatable(obj, self)
self.__index = self
return obj
end
实例找不到字段或方法时,会通过 __index 去类表里找。
冒号调用要重点记:
obj:Move()
等价于:
obj.Move(obj)
也就是自动把调用者作为第一个参数 self 传进去。
子类调用父类方法时,经常会写:
self.base.Move(self)
这里用 . 调用父类方法,并且显式传入当前实例。否则很容易把类表当成 self,导致字段写到错误位置。
Lua 面向对象这一套本质上是约定出来的,不是语言强制的。实际项目里关键是把规则统一好:字段放哪里、方法放哪里、继承链怎么查、父类方法怎么调。
自带库
常用库先记这些:
| 库 | 常用内容 | 说明 |
|---|---|---|
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 库基础阶段不要乱用,但 debug.traceback 对错误堆栈很有用。
垃圾回收
Lua 有自动垃圾回收,不需要像 C/C++ 那样手动释放 Lua 对象。
基础阶段先记两个:
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
a 和 b 原来互相引用,但外部已经访问不到它们了,后续 GC 仍然可以回收。
所以不要把 Lua GC 简单理解成引用计数。弱表、__gc、增量式 GC、分代 GC 都是更细的机制,基础语法篇先知道方向即可。
实际项目里不要在热路径里频繁手动:
collectgarbage("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 可以看基础 Lua 版本,但 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)中间插入会移动后面的元素。
LuaJIT 不是 Lua 最新版。它更接近 Lua 5.1,虽然带部分扩展,但不能默认支持 Lua 5.3 / 5.4 的全部语法。
不要在 LuaJIT / Lua 5.1 项目里默认写:
local a = 1 & 2
local x <close> = xxx
最终以项目真正嵌入的 Lua 运行时为准。
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