19.总结
19.1 核心要点速览
分组、注释和打印
| 功能 |
语法/示例 |
说明 |
| 单行注释 |
-- 注释内容 |
--[=[ 可临时关闭注释 |
| 多行注释 |
--[[ 多行注释内容 ]] |
嵌套注释需用 --[=[ … ]=] |
| 打印输出 |
print("Hello Lua") |
自动拼接参数(如 print(1, "a") 输出 1 a ) |
变量类型
Lua 共有 8 种变量类型,均为动态类型(赋值时自动推导,无需显式声明):
| 类型 |
核心特点 & 示例 |
特殊说明 |
nil |
唯一值 nil,标识“空” 示例:local a(默认 nil)、a = nil(释放变量) |
未赋值变量默认是 nil;给变量赋 nil 可回收其内存 |
boolean |
仅 true、false 两个值 示例:local flag = true |
真假规则:仅 nil 和 false 为“假”,其余(如 0、空字符串 ""、空表 {} )均为“真” |
number |
统一数值类型(整数 + 浮点数,内部以双精度浮点数存储,Lua5.3+ 支持整数区分) 示例:local num = 10(整数)、local pi = 3.14(浮点数) |
语法无整数/浮点数区分,运算自动适配(如 5/2=2.5,5//2=2 是整除) |
string |
字符串,用引号("abc"、'abc')或长字符串语法 [[ 多行内容 ]] 定义 示例:local str = "Hello"、local text = [[Lua 多行]] |
不可变(修改需重新生成);拼接用 ..(如 "Hi" .. "Lua" );长度用 #(如 #"abc"=3 ) |
function |
函数,支持匿名函数、闭包 示例:
function add(a,b) return a+b end(命名函数)
local f = function() print("匿名") end(匿名函数) |
可作为参数/返回值(高阶函数);闭包可捕获外部变量(如 local x=1; function f() x=x+1 end ) |
table |
核心复合类型,兼具数组(顺序结构)和字典(键值对)功能 示例:
local arr = {1,2,3}(数组,索引从 1 开始)
local dict = {name="Lua", ver=5.4}(字典,通过 dict.name 访问) |
动态扩容,无固定长度;是 Lua 唯一的复合数据结构,需用 table 库操作(如 table.insert 插入) |
userdata |
自定义用户数据(用于 C/C++ 扩展),存储外部语言(如 C)的内存数据 示例:(需结合 Lua C API 实现,纯 Lua 代码无直接示例) |
需通过 C 扩展创建,Lua 脚本中仅作为“黑盒”对象使用(如游戏引擎的自定义资源对象) |
thread |
协同程序(coroutine),非系统线程,是协作式多任务的载体 示例:
local co = coroutine.create(function() print("协程") end) |
通过 coroutine.resume 启动,coroutine.yield 暂停;同一时间仅一个协程运行,实现“伪并发” |
- 基础类型(
nil、boolean、number、string)是“值类型”,赋值时拷贝内容;
- 复杂类型(
function、table、userdata、thread)是“引用类型”,赋值时传递引用(修改会影响原对象)。
字符串
字符串基础(声明、长度、多行)
| 功能 |
语法/示例 |
关键说明 |
| 声明方式 |
str1 = "双引号"
str2 = '单引号'
str3 = [[长字符串(支持换行)]] |
双引号/单引号功能一致;[[...]]可多行,无需转义 |
| 长度计算(#) |
print(#"aBcdEfG字符串") → 16(汉字占3字节,UTF-8编码) |
#统计字节数(非字符数),如”中”占3字节→#"中"=3 |
| 多行打印 |
print("123\n456")(转义符\n)
str = [[我是\n韬]](长字符串保留换行) |
转义符\n显式换行;[[...]]直接保留换行格式 |
字符串拼接
| 方式 |
语法/示例 |
特点说明 |
运算符 .. |
print("123" .. "456") → 123456
str1=111; print(str1 .. 222) → 111222 |
自动拼接,支持非字符串(自动转字符串) |
string.format |
print(string.format("我今年%d岁", 18)) → 我今年18岁 |
占位符拼接:%d(数字)、%s(字符串)等 |
补充:.. 拼接遇到 nil 会报错,安全写法是先 tostring:"a" .. tostring(x)(见 9.函数.md 的示例修正)。
类型转换(其他类型→字符串)
| 方法 |
语法/示例 |
说明 |
tostring |
a=true; print(tostring(a)) → "true"
num=123; print(tostring(num)) |
强制转换任意类型为字符串(nil转”nil”) |
字符串库函数(string.xxx)
| 方法名 |
语法/示例 |
关键说明 |
注意事项 |
转大写 upper |
string.upper("abC") → "ABC" |
所有字符转大写(仅ASCII字母有效) |
不修改原字符串 |
转小写 lower |
string.lower("AbC") → "abc" |
所有字符转小写(仅ASCII字母有效) |
同上 |
翻转 reverse |
string.reverse("abc") → "cba" |
反转字符串字节顺序(汉字会乱码,因UTF-8多字节) |
索引从1开始(Lua通用规则) |
查找 find |
string.find("abCdef", "Cde") → 3 5(返回起始、结束索引) |
查找子串位置,无匹配返回nil |
区分大小写 |
截取 sub |
string.sub("abCdef", 3) → "Cdef"
string.sub("abCdef", 3, 4) → "Cd" |
截取子串,省略结束索引则到末尾 |
索引从1开始,支持负数(倒数) |
重复 rep |
string.rep("ab", 3) → "ababab" |
重复拼接字符串 |
次数需为非负整数 |
替换 gsub |
string.gsub("abCdeC", "C", "“) → “abde** 2"(返回新串+替换次数) |
全局替换子串,可指定替换次数(第3参数) |
返回两个值:新串、替换次数 |
转ASCII byte |
string.byte("Lua", 1) → 76(取第1个字符的ASCII码) |
提取字符的ASCII码(多字节字符仅取首字节) |
索引可选(默认1) |
ASCII转字符 char |
string.char(76, 117, 97) → "Lua" |
将ASCII码转为字符 |
支持多参数,按顺序拼接 |
运算符
| 类型 |
运算符 |
示例/特性 |
| 算术 |
+ - * / % ^ |
5^2 = 25(幂)、7%3 = 1(取余) |
| 比较 |
== ~= > < >= <= |
nil == false → false(仅 nil/false 为假) |
| 逻辑 |
and or not |
短路特性(如 true and false → false ) |
| 位运算 |
Lua 5.3+:& | ~ << >>(位与/位或/位异或/位移),Lua 5.2/5.1 需用 bit32(5.2)或第三方库 |
示例:8 >> 1 = 4、3 & 1 = 1 |
| 三目 |
不支持 |
- |
条件分支语句
if 条件1 then
-- 分支1
elseif 条件2 then
-- 分支2
else
-- 分支3
end
真值规则:仅 nil、false 为假,其余(0、空字符串等)均为真。
循环语句
| 类型 |
语法模板 |
执行逻辑 |
示例代码(简化) |
核心特点/注意事项 |
| while |
while 条件 do
-- 循环体
end |
先判条件,真则执行循环体 |
num=0; while num<5 do print(num); num=num+1 end |
可能一次不执行;需手动更新条件(否则死循环) |
| repeat-until |
repeat
-- 循环体
until 条件 |
先执行循环体,再判条件,真则退出 |
num=0; repeat print(num); num=num+1 until num>5 |
至少执行一次;退出条件与C# do-while 相反(Lua“条件真→退出”,C#“条件假→退出”) |
| 数值for |
for 变量 = 初始值, 终止值, 步长 do
-- 循环体
end |
变量按步长增减,超终止值(方向由步长定)则退出 |
-- 递增(步长1):
for i=2,5 do print(i) end
-- 自定义步长:
for i=1,5,2 do print(i) end
-- 递减:
for i=5,1,-1 do print(i) end |
步长可选(默认1);变量局部作用域; 初始值与步长方向矛盾则不执行(如 for i=1,5,-1) |
细节补充:
- 数值for的终止规则:
- 步长为正 → 变量 > 终止值 时退出(如
for i=1,5 → i=6 退出);
- 步长为负 → 变量 < 终止值 时退出(如
for i=5,1,-1 → i=0 退出);
- 若初始值与步长方向矛盾(如
for i=3,1,1),循环直接跳过。
- 死循环处理:
while true / repeat ... until false 需用 break 手动终止。
函数核心特性
声明与调用方式
| 类型 |
声明语法 |
调用语法 |
示例 |
| 命名函数 |
function 函数名(参数) ... end |
函数名(参数) |
function add(a,b) return a+b end
add(1,2) |
| 匿名函数 |
local 变量 = function(参数) ... end |
变量(参数) |
local f = function() print("匿名") end
f() |
参数处理
| 场景 |
规则说明 |
示例代码 |
输出结果 |
| 常规参数 |
按顺序传递,无需声明类型 |
function F(a) print(a) end
F("str") |
str |
| 参数不匹配 |
多传则丢弃,少传补nil |
F()
F(1,2,3) |
nil
1(丢弃2、3) |
| 变长参数 |
用...接收,存入arg表(Lua5.1+)或自定义表 |
function F(...) local t={...} print(#t) end
F(1,"a",true) |
3(表长度) |
返回值机制
| 类型 |
语法示例 |
调用与接收方式 |
注意事项 |
| 单返回值 |
function F(a) return a*2 end |
local res = F(5)
print(res) |
10 |
| 多返回值 |
function F(a) return a, "ok", true end |
local x, msg, flag = F(10)
print(x, msg, flag) |
10 ok true |
| 变量匹配 |
接收变量不足则忽略多余返回值,多余变量补nil |
local x = F(10)
print(x) |
10(忽略”ok”、true) |
特殊特性
| 特性 |
说明 |
示例代码 |
核心逻辑 |
| 函数类型 |
变量类型为function |
local f = function() end
print(type(f)) |
function |
| 重载限制 |
同名函数后声明覆盖前声明 |
function F() print(1) end
function F() print(2) end
F() |
2(调用最后声明的函数) |
| 函数嵌套 |
内部函数可返回给外部 |
function F() return function() print("嵌套") end end
local f = F()
f() |
嵌套 |
| 闭包 |
内部函数捕获外部变量 |
function F(x) return function(y) return x+y end end
local f = F(10)
print(f(5)) |
15(x=10被持久化) |
闭包核心示例
-- 闭包捕获外部变量x,形成持久化状态
function makeAdder(x)
return function(y)
return x + y -- 即使makeAdder执行完毕,x仍被保留
end
end
local add10 = makeAdder(10)
local add20 = makeAdder(20)
print(add10(5)) -- 15
print(add20(5)) -- 25
关键:闭包延长了外部变量的生命周期,不同闭包实例独立维护各自的捕获变量(如add10和add20分别保存x=10和x=20)。
表(Table)
表的本质与基本概念
- 万能容器:表是Lua唯一的复合类型,可实现数组、字典、类、结构体等所有复杂数据结构。
- 动态特性:无固定结构,运行时可任意增删改查键值对。
数组功能(顺序索引)
| 功能 |
语法/示例 |
注意事项 |
| 声明 |
t = {1, 2, "a", nil, true} |
索引默认从1开始,nil会影响#计算长度 |
| 取值 |
print(t[1]) → 1
print(t[5]) → true |
索引从1开始,0或负数索引需显式声明(如t[0] = "zero") |
| 长度 |
print(#t) → 4(若t = {1, nil, 3}则#t=1,因遇nil提前终止) |
#操作符不可靠,仅适用于连续非nil元素的数组 |
| 遍历 |
for i=1, #t do print(t[i]) end |
推荐用ipairs替代(见迭代器部分) |
字典功能(自定义索引)
| 功能 |
语法/示例 |
注意事项 |
| 声明 |
t = {["name"]="Lua", age=5.4, [100]="key"} |
键可以是任意类型(除nil),值可为任意类型 |
| 取值 |
print(t.name) → "Lua".
print(t["age"]) → 5.4 |
数字键不能用.访问(需[]),如t[100] |
| 修改/新增 |
t.name = "LuaNew".
t["version"] = 5.4 |
直接赋值,不存在的键自动创建 |
| 删除 |
t.name = nil.
t["version"] = nil |
赋值nil即可删除键值对 |
| 遍历 |
for k, v in pairs(t) do print(k, v) end |
必须用pairs遍历所有键值对 |
迭代器遍历对比
| 迭代器 |
语法 |
遍历范围 |
特点 |
ipairs |
for i,v in ipairs(t) |
仅顺序索引(1,2,3…) |
遇nil终止,无法遍历自定义索引 |
pairs |
for k,v in pairs(t) |
所有索引(含负数、字符串等) |
遍历所有键值对,无顺序保证 |
类与结构体实现(基于表)
| 功能 |
语法/示例 |
核心逻辑 |
| 类声明 |
Student = {age=18, name="Lua"}
function Student:printInfo() print(self.age) end |
表存储属性和方法,self指代实例 |
| 实例化 |
local stu = {}setmetatable(stu, {__index=Student}) |
通过元表__index继承类属性和方法 |
| 方法调用 |
stu:printInfo()(等价于Student.printInfo(stu)) |
:调用自动传递self参数 |
表的公共方法(table.xxx)
| 方法名 |
语法/示例 |
说明 |
insert |
table.insert(t, 3, "new")(在索引3插入元素)
table.insert(t, "new")(追加到末尾) |
插入元素,自动调整后续索引 |
remove |
table.remove(t, 2)(删除索引2的元素,返回其值)
table.remove(t)(删除末尾元素) |
删除元素,后续元素前移 |
sort |
table.sort(t)(升序)
table.sort(t, function(a,b) return a>b end)(降序) |
排序数组(仅适用于顺序索引表) |
concat |
table.concat(t, ",")(用逗号拼接元素)
table.concat(t)(无分隔符) |
拼接表元素为字符串 |
多脚本与模块系统
变量作用域:全局与局部
| 类型 |
声明方式 |
作用域 |
示例代码 |
注意事项 |
| 全局变量 |
直接赋值(无local) |
所有脚本可见 |
a = 10
for i=1,2 do c="test" end
print(c)(输出”test”) |
过多使用消耗内存,污染全局环境 |
| 局部变量 |
local 变量=值 |
仅当前作用域可见 |
for i=1,2 do local d="test" end
print(d)(输出nil) |
函数内建议用local声明,避免全局污染 |
模块加载机制:require
| 阶段 |
语法/逻辑 |
示例代码 |
核心说明 |
| 声明模块 |
脚本末尾return 导出表 |
-- module.lua
local M={}
M.fun=function() print("hi") end
return M |
导出表需包含对外接口,避免直接暴露内部变量 |
| 加载模块 |
local mod = require("模块名") |
local utils = require("utils")
utils.fun()(调用模块方法) |
首次加载执行脚本,缓存结果;重复加载直接返回缓存 |
| 返回值处理 |
接收require的返回值 |
local localVar = require("script")
print(localVar)(获取脚本返回的局部变量) |
脚本可通过return暴露局部变量,外部仅能访问返回值 |
脚本卸载与重新加载
| 操作 |
语法/逻辑 |
示例代码 |
应用场景 |
| 卸载脚本 |
package.loaded["模块名"] = nil |
package.loaded["LuaMultiScriptTest"] = nil
require("LuaMultiScriptTest")(重新执行脚本) |
修改脚本后强制重新加载,测试时常用 |
| 检查加载状态 |
package.loaded["模块名"] |
print(package.loaded["utils"])(返回true或模块导出值) |
判断模块是否已加载,避免重复操作 |
全局变量存储:_G表
特殊语法
多变量赋值
- 规则:变量与值左对齐匹配,值不足补
nil,值过多忽略。
- 示例:
a,b,c = 1,2 -- a=1, b=2, c=nil
x,y = y,x -- 交换变量值
多返回值函数
- 特性:函数可返回多个值,接收时按顺序匹配。
- 示例:
function f() return 1,2,3 end
a,b = f() -- a=1, b=2(忽略3)
逻辑运算符(and/or)
- 短路规则:
and:左假返左,左真返右。
or:左真返左,左假返右。
- 非布尔值:除
nil/false外均为真,如1 and 2 → 2,nil or 3 → 3。
模拟三目运算符
注意:当「真结果」可能是 false / nil 时,这种写法会失效(会继续落到 or 分支),需要改成显式 if/else。
协同程序(Coroutine)
协程创建与执行对比
| 类型 |
创建方法 |
返回类型 |
执行方式 |
特点 |
| 线程型 |
coroutine.create(func) |
thread |
coroutine.resume(co) |
返回首个值为执行状态(true/false),适合分步控制 |
| 函数型 |
coroutine.wrap(func) |
function |
co() |
直接返回函数结果,无状态返回,使用更简洁 |
挂起与分步执行
| 类型 |
挂起方法 |
执行结果 |
示例代码 |
| 线程型 |
coroutine.yield(...) |
resume(co) 返回 {true, v1, v2...} |
co = create(func); resum(co) |
| 函数型 |
coroutine.yield(...) |
co() 返回 v1, v2... |
co = wrap(func); print(co()) |
协程状态
| 状态 |
含义 |
检查方法 |
示例场景 |
dead |
执行完毕/未启动 |
coroutine.status(co) |
无yield的协程执行后 |
suspended |
挂起(可继续执行) |
coroutine.status(co) |
执行yield后 |
running |
正在执行 |
函数内coroutine.status() |
协程函数执行中 |
核心API
| 方法 |
作用 |
示例 |
coroutine.running() |
获取当前协程线程号 |
函数内print(coroutine.running()) |
coroutine.yield(...) |
挂起协程并返回值 |
coroutine.yield(1, "ok") |
元表基础概念
- 关系:元表是表的“父表”,子表执行特定操作时会调用元表中的元方法。
- 设置/获取:
setmetatable(子表, 元表):设置元表。
getmetatable(子表):获取元表。
核心元方法对比
| 元方法 |
触发条件 |
参数 |
示例代码 |
说明 |
__tostring |
子表被转为字符串(如print) |
function(子表) |
print(子表) → 调用元表的__tostring返回值 |
自定义表的字符串表示 |
__call |
子表被当作函数调用 |
function(子表, arg1, arg2...) |
子表(1, 2) → 执行元表的__call |
使表可像函数一样调用 |
__add |
子表参与+运算 |
function(表1, 表2) |
表1 + 表2 → 调用__add返回和 |
重载加法运算符 |
__index |
子表查找属性不存在时 |
- 表/函数:function(子表, 键) - 表:{键=值} |
子表.属性 → 查元表__index
__index=function(t,k) return t[k] or "默认值" |
属性查找的fallback机制,可设为函数或表 |
__newindex |
子表赋值属性不存在时 |
function(子表, 键, 值) |
子表.属性=值 → 调用__newindex
__newindex=function(t,k,v) t[k]=v*2 end |
控制属性赋值行为,可设为函数或表 |
运算重载元方法
| 运算符 |
元方法 |
示例 |
+ |
__add |
a + b → __add(a, b) |
- |
__sub |
a - b → __sub(a, b) |
* |
__mul |
a * b → __mul(a, b) |
/ |
__div |
a / b → __div(a, b) |
== |
__eq |
a == b → __eq(a, b) |
..(拼接) |
__concat |
a .. b → __concat(a, b) |
rawget与rawset(绕过元方法)
| 函数 |
作用 |
示例代码 |
对比 |
rawget(表, 键) |
直接获取表属性,不触发__index |
rawget(t, "key") → 直接查t["key"] |
与表.键的区别:后者会触发__index |
rawset(表, 键, 值) |
直接设置表属性,不触发__newindex |
rawset(t, "key", 10) → 直接设t["key"]=10 |
与表.键=值的区别:后者会触发__newindex |
面向对象(模拟)
核心实现基础
| 概念 |
实现方式 |
关键代码示例 |
| 类定义 |
表 + 元表 |
Object = {}(万物之父类) |
| 实例化 |
:new() 方法返回表对象,设置元表 |
function Object:new() local obj={}; setmetatable(obj,self); self.__index = self end |
| 属性/方法 |
表字段存储属性,函数存储方法 |
Object.id=1; function Object:PrintId() print(self.id) end |
封装:万物之父类设计
| 功能 |
实现逻辑 |
示例效果 |
| 实例化 |
新建表,设元表为当前类,__index指向自身 |
local obj = Object:new() → obj是表,元表为Object |
| 属性访问 |
实例无属性时查元表__index |
obj.id → 查Object.id(默认1),实例赋值后覆盖 |
| 方法调用 |
方法存储在类表,self传实例 |
obj:PrintId() → 调用Object.PrintId(obj) |
继承:子类注册机制
| 步骤 |
核心代码 |
继承链 |
| 注册子类 |
Object:subClass("Person") → 在_G中创建子类表 |
_G.Person = {}; setmetatable(Person, Object) |
| 属性继承 |
子类无属性时查父类元表__index |
person1.id → 查Person.__index→Object.id |
| 方法继承 |
子类方法不存在时查父类 |
person1:PrintId() → 调用Object.PrintId |
多态:方法重写
| 场景 |
父类方法 |
子类重写 |
| 移动方法 |
GameObject:Move() → 坐标+1 |
Player:Move() → 调用父类逻辑后扩展 |
| 调用父类 |
self.base.Move(self)(显式传实例) |
Player:Move() 中通过base调用父类 |
| 多态表现 |
同一方法名,不同实现 |
player1:Move() 与 gameObj:Move() 行为不同 |
关键代码模板
类定义与实例化
Object = {}
setmetatable(Object, Object)
Object.__index = Object
function Object:new()
local obj = {}
setmetatable(obj, self)
self.__index = self
return obj
end
子类继承
function Object:subClass(className)
_G[className] = {}
_G[className].base = self
setmetatable(_G[className], self)
self.__index = self
end
多态方法重写
-- 父类方法
function GameObject:Move()
self.posX = self.posX + 1; print(self.posX)
end
-- 子类重写
function Player:Move()
self.base.Move(self);
print("Player移动")
end
自带库
时间库(os)
| 函数 |
说明 |
参数 |
返回值 |
示例 |
time() |
获取当前时间戳(秒) |
无 |
数值(时间戳) |
print(os.time()) → 1695818702 |
time({year, month, day}) |
生成指定日期的时间戳 |
表(year, month, day等) |
数值(时间戳) |
os.time({year=2014, month=8, day=14}) → 1407988800 |
date("*t") |
获取当前时间的表对象 |
格式字符串"*t" |
表(含year, month, day等字段) |
local t=os.date("*t"); print(t.year) → 2023 |
数学库(math)
| 函数 |
说明 |
参数 |
返回值 |
示例 |
abs(x) |
取绝对值 |
数值x |
数值(绝对值) |
math.abs(-1) → 1 |
deg(x) |
弧度转角度 |
弧度值x |
角度值 |
math.deg(math.pi) → 180 |
cos(x) |
余弦函数(弧度) |
弧度值x |
余弦值 |
math.cos(math.pi) → -1 |
floor(x) |
向下取整 |
数值x |
整数(≤x的最大整数) |
math.floor(2.6) → 2 |
ceil(x) |
向上取整 |
数值x |
整数(≥x的最小整数) |
math.ceil(5.2) → 6 |
max(a, b) |
取最大值 |
数值a, b |
较大值 |
math.max(1, 2) → 2 |
min(a, b) |
取最小值 |
数值a, b |
较小值 |
math.min(4, 5) → 4 |
modf(x) |
分离整数和小数部分 |
数值x |
整数部分和小数部分 |
math.modf(1.2) → 1, 0.2 |
pow(x, y) |
幂运算(x^y) |
底数x, 指数y |
数值(x的y次方) |
math.pow(2, 5) → 32 |
randomseed(seed) |
设置随机数种子 |
种子值seed |
无 |
math.randomseed(os.time()) |
random(n) |
生成[1, n]的随机整数 |
最大值n |
随机整数 |
math.random(100) → 20(示例值) |
sqrt(x) |
开平方 |
数值x |
平方根 |
math.sqrt(4) → 2 |
路径库(package.path)
| 功能 |
说明 |
操作方式 |
示例 |
| 查看加载路径 |
获取Lua脚本搜索路径 |
print(package.path) |
输出路径字符串(如;.\?.lua;...) |
| 修改加载路径 |
拼接新路径(如添加自定义目录) |
package.path = package.path .. ";C:\\\\" |
路径末尾添加 ;C:\(Lua 字符串里 \ 需转义) |
垃圾回收(GC)
- Lua 垃圾回收从 5.1 的增量式三色标记-清除演进到 5.4 的分代式回收,既兼顾低停顿,又优化短命对象的处理。
- 三色标记机制将对象分为白、灰、黑三种状态:
- 白色:未检查、潜在垃圾;
- 灰色:已可达、待扫描其子引用;
- 黑色:自身及子对象均已扫描、安全保留。
- 分代回收将“新生代”对象高频回收、“老生代”对象低频处理,通过晋升策略减少对长寿对象的重复扫描。
- 弱引用表(
__mode="k"|"v"|"kv")让缓存或循环引用的一端不阻止 GC,自动释放不再需要的对象,避免内存泄漏。
- 终结器(
__gc 元方法)为 userdata 提供析构式清理入口,在对象回收前执行自定义逻辑,确保外部资源(文件、网络、C 内存等)得到正确释放。
collectgarbage() 系列接口允许:
- 统计内存(
"count")、触发完整回收("collect");
- 调节回收阈值(
"setpause")与步长("setstepmul");
- 手动步进(
"step")、暂停/重启 GC("stop"/"restart")。
- 在性能敏感场景(如游戏关键帧或热更新逻辑)建议结合手动触发以控制停顿。
深拷贝
浅拷贝与深拷贝对比
| 类型 |
浅拷贝(=) |
深拷贝(自定义实现) |
| 基本类型(string/number/boolean) |
复制值,新变量与原变量独立 |
同浅拷贝(无需额外处理) |
表(table) |
复制引用,修改新表会影响原表 |
递归复制所有层级表,新表与原表完全独立 |
| 示例 |
tbl2=tbl1; tbl2.x=4 → tbl1.x=4 |
tbl2=clone(tbl1); tbl2.x=4 → tbl1.x=1 |
深拷贝实现核心逻辑
| 步骤 |
代码要点 |
说明 |
| 1. 缓存表防循环引用 |
local lookup_table = {} |
记录已拷贝的表,避免循环引用导致无限递归 |
| 2. 处理基本类型 |
if type(object)~="table" then return object |
非表类型直接返回(值复制) |
| 3. 处理已拷贝表 |
elseif lookup_table[object] then return lookup_table[object] |
若表已拷贝,直接返回缓存的新表 |
| 4. 递归拷贝表 |
new_table[_copy(key)] = _copy(value) |
对表的每个键值对递归调用拷贝函数,键和值都可能是表 |
| 5. 保留元表 |
setmetatable(new_table, getmetatable(object)) |
新表继承原表的元表,保持行为一致性 |
核心代码示例
function clone(obj)
local lookup = {}
local function _copy(o)
if type(o) ~= "table" then return o end
if lookup[o] then return lookup[o] end
local new = {}
lookup[o] = new
for k, v in pairs(o) do
new[_copy(k)] = _copy(v)
end
return setmetatable(new, getmetatable(o))
end
return _copy(obj)
end
19.2 面试题精选
基础题
1. Lua 里哪些值会被当成 false?
题目
Lua 的真值规则是什么?0、空字符串 ""、空表 {} 分别算真还是假?
深入解析
- Lua 只有
nil 和 false 会被当成“假”,其余一律为“真”(包含 0、""、{})。
- 这会影响
if 判断、and/or 的返回值逻辑,以及一些“短路写法”的正确性。
答题示例
Lua 里只有 nil 和 false 是假,其它都是真。
0、空字符串、空表都是真。实际写条件判断时,不能用 if x then 来判断“是否为 0/空字符串/空表”,要写成更明确的比较条件。
参考文章
2. and / or 的短路和返回值规则是什么?
题目
Lua 的 and / or 除了短路,还有什么“非布尔”特性?a and b / a or b 分别返回什么?
深入解析
and:左操作数为“假”→返回左值;为“真”→返回右值。
or:左操作数为“真”→返回左值;为“假”→返回右值。
- 因为返回的是“参与运算的原值”,常用来写默认值:
x = x or default。
答题示例
and 是“左假返回左,左真返回右”,or 是“左真返回左,左假返回右”,并且会短路。
所以 Lua 里经常用 x = x or default 做默认值;需要注意 false/nil 才会走到默认值。
参考文章
3. .. 拼接字符串有哪些坑?怎么写更稳?
题目
.. 拼接非字符串时会怎样?遇到 nil 会怎样?如何避免拼接时报错?
深入解析
.. 会把 number 转成 string 再拼接,但 遇到 nil 会直接报错。
- 稳定写法:
"xxx" .. tostring(v);或者用 string.format("%s", v)(对 nil 也安全)。
答题示例
.. 一般会做字符串拼接,number 会被转成字符串,但 nil 拼接会报错。
面试里我会强调安全写法:拼接前 tostring,比如 "a" .. tostring(x),或者用 string.format。
参考文章
进阶题
1. #t 为什么“不可靠”?什么时候能用?
题目
Lua 里 #table 的长度规则是什么?为什么带 nil 的数组 # 结果可能不稳定?生产代码里该怎么遍历?
深入解析
#t 适用于 连续、从 1 开始、无空洞(nil) 的数组部分。
- 一旦出现 “空洞”,
#t 的结果在不同版本/实现下可能不一致。
- 遍历建议:
- 连续数组:
ipairs 或 for i=1,#t(确保无空洞)
- 非连续/字典:
pairs
答题示例
#t 只在 1 开始、连续非 nil 的数组场景可靠;出现 nil 空洞时结果就不可依赖,甚至不同版本会不一致。
所以遍历数组我会用 ipairs 或自己维护长度;遍历字典/混合表用 pairs。
参考文章
2. require 的缓存机制是什么?怎么强制重新加载?
题目
require 会不会重复执行脚本?它的缓存在哪里?怎么卸载并重新加载一个模块?
深入解析
require 首次加载会执行目标脚本,并把返回值缓存到 package.loaded[name]。
- 后续
require(name) 直接返回缓存,不会重复执行。
- 强制重载:
package.loaded[name] = nil 后再 require(name)。
答题示例
require 默认只执行一次,结果会缓存在 package.loaded。
要重载就把 package.loaded["xxx"] = nil,然后再 require("xxx") 触发重新执行。
参考文章
题目
Lua 元表的 __index 的查找链是什么?rawget 为什么能“绕过”元表?常见用法有哪些?
深入解析
setmetatable(t, mt) 给表设置元表。
- 当访问
t[k] 找不到键时,会看元表的 __index:
- 如果是表:去那个表里找
k
- 如果是函数:调用
__index(t, k) 决定返回什么
rawget(t, k) 只查 t 自己,不触发 __index,用于调试/绕过代理表行为。
答题示例
__index 是“缺键时的兜底查找”:可以是表,也可以是函数。
rawget 只查原表本身,不走元表链,常用于绕过代理、调试元表行为。
参考文章
深度题
1. 用 Lua 实现“类/继承/多态”时,: 和 . 的差异会引发什么 bug?
题目
Lua 里 obj:func() 和 obj.func(obj) 等价吗?在调用父类方法时为什么有时必须用 . 并显式传 self?
深入解析
: 调用会把调用者作为第一个参数(self)隐式传入。
- 如果错误地用
: 去调用“类表上的方法”,可能把类表当成 self,导致修改的是类表字段(看起来像“静态变量被改了”)。
- 典型场景:子类调用父类方法时,为了让父类逻辑作用在“实例对象”上,经常写
self.base.Move(self)。
答题示例
: 本质是帮你隐式传 self,obj:func(x) 等价于 obj.func(obj, x)。
所以调用父类方法时要保证传进去的是“实例”,不然就会把类表当 self,改到共享字段上,出现类似静态变量污染的问题;常见正确写法是 self.base.Move(self)。
参考文章
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com