Lua 5.4 元表实战:给轻量数据加上领域行为
Lua 的 table 已经能表达大多数应用数据。元表的作用不是把 Lua 伪装成另一门面向对象语言,而是在确有需要时为 table 增加受控行为:方法查找、可读输出、运算规则或访问保护。一个好的元表模型仍然应该能用普通字段和函数解释。
示例使用 Lua 5.4.7,并依赖 Lua 5.4 系列的稳定语义。
用 __index 共享行为
账户包含余额和名称,修改必须经过验证方法。实例保存数据,Account table 保存共享行为。
local Account = {}
Account.__index = Account
function Account.new(name, opening_balance)
assert(type(name) == "string" and name ~= "", "name is required")
assert(type(opening_balance) == "number", "balance must be a number")
assert(opening_balance >= 0, "balance cannot be negative")
return setmetatable({
name = name,
balance = opening_balance,
}, Account)
end
function Account:deposit(amount)
assert(type(amount) == "number" and amount > 0, "amount must be positive")
self.balance = self.balance + amount
end
function Account:withdraw(amount)
assert(type(amount) == "number" and amount > 0, "amount must be positive")
if amount > self.balance then
return nil, "insufficient funds"
end
self.balance = self.balance - amount
return true
end
function Account:__tostring()
return string.format("Account(%s, %.2f)", self.name, self.balance)
end
local account = Account.new("Travel", 200)
account:deposit(50)
assert(account:withdraw(80))
print(account) -- Account(Travel, 170.00)
当读取实例中不存在的 deposit 时,__index 指向 Account,Lua 会在那里找到方法。冒号语法只是把实例作为第一个参数 self 传入;它没有隐藏类系统。
失败方式要有一致规则
示例对程序员错误使用 assert,例如传入字符串金额;对余额不足这种预期业务结果返回 nil, message。真实模块可以改用结构化错误值,但同一层 API 应保持一致。若某个方法抛错、另一个返回布尔值、第三个静默修正输入,调用者很难写出可靠流程。
不要用 __newindex 自动验证所有写入,除非确实需要代理 table。它只在普通赋值找不到现有键时触发,字段存在后行为可能与直觉不同。更清晰的做法是把内部数据放在独立 table,通过明确方法修改,或者约定字段只读并在模块边界隐藏实例构造细节。
谨慎添加“聪明”元方法
__tostring 适合诊断,因为它不改变领域语义。__eq、__add、__len 等运算元方法则需要更强的理由。两个账户相加究竟得到余额、组合账户还是错误?如果答案需要解释,命名函数通常更清楚。
还要避免很深的 __index 链来模拟多层继承。查找规则会变得隐蔽,覆盖方法也难以定位。组合几个小对象或把函数作为依赖传入,往往比复制类继承结构更符合 Lua 的简单模型。
模块封装与序列化
如果调用方不应该替换元表,可以不导出 Account table,或者使用文档定义的 __metatable 字段阻止普通的 getmetatable 与 setmetatable 操作。这只是减少意外耦合,不是安全沙箱;同一进程中拥有调试库或宿主能力的代码仍可能做更多事情。
序列化时不要直接遍历一个行为复杂的实例并期待恢复后仍然等价。明确提供 to_table,只输出名称、余额等数据,再由 Account.new 或专用恢复函数重新验证并附加元表。这样旧数据迁移、字段缺失和非法值都会经过一个可审查入口,也不会把函数或内部缓存写进配置文件。
测试不变量
领域对象的测试应围绕不变量:负数开户失败;存款必须为正;透支不改变余额;成功取款只扣一次;字符串输出不泄露敏感数据。测试可以直接运行于 lua 解释器,不需要框架才能覆盖这些规则。
元表最有价值的地方,是让普通 table 在模块边界表现出一组稳定协议。保持数据可见、行为明确、错误一致,就能得到轻量而可靠的模型,而不必在 Lua 里重建一套复杂类系统。