常见坑与误区
全站的坑收拢成一份清单,按主题分组,每条给正解和出处。查无实据的标「来源未考」。
语法与类型
- 数组索引从 1 开始,不是 0。从 0 索引语言迁来最容易栽在这里。
- 不等号是
~=不是!=;^和/恒返回浮点数(10/5得2.0),要整数结果必须用//。 1 / 0不报错,结果是inf;0 / 0得nan。只有整数//或%除数为零才抛错。要除零报错须显式判断。//和%都向负无穷取整:-1 // 2是 -1,-1 % 2是 1。- 字符串不可变,任何「修改」都产生新字符串;
string.len/string.sub按字节操作,中文截断会切坏字符,处理 UTF-8 用utf8库。 - Lua 没有内置 split,用
string.gmatch加模式([^,]+)模拟。 pairs遍历哈希部分无序,不能当有序遍历用;ipairs从 1 连续遍历,遇到 nil 就停;#t对含空洞的稀疏表不给出确定结果,不要当「元素个数」用。- 不加
local就是全局变量,这是默认行为,也是要避免的习惯。
来源:操作符和表达式、常见问题与解决方案、字符串处理、Lua标准库参考。
表与元表
t[{}] = "value"之后t[{}]取回 nil:每次{}都创建新表,写入和读取用的是两个不同对象。表作键必须存进局部变量复用。__index/__newindex元方法里直接读写原始表会无限递归,绕开要用rawget/rawset。pairs和#都不走__index链:pairs(instance)看不到类上的方法,继承来的字段不计入#。- 一张表只有一张元表,第二次
setmetatable是整体替换,先挂的链会静默丢失。 __newindex一旦定义就接管写入,普通赋值不再落在本表,要写本表得用rawset。
面向对象
- 点号冒号混用是最多发的错误:冒号定义的方法用点号调用,
self为 nil 报attempt to index a nil value;点号定义冒号调用则参数错位。 - 忘了
Class.__index = Class:元表挂上了但查找在实例处就停,实例上找不到方法。 - 子类方法里调父类同名方法必须显式传 self(
Account.withdraw(self, amount)),写成Account:withdraw(amount)会把 Account 表本身当 self。 - 构造函数里写死类名(
setmetatable({}, Dog))会让子类实例全挂到父类上,要写setmetatable({}, self)。 - 方法作回调传递时
self会丢,用闭包绑定对象或调用时手动补。
协程
coroutine.wrap没有错误保护层,协程内错误直接抛给调用者;需要容错用create+resume,并检查第一个返回值。- 恢复已死亡的协程得到
cannot resume dead coroutine,不会崩溃但也不会执行;调度器要在协程死亡时把它移出任务列表。 - 阻塞调用(
io.read、同步网络请求、os.execute)会卡住整个进程,其他协程毫无办法。 - 协程不是并行:同一 Lua 状态里任意时刻只有一个协程在跑,拉高计算吞吐与协程无关。
- 同一个 Lua 状态机不是线程安全的,多线程访问要外部加锁或每线程独立状态机。
来源:协程的应用场景与最佳实践、协程与线程的比较。
模块
module函数自 Lua 5.2 起已移除,不要再写。- 模块文件忘了
return,require返回true;C 扩展的luaopen_mylib返回 0 也是同样效果。 - 改了模块文件后不清
package.loaded缓存,require拿到的仍是旧版本;热加载就是清缓存再require,但注意模块状态丢失和依赖未同步重载。
来源:Lua的编码规范、编写Lua C扩展、模块的定义与加载、热加载与动态更新。
错误处理
io.open打开失败不抛错,返回nil, errmsg,所以pcall(io.open, ...)捕获不到「文件不存在」,真正出错的是随后对 nil 调用read。先检查返回值。error默认在消息前附加位置信息;第二个参数传 0 去掉前缀,传 2 用调用者的位置。- 访问表中不存在的键只得到 nil,不报错;只有对 nil 本身做索引才报错。
GC 与性能
- 「Lua 用引用计数做 GC」是误传,实际是标记-清除(增量/分代);引用计数只是个别内部场合的思想借用。循环引用不会泄漏,标记-清除能整组回收。
collectgarbage没有"start"选项,重新启动用"restart";5.4 里setpause/setstepmul已过时,新代码用collectgarbage("incremental", ...)形式。- 「用表打包参数比按位置传参快」是误区:表打包要额外分配一张表,通常更慢。表传参图的是命名参数和可读性。
- 常量折叠只对两个操作数都是常量的表达式生效,有变量参与就失效。
- 元方法调用有额外开销,频繁调用的热路径避免元方法;
debug库操作同样有开销,别拿它做生产性能分析。 jit.dump和-jp观察的是 JIT trace 编译与采样,不是 GC 分析器;Lua 5.4 标准解释器没有内置 GC 分析器。- 「Lua 沙箱能防住恶意代码」是误传:沙箱里的代码拿到
debug就能读钩子、改钩子、访问注册表,ffi.load进来的 C 代码绕过 Lua 层的一切检查,定时与内存布局差异构成侧信道。沙箱只是纵深防御的一层,硬隔离在操作系统一侧。
来源:引用计数与标记-清除算法、垃圾回收的调优、Lua代码的优化技巧、代码生成与优化、垃圾回收的优化策略。
实现层误传
- 「Lua 协程切换用了 setjmp/longjmp」是误传:切换只交换栈指针和
CallInfo,setjmp/longjmp只用于错误恢复,所以协程只能同线程协作切换。 - 「预编译字节码块可以跨平台分发」不成立:官方明确不保证
luac -o产物跨机器可移植,要在目标平台重编译。 - Lua 脚本层没有条件编译,平台判断只能在运行时做(
package.config,LuaJIT 下可用jit.os)。 - 「Lua 内部值类型叫 TaggedValue」是讹传,5.4/5.5 源码里是
TValue。
来源:协程的底层实现、字节码与虚拟机、跨平台发布Lua应用、Lua 5.5 源码解析、沙箱与不可信代码。