20.Lua语法知识总结

  1. 20.总结
    1. 20.1 核心要点速览
      1. 分组、注释和打印
      2. 简单变量类型
      3. 字符串操作
      4. 运算符
      5. 条件分支语句
      6. 循环语句
      7. 函数
      8. 模块与多脚本
      9. 特殊语法
      10. 协同程序
      11. 元表
      12. 面向对象
      13. 自带库
      14. 垃圾回收
      15. 错误处理
      16. Lua版本差异
    2. 20.2 面试题精选
      1. 基础题
        1. 1. Lua 里哪些值会被当成 false?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. and / or 的短路和返回值规则是什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. .. 拼接字符串有哪些坑?怎么写更稳?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        4. 4. Lua 函数参数个数不匹配会怎样?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 1. #t 为什么“不可靠”?什么时候能用?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. pairs 和 ipairs 的区别是什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. require 的缓存机制是什么?怎么强制重新加载?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        4. 4. setmetatable / __index / rawget 分别解决什么问题?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        5. 5. pcall 和 xpcall 怎么选?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 1. 用 Lua 实现“类 / 继承 / 多态”时,: 和 . 的差异会引发什么 bug?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. Lua GC 回收的是什么?是不是引用计数?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. LuaJIT 和 Lua 5.3 / 5.4 是什么关系?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        4. 4. Lua 5.1 的 setfenv 和 Lua 5.2 的 _ENV 有什么关系?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        5. 5. Lua 5.4 的 __close 和 __gc 有什么区别?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

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
  • 只有 nilfalse 是假。
  • 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 可能是 falsenil,这个写法会失效,要改成显式 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.timeos.date 时间戳、日期表
math absfloorceilrandomsqrt 数学计算
string findsubgsubformat 字符串处理
table insertremovesortconcat 表处理
package package.pathpackage.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

ab 原来互相引用,但外部已经访问不到它们了,后续 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 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 资源关闭和协程关闭语义要注意

跨版本通用坑:

  • # 对有洞 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 只有 nilfalse 会被当成“假”,其余一律为“真”。
  • 0、空字符串、空表在 Lua 里都是真。
  • 这个点会影响 if 判断、and/or 的返回值逻辑,以及模拟三目写法。
答题示例

Lua 里只有 nilfalse 是假,其它都是真。
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. pairsipairs 的区别是什么?

题目

pairsipairs 分别适合遍历什么表?各自有什么坑?

深入解析
  • 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. pcallxpcall 怎么选?

题目

Lua 里 pcallxpcall 都能保护调用,它们有什么区别?项目里什么时候用 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

×

喜欢就点赞,疼爱就打赏