第 12 章 面向对象 Lua
第 12 章 面向对象 Lua
Lua 没有内置类,但表、函数和元表足以实现对象系统。理解这些机制比记某个“类库语法”更重要,因为不同项目的 Lua 面向对象实现可能不同。
用表表示对象
最简单的对象是一个包含数据与函数的表:
local account = {
owner = "Ada",
balance = 100
}
function account.deposit(self, amount)
self.balance = self.balance + amount
end
account.deposit(account, 50)
print(account.balance) -- 150
使用冒号语法时,self 会自动作为第一个参数传入:
function account:withdraw(amount)
if amount > self.balance then
return nil, "insufficient balance"
end
self.balance = self.balance - amount
return self.balance
end
account:withdraw(25)
每个对象都复制方法会浪费内存。更常见的做法是把方法放在共享表里。
类与实例
创建类表,并让实例通过 __index 查找方法:
local Account = {}
Account.__index = Account
function Account.new(owner, balance)
return setmetatable({
owner = owner,
balance = balance or 0
}, Account)
end
function Account:deposit(amount)
if type(amount) ~= "number" or amount <= 0 then
return nil, "amount must be positive"
end
self.balance = self.balance + amount
return self.balance
end
function Account:withdraw(amount)
if amount <= 0 then
return nil, "amount must be positive"
end
if amount > self.balance then
return nil, "insufficient balance"
end
self.balance = self.balance - amount
return self.balance
end
local account = Account.new("Ada", 100)
account:deposit(50)
print(account.balance) -- 150
实例没有 deposit 键,因此访问 account.deposit 时,元表的 __index 指向 Account,从而找到共享方法。
实例判断
可以为每个类维护独立元表,这样就能检查实例身份:
local function isInstance(value, class)
local meta = getmetatable(value)
return type(value) == "table" and meta == class
end
print(isInstance(account, Account)) -- true
如果有继承,需要沿原型链向上检查,或者用显式的类标记。
继承与多态
通过“类元表”的 __index 连接父类:
local Account = {}
Account.__index = Account
function Account.new(owner, balance)
return setmetatable({
owner = owner,
balance = balance or 0
}, Account)
end
function Account:withdraw(amount)
if amount > self.balance then
return nil, "insufficient balance"
end
self.balance = self.balance - amount
return self.balance
end
local SavingsAccount = setmetatable({}, {
__index = Account
})
SavingsAccount.__index = SavingsAccount
function SavingsAccount.new(owner, balance, minimum)
local self = Account.new(owner, balance)
self.minimum = minimum or 0
return setmetatable(self, SavingsAccount)
end
function SavingsAccount:withdraw(amount)
if self.balance - amount < self.minimum then
return nil, "minimum balance required"
end
return Account.withdraw(self, amount)
end
SavingsAccount 的元表用于查找父类方法,SavingsAccount.__index 用于查找子类实例方法。调用父类方法时使用 Account.withdraw(self, amount),显式传入 self。
多态
local function withdrawAll(accountValue, amount)
return accountValue:withdraw(amount)
end
local standard = Account.new("Ada", 100)
local savings = SavingsAccount.new("Bob", 100, 50)
print(withdrawAll(standard, 80))
print(withdrawAll(savings, 80))
调用者只依赖 withdraw 协议,不必知道具体类。继承适合稳定的“是一个”关系;仅为了复用几个函数时,组合通常更简单。
封装与访问控制
Lua 表字段默认公开。常见封装方式有三种:
闭包封装
local function createAccount(initial)
local balance = initial
return {
getBalance = function()
return balance
end,
withdraw = function(amount)
if amount > balance then
return nil, "insufficient balance"
end
balance = balance - amount
return balance
end
}
end
闭包真正隐藏了状态,但每个对象都复制方法,也无法直接访问原型链。
弱约定
以下划线开头表示内部字段:
local account = {
owner = "Ada",
_balance = 100
}
这是最常见的 Lua 工程约定,依赖团队纪律而不是语言强制。
代理访问
用元表拦截字段读写,支持 getter、setter 和校验,但会增加间接层,调试时要查看真实数据所在位置。
设计取舍
在以下场景使用面向对象写法:
-
对象有明确状态和生命周期。
-
同一接口存在多个实现。
-
需要复用稳定的行为层级。
在以下场景优先使用普通表和函数:
-
数据只是配置或传输结构。
-
函数没有共享状态。
-
继承层级会很浅或根本不需要。
Lua 的优势是组合成本低。不要为了模拟其他语言而建立庞大的类层级。