Lua 面向对象编程完全指南:从元表到私密性,解锁灵活封装技巧
Lua 作为一门轻量级脚本语言,并未原生提供 class、extends 等面向对象(OOP)关键字,但凭借 table(表) 和 metatable(元表) 两大核心机制,依然能灵活模拟 OOP 的三大特性:封装、继承、多态。更令人惊喜的是,通过 Lua 的闭包(closure)特性,还能实现严格的私密性控制,避免内部状态泄露。
本文将结合《Lua 程序设计(第 2 版)》第 16 章核心内容,从基础的 “table 即对象” 讲起,逐步深入继承、多重继承、私密性控制,并解答 “私密性与元表是否冲突”“公有变量如何暴露” 等实操问题,全程附完整可运行代码,帮助你彻底掌握 Lua 式 OOP。
一、Lua OOP 基石:table 即对象
在 Lua 中,table 就是对象,这一结论源于 table 的三大特性,与传统 OOP 中的 “对象” 定义完全契合:
- 状态(State):table 可存储键值对(如
balance = 0),对应对象的属性; - 标识(Identity):两个值完全相同的 table 是独立对象(
a={}和b={}永远不等); - 生命周期(Lifecycle):table 的存在不依赖创建环境,只要有引用就不会被垃圾回收。
而 OOP 中的 “方法”,本质是存储在 table 字段中的函数。
1.1 初始实现:直接绑定 table 的方法
先定义一个 “普通账户” 对象,包含余额属性和取款方法:
-- 定义账户对象(初始余额 0)
Account = { balance = 0 }-- 定义取款方法:直接操作 Account 的 balance
function Account.withdraw(v)Account.balance = Account.balance - v
end-- 调用方法:取款 100
Account.withdraw(100)
print(Account.balance) --> -100
缺陷:方法与 table 强绑定
若将 Account 赋值给其他变量后清空,方法会失效(因方法内硬编码了 Account):
a = Account -- a 指向原账户对象
Account = nil -- 清空原 Account 变量
a.withdraw(50) --> 报错:attempt to perform arithmetic on a nil value
1.2 关键改进:引入 self 参数(类似 this)
为方法添加 self 参数,代表 “调用方法的对象”,让方法与具体 table 解耦:
Account = { balance = 0 }-- 方法定义:self 指代调用者
function Account.withdraw(self, v)self.balance = self.balance - v
end-- 显式传入 self 调用
a = Account
Account = nil
a.withdraw(a, 50) -- 把 a 作为 self 传入
print(a.balance) --> -50
1.3 语法糖:冒号(:)简化 self 传递
Lua 提供冒号语法,可自动处理 self 的声明和传递,避免冗余代码:
- 定义方法时:
function Account:withdraw(v)等价于function Account.withdraw(self, v); - 调用方法时:
a:withdraw(50)等价于a.withdraw(a, 50)。
优化后的完整代码:
Account = { balance = 0 }-- 冒号定义方法(自动包含 self)
function Account:deposit(v)self.balance = self.balance + v
endfunction Account:withdraw(v)if v > self.balance then error("余额不足") endself.balance = self.balance - v
end-- 冒号调用方法(自动传入 self)
a = Account:new() -- 后续会实现 new 构造函数
a:deposit(300)
a:withdraw(100)
print(a.balance) --> 200
二、类的实现:原型继承与元表
“类” 在 Lua 中是 原型对象—— 当实例找不到字段(属性 / 方法)时,通过元表的 __index 元方法,从原型对象中查找。这种 “原型链” 机制,是 Lua 实现继承的核心。
2.1 构造函数 new:创建实例并绑定元表
构造函数 new 的核心作用是:创建实例、绑定元表、配置继承规则。完整实现:
-- 定义账户类(原型对象)
Account = { balance = 0 }-- 构造函数:创建 Account 实例
function Account:new(o)o = o or {} -- 若用户未传入 table,创建空 tablesetmetatable(o, self) -- 实例的元表设为 Account(原型)self.__index = self -- 关键:实例找不到字段时,从原型中查找return o
end-- 类的通用方法(所有实例可继承)
function Account:deposit(v)self.balance = self.balance + v
endfunction Account:withdraw(v)if v > self.balance then error("余额不足") endself.balance = self.balance - v
end-- 创建实例并使用
a1 = Account:new({ balance = 100 }) -- 传入初始余额
a1:withdraw(50)
print(a1.balance) --> 50a2 = Account:new() -- 未传入,继承原型的 balance = 0
a2:deposit(200)
print(a2.balance) --> 200
核心逻辑:元表的 __index 作用
当调用 a1:withdraw(50) 时,Lua 的查找流程是:
- 检查
a1自身是否有withdraw方法 → 无; - 查看
a1的元表(Account)是否有__index→ 有,且指向 Account; - 从 Account 中查找
withdraw方法 → 执行。
2.2 继承:子类扩展父类
Lua 的继承本质是 “子类作为父类的实例,同时自身作为新原型”。以 “支持透支的特殊账户” 为例,演示子类如何继承并扩展父类:
lua
-- 1. 复用上面的 Account 父类代码(原型对象+方法)-- 2. 创建子类 SpecialAccount:让子类成为 Account 的实例
SpecialAccount = Account:new()-- 3. 重写父类方法(支持透支)
function SpecialAccount:withdraw(v)local limit = self.limit or 0 -- 透支额度,默认 0if v > self.balance + limit then error("超出透支额度") endself.balance = self.balance - v
end-- 4. 子类新增方法(父类无此功能)
function SpecialAccount:setLimit(v)self.limit = v
end-- 子类实例的使用
s = SpecialAccount:new({ balance = 200 })
s:setLimit(100) -- 设置透支额度 100
s:withdraw(250) -- 200-250=-50,未超额度
print(s.balance) --> -50
2.3 多重继承:继承多个父类
Lua 支持多重继承,核心思路是将 __index 设为函数,而非单一原型 —— 当实例查找字段时,函数会依次在多个父类中搜索。
步骤 1:实现多重继承工具函数
-- 辅助函数:在父类列表中查找字段 k
local function search(k, parents)for i = 1, #parents dolocal v = parents[i][k]if v then return v endend
end-- 工厂函数:创建支持多重继承的子类
function createClass(...)local c = {} -- 新子类local parents = {...} -- 父类列表-- 元表:__index 函数负责在父类中查找字段setmetatable(c, {__index = function(t, k)return search(k, parents)end})c.__index = c -- 实例的 __index 指向子类-- 子类构造函数function c:new(o)o = o or {}setmetatable(o, c)return oendreturn c
end
步骤 2:定义多个父类并创建子类
假设需要继承 “账户功能” 和 “名称管理功能”:
-- 父类 1:Account(账户功能,复用之前代码)
Account = { balance = 0 }
function Account:new(o) o = o or {}; setmetatable(o, self); self.__index = self; return o end
function Account:deposit(v) self.balance = self.balance + v end
function Account:withdraw(v) if v > self.balance then error("余额不足") end self.balance = self.balance - v end-- 父类 2:Named(名称管理功能)
Named = {}
function Named:new(o) o = o or {}; setmetatable(o, self); self.__index = self; return o end
function Named:setName(n) self.name = n end
function Named:getName() return self.name end-- 创建多重继承子类:同时继承 Account 和 Named
NamedAccount = createClass(Account, Named)-- 子类实例使用
na = NamedAccount:new({ balance = 500, name = "Alice" })
na:deposit(300) -- 继承 Account 的 deposit
na:setName("Bob") -- 继承 Named 的 setName
print(na:getName(), na.balance) --> Bob 800
三、核心疑问解答:私密性与元表的冲突、公有变量的暴露
在实际开发中,我们常面临两个关键问题:如何实现严格的私密性(隐藏内部状态)?私密性与元表继承是否冲突?公有变量该如何安全暴露?
3.1 私密性控制:闭包实现隐藏状态
Lua 的 table 默认无 “私有字段”,所有字段均可外部访问。但通过 闭包(closure),可将内部状态存储在工厂函数的局部变量中,仅暴露必要的公有方法,实现严格私密性。
完整实现:带私密性的账户
-- 工厂函数:创建带私密性的账户,返回公有接口
function newAccount(initialBalance, ownerName)-- 私有状态:局部变量,外部完全无法访问local self = {balance = initialBalance, -- 私有属性:余额secret = "123456" -- 私有属性:密码}-- 私有方法:仅内部调用,未暴露local checkSecret = function(input)return input == self.secretend-- 公有变量:需要外部访问,后续放入接口 tablelocal owner = ownerName -- 公有属性:开户人姓名local publicInfo = "这是公开信息" -- 公有属性:通用信息-- 公有方法 1:存款local deposit = function(v)self.balance = self.balance + vend-- 公有方法 2:取款(需验证密码)local withdraw = function(v, inputSecret)if not checkSecret(inputSecret) then error("密码错误") endif v > self.balance then error("余额不足") endself.balance = self.balance - vend-- 公有方法 3:查询余额(需验证密码)local getBalance = function(inputSecret)if not checkSecret(inputSecret) then error("密码错误") endreturn self.balanceend-- 关键:返回接口 table,暴露公有变量和方法return {-- 暴露公有方法deposit = deposit,withdraw = withdraw,getBalance = getBalance,-- 暴露公有变量owner = owner,publicInfo = publicInfo}
end-- 调用示例
acc = newAccount(1000, "张三")-- 访问公有变量
print(acc.owner) --> 张三
print(acc.publicInfo) --> 这是公开信息-- 调用公有方法
acc.deposit(500)
acc.withdraw(300, "123456")
print(acc.getBalance("123456")) --> 1200-- 尝试访问私有状态:失败
print(acc.balance) --> nil
print(acc.secret) --> nil
acc.checkSecret("123456") --> 报错:attempt to call a nil value
3.2 关键结论 1:私密性与元表通常二选一
为什么上面的私密性实现没有用元表?因为元表的 __index 会 “穿透” 私有隔离:
反例:用元表会泄露私有状态
-- 错误示范:给接口 table 设置元表,导致私有状态泄露
function newAccount(initialBalance)local self = { balance = initialBalance, secret = "123456" } -- 私有状态local deposit = function(v) self.balance = self.balance + v end-- 错误操作:让接口 table 继承 selflocal interface = { deposit = deposit }setmetatable(interface, { __index = self })return interface
endacc = newAccount(1000)
print(acc.balance) --> 1000(私有状态被访问到)
原因分析
元表的 __index 逻辑是:实例找不到字段时,自动去 __index 指向的 table 中查找。若 __index 指向存储私有状态的 self,就等于间接暴露了所有私有字段,完全打破私密性。
因此:
- 若需要 严格私密性:优先用 “局部变量 + 接口 table”,不依赖元表;
- 若需要 继承 / 方法复用:用元表,但此时通常不强调 “严格私密性”(继承本身需要共享字段)。
3.3 关键结论 2:公有变量通过接口 table 暴露
无论是公有方法还是公有变量,都需要 显式放到 return 的接口 table 中,外部仅能通过这个 table 访问,无法直接触碰内部局部变量。
暴露规则:
- 公有变量直接作为接口 table 的键值对(如
owner = owner); - 若公有变量是可变类型(如 table),可返回只读副本,避免外部修改内部状态。
3.4 折中方案:既想私密性,又想复用方法?
若需同时满足 “私密性” 和 “方法复用”,可将共享方法放到独立原型中,通过 “显式引用” 而非 “元表继承” 复用:
-- 共享方法原型:存储无状态的通用工具
local SharedProto = {checkPhone = function(phone)-- 验证手机号格式的通用方法return string.match(phone, "^1[3-9]%d%d%d%d%d%d%d%d%d%d$") ~= nilend
}-- 带私密性的工厂函数,复用共享方法
function newAccount(initialBalance, phone)local self = { balance = initialBalance, secret = "123456" } -- 私有状态local deposit = function(v) self.balance = self.balance + v end-- 接口 table:显式引用共享方法local interface = {deposit = deposit,getBalance = function(inputSecret)if inputSecret ~= self.secret then error("密码错误") endreturn self.balanceend,phone = phone,checkPhone = SharedProto.checkPhone -- 复用共享方法}return interface
end-- 调用示例
acc = newAccount(1000, "13800138000")
print(acc.checkPhone(acc.phone)) --> true(复用共享方法)
print(acc.balance) --> nil(私密性保留)最后来个综合的例子来理解一下
-- 辅助函数:在父类列表paterns中查找字段k,找到返回
local function search(k, parents)for i = 1, #parents dolocal v = parents[i][k]if v thenreturn vendend
end-- 工厂函数:创建支持多重继承的子类,参数为多个父类
function createClass(...)local c = {}local parents = {...}setmetatable(c, {__index = function(t, k)return search(k, parents)end})c.__index = cfunction c:new(o)o = o or {}setmetatable(o, c)return oendreturn c
end-- 父类1:角色基础属性
local Character = {name = "未知角色",hp = 100,showBaseInfo = function(self)print(string.format("角色:%s,血量: %d ", self.name, self.hp))end
}-- 父类2:战斗相关技能
local Warrior = {attackPower = 20,attack = function (self, target)print(string.format("%s 对 %s 发动物理攻击,造成 %d 点伤害!", self.name, target.name, self.attackPower))target.hp = target.hp - self.attackPowerend
}-- 父类3:魔法相关能力
local Mage = {mp = 80,magicAttack = function(self, target)if self.mp >= 10 thenprint(string.format("%s 对 %s 释放了火球术,造成了 %d 的伤害", self.name, target.name, 30))self.mp = self.mp - 10target.hp = target.hp - 30elseprint(string.format("%s 魔法值不足,无法释放技能!", self.name))endend
}-- 创建子类
local BattleMage = createClass(Character, Warrior, Mage)BattleMage.defense = 15
function BattleMage:defend()print(string.format("%s 开启防御,减免 50% 伤害!", self.name))
end-- 重写
function BattleMage:showBaseInfo()print(string.format("战斗法师:%s,血量:%d,魔法值:%d,防御: %d", self.name, self.hp, self.mp, self.defense))
endlocal mage1 = BattleMage:new()
print("===实例1:默认状态===")
-- 修正:先给 mage1 赋值 name,再调用方法
mage1.name = "小火龙"
mage1:showBaseInfo() -- 此时 self.name 是 "小火龙",不再是父类的默认值local mage2 = BattleMage:new({name = "冰公主",hp = 120, -- 自定义血量(覆盖父类默认100)attackPower = 25 -- 自定义攻击力(覆盖父类默认20)
})
print("\n=== 实例2(自定义属性)初始状态 ===")
mage2:showBaseInfo()-- 3. 调用继承的父类方法(物理攻击+魔法攻击)
print("\n=== 战斗过程 ===")
mage1:attack(mage2) -- 继承 Warrior 的 attack 方法
mage2:magicAttack(mage1) -- 继承 Mage 的 magicAttack 方法-- 4. 调用子类独有的方法(此时 mage1.name 已存在,不会 nil)
mage1:defend() -- 调用 BattleMage 独有的 defend 方法-- 5. 查看攻击后的数据
print("\n=== 攻击后状态 ===")
mage1:showBaseInfo()
mage2:showBaseInfo()
===实例1:默认状态===
战斗法师:小火龙,血量:100,魔法值:80,防御: 15=== 实例2(自定义属性)初始状态 ===
战斗法师:冰公主,血量:120,魔法值:80,防御: 15=== 战斗过程 ===
小火龙 对 冰公主 发动物理攻击,造成 20 点伤害!
冰公主 对 小火龙 释放了火球术,造成了 30 的伤害
小火龙 开启防御,减免 50% 伤害!=== 攻击后状态 ===
战斗法师:小火龙,血量:70,魔法值:80,防御: 15
战斗法师:冰公主,血量:100,魔法值:70,防御: 15