19.Lua语法知识总结

  1. 19.总结
    1. 19.1 核心要点速览
      1. 分组、注释和打印
      2. 变量类型
      3. 字符串
        1. 字符串基础(声明、长度、多行)
        2. 字符串拼接
        3. 类型转换(其他类型→字符串)
        4. 字符串库函数(string.xxx)
      4. 运算符
      5. 条件分支语句
      6. 循环语句
      7. 函数核心特性
        1. 声明与调用方式
        2. 参数处理
        3. 返回值机制
        4. 特殊特性
        5. 闭包核心示例
      8. 表(Table)
        1. 表的本质与基本概念
        2. 数组功能(顺序索引)
        3. 字典功能(自定义索引)
        4. 迭代器遍历对比
        5. 类与结构体实现(基于表)
        6. 表的公共方法(table.xxx)
      9. 多脚本与模块系统
        1. 变量作用域:全局与局部
        2. 模块加载机制:require
        3. 脚本卸载与重新加载
        4. 全局变量存储:_G表
      10. 特殊语法
        1. 多变量赋值
        2. 多返回值函数
        3. 逻辑运算符(and/or)
        4. 模拟三目运算符
      11. 协同程序(Coroutine)
        1. 协程创建与执行对比
        2. 挂起与分步执行
        3. 协程状态
        4. 核心API
      12. 元表(Metatable)
        1. 元表基础概念
        2. 核心元方法对比
        3. 运算重载元方法
        4. rawget与rawset(绕过元方法)
      13. 面向对象(模拟)
        1. 核心实现基础
        2. 封装:万物之父类设计
        3. 继承:子类注册机制
        4. 多态:方法重写
        5. 关键代码模板
      14. 自带库
        1. 时间库(os)
        2. 数学库(math)
        3. 路径库(package.path)
      15. 垃圾回收(GC)
      16. 深拷贝
        1. 浅拷贝与深拷贝对比
        2. 深拷贝实现核心逻辑
        3. 核心代码示例
    2. 19.2 面试题精选
      1. 基础题
        1. 1. Lua 里哪些值会被当成 false?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. and / or 的短路和返回值规则是什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. .. 拼接字符串有哪些坑?怎么写更稳?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 1. #t 为什么“不可靠”?什么时候能用?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. require 的缓存机制是什么?怎么强制重新加载?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. setmetatable / __index / rawget 分别解决什么问题?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 1. 用 Lua 实现“类/继承/多态”时,: 和 . 的差异会引发什么 bug?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

19.总结


19.1 核心要点速览

分组、注释和打印

功能 语法/示例 说明
单行注释 -- 注释内容 --[=[ 可临时关闭注释
多行注释 --[[ 多行注释内容 ]] 嵌套注释需用 --[=[ … ]=]
打印输出 print("Hello Lua") 自动拼接参数(如 print(1, "a") 输出 1 a

变量类型

Lua 共有 8 种变量类型,均为动态类型(赋值时自动推导,无需显式声明):

类型 核心特点 & 示例 特殊说明
nil 唯一值 nil,标识“空”
示例:local a(默认 nil)、a = nil(释放变量)
未赋值变量默认是 nil;给变量赋 nil 可回收其内存
boolean truefalse 两个值
示例:local flag = true
真假规则:仅 nilfalse 为“假”,其余(如 0、空字符串 ""、空表 {} )均为“真”
number 统一数值类型(整数 + 浮点数,内部以双精度浮点数存储,Lua5.3+ 支持整数区分)
示例:local num = 10(整数)、local pi = 3.14(浮点数)
语法无整数/浮点数区分,运算自动适配(如 5/2=2.55//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 暂停;同一时间仅一个协程运行,实现“伪并发”
  • 基础类型(nilbooleannumberstring)是“值类型”,赋值时拷贝内容;
  • 复杂类型(functiontableuserdatathread)是“引用类型”,赋值时传递引用(修改会影响原对象)。

字符串

字符串基础(声明、长度、多行)

功能 语法/示例 关键说明
声明方式 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 = 43 & 1 = 1
三目 不支持 -

条件分支语句

if 条件1 then
    -- 分支1
elseif 条件2 then
    -- 分支2
else
    -- 分支3
end

真值规则:仅 nilfalse 为假,其余(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,5i=6 退出);
    • 步长为负 → 变量 < 终止值 时退出(如 for i=5,1,-1i=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

关键:闭包延长了外部变量的生命周期,不同闭包实例独立维护各自的捕获变量(如add10add20分别保存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

  • 本质_G是存储所有全局变量的表,等同于全局作用域。
  • 遍历示例
    for k, v in pairs(_G) do
        print(k, type(v)) -- 输出全局变量名和类型
    end
    
  • 注意
    • 局部变量(local声明)不在_G中,如local a=1_G.anil
    • 全局变量可通过_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 → 2nil or 3 → 3

模拟三目运算符

  • 语法(条件) and 真结果 or 假结果
  • 示例
    local max = (x>y) and x or y  -- 等价于x>y?x:y  
    

注意:当「真结果」可能是 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")

元表(Metatable)

元表基础概念

  • 关系:元表是表的“父表”,子表执行特定操作时会调用元表中的元方法。
  • 设置/获取
    • 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.__indexObject.id
方法继承 子类方法不存在时查父类 person1:PrintId() → 调用Object.PrintId

多态:方法重写

场景 父类方法 子类重写
移动方法 GameObject:Move() → 坐标+1 Player:Move() → 调用父类逻辑后扩展
调用父类 self.base.Move(self)(显式传实例) Player:Move() 中通过base调用父类
多态表现 同一方法名,不同实现 player1:Move()gameObj:Move() 行为不同

关键代码模板

  1. 类定义与实例化

     Object = {}
     setmetatable(Object, Object)
     Object.__index = Object
    
     function Object:new()
         local obj = {}
         setmetatable(obj, self)
         self.__index = self
         return obj
     end
    
  2. 子类继承

     function Object:subClass(className)
         _G[className] = {}
         _G[className].base = self
         setmetatable(_G[className], self)
         self.__index = self
     end
    
  3. 多态方法重写

    -- 父类方法  
    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 只有 nilfalse 会被当成“假”,其余一律为“真”(包含 0""{})。
  • 这会影响 if 判断、and/or 的返回值逻辑,以及一些“短路写法”的正确性。
答题示例

Lua 里只有 nilfalse 是假,其它都是真。
0、空字符串、空表都是真。实际写条件判断时,不能用 if x then 来判断“是否为 0/空字符串/空表”,要写成更明确的比较条件。

参考文章
  • 4.简单变量类型
  • 12.特殊语法

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 才会走到默认值。

参考文章
  • 6.运算符
  • 12.特殊语法

3. .. 拼接字符串有哪些坑?怎么写更稳?

题目

.. 拼接非字符串时会怎样?遇到 nil 会怎样?如何避免拼接时报错?

深入解析
  • .. 会把 number 转成 string 再拼接,但 遇到 nil 会直接报错
  • 稳定写法:"xxx" .. tostring(v);或者用 string.format("%s", v)(对 nil 也安全)。
答题示例

.. 一般会做字符串拼接,number 会被转成字符串,但 nil 拼接会报错。
面试里我会强调安全写法:拼接前 tostring,比如 "a" .. tostring(x),或者用 string.format

参考文章
  • 5.字符串操作
  • 9.函数

进阶题

1. #t 为什么“不可靠”?什么时候能用?

题目

Lua 里 #table 的长度规则是什么?为什么带 nil 的数组 # 结果可能不稳定?生产代码里该怎么遍历?

深入解析
  • #t 适用于 连续、从 1 开始、无空洞(nil) 的数组部分。
  • 一旦出现 “空洞”,#t 的结果在不同版本/实现下可能不一致。
  • 遍历建议:
    • 连续数组:ipairsfor i=1,#t(确保无空洞)
    • 非连续/字典:pairs
答题示例

#t 只在 1 开始、连续非 nil 的数组场景可靠;出现 nil 空洞时结果就不可依赖,甚至不同版本会不一致。
所以遍历数组我会用 ipairs 或自己维护长度;遍历字典/混合表用 pairs

参考文章
  • 10.表

2. require 的缓存机制是什么?怎么强制重新加载?

题目

require 会不会重复执行脚本?它的缓存在哪里?怎么卸载并重新加载一个模块?

深入解析
  • require 首次加载会执行目标脚本,并把返回值缓存到 package.loaded[name]
  • 后续 require(name) 直接返回缓存,不会重复执行。
  • 强制重载:package.loaded[name] = nil 后再 require(name)
答题示例

require 默认只执行一次,结果会缓存在 package.loaded
要重载就把 package.loaded["xxx"] = nil,然后再 require("xxx") 触发重新执行。

参考文章
  • 11.多脚本

3. setmetatable / __index / rawget 分别解决什么问题?

题目

Lua 元表的 __index 的查找链是什么?rawget 为什么能“绕过”元表?常见用法有哪些?

深入解析
  • setmetatable(t, mt) 给表设置元表。
  • 当访问 t[k] 找不到键时,会看元表的 __index
    • 如果是表:去那个表里找 k
    • 如果是函数:调用 __index(t, k) 决定返回什么
  • rawget(t, k) 只查 t 自己,不触发 __index,用于调试/绕过代理表行为。
答题示例

__index 是“缺键时的兜底查找”:可以是表,也可以是函数。
rawget 只查原表本身,不走元表链,常用于绕过代理、调试元表行为。

参考文章
  • 14.元表

深度题

1. 用 Lua 实现“类/继承/多态”时,:. 的差异会引发什么 bug?

题目

Lua 里 obj:func()obj.func(obj) 等价吗?在调用父类方法时为什么有时必须用 . 并显式传 self

深入解析
  • : 调用会把调用者作为第一个参数(self)隐式传入。
  • 如果错误地用 : 去调用“类表上的方法”,可能把类表当成 self,导致修改的是类表字段(看起来像“静态变量被改了”)。
  • 典型场景:子类调用父类方法时,为了让父类逻辑作用在“实例对象”上,经常写 self.base.Move(self)
答题示例

: 本质是帮你隐式传 selfobj:func(x) 等价于 obj.func(obj, x)
所以调用父类方法时要保证传进去的是“实例”,不然就会把类表当 self,改到共享字段上,出现类似静态变量污染的问题;常见正确写法是 self.base.Move(self)

参考文章
  • 15.面向对象


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏