Table 与 Metatable
在 Lua 中,Table 就是对象。它可以存储数据(属性),也可以存储函数(方法)。
但仅有 Table 是不够的,我们需要 Metatable(元表) 来实现"继承"和"方法查找"。
- 元表:就像是一个对象的"原型"或"父类"。
__index元方法:这是 Lua OOP 的灵魂。当你访问一个表中不存在的键(比如调用一个方法)时,Lua 会去查找它的元表。如果元表里有这个方法,就执行它。
🛠️ 1. 类的实现(封装)
在 Lua 中,我们通常把一个 Table 当作"类",并通过 new 函数来实例化对象。
关键步骤:
- 定义一个表作为类。
- 设置
self.__index = self,这样实例找不到方法时能找到类。 - 编写构造函数
new,使用setmetatable将实例与类关联。
代码示例:
-- 1. 定义类
Person = { name = "Unknown", age = 0 }
Person.__index = Person -- 关键:让实例在找不到方法时回退查找 Person
-- 2. 构造函数
function Person:new(name, age)
local instance = {} -- 创建新对象
instance.name = name
instance.age = age
setmetatable(instance, self) -- 设置元表,让 instance 继承 Person 的方法
return instance
end
-- 3. 定义方法 (注意使用冒号语法)
function Person:sayHello()
print("你好,我是 " .. self.name .. ",今年 " .. self.age .. " 岁。")
end
-- 4. 实例化与调用
local p1 = Person:new("Alice", 30)
p1:sayHello() -- 输出: 你好,我是 Alice,今年 30 岁。
🧬 2. 继承的实现
Lua 的继承也是基于元表。子类需要将自己的元表设置为父类,或者设置一个 __index 指向父类。
实现逻辑:
-
创建一个新表作为子类。
-
设置子类的元表为父类(
setmetatable(Child, {__index = Parent}))。 -
重写
new方法,确保实例的元表指向子类自己。-- 继承自 Person
Student = {}
-- 设置 Student 的元表,使其能访问 Person 的方法(实现继承)
setmetatable(Student, { __index = Person })
Student.__index = Student -- 让 Student 的实例能访问 Student 的方法-- 子类构造函数
function Student:new(name, age, school)
-- 调用父类构造函数创建一个基础对象
local instance = Person:new(name, age)
-- 重新设置元表为 Student(这一步很重要,否则找不到 Student 的特有方法)
setmetatable(instance, Student)
instance.school = school
return instance
end-- 子类特有方法
function Student:study()
print(self.name .. " 在 " .. self.school .. " 学习。")
endlocal s1 = Student:new("Bob", 20, "清华大学")
s1:sayHello() -- 继承自父类的方法
s1:study() -- 子类特有方法
🎯 3. 语法糖:冒号 (:) 与点号 (.)
你可能注意到了代码中混用了 : 和 .,这在 Lua OOP 中非常重要:
表格
| 语法 | 含义 | 自动传参 | 适用场景 |
|---|---|---|---|
冒号 : |
obj:func(arg) |
自动 传递 self |
实例方法调用、定义 |
点号 . |
obj.func(obj, arg) |
不 自动传递 self |
静态方法、工具函数、或显式传参 |
- 定义时 :
function Person:say()等价于function Person.say(self) - 调用时 :
p1:say()等价于p1.say(p1)
🔒 4. 私有性与封装
Lua 的 Table 默认是公开的,任何人都可以访问内部数据。要实现"私有变量"(如 private balance),通常利用 闭包(Closure) 或 局部变量 来实现。
方法:利用闭包隐藏数据
我们不把数据放在 self 里,而是放在函数的局部变量中,只暴露操作接口。
function createAccount(initialBalance)
-- 私有变量(在闭包作用域内,外部无法直接访问)
local balance = initialBalance or 0
-- 返回公共接口
return {
deposit = function(amount)
balance = balance + amount
print("存款成功,余额: " .. balance)
end,
withdraw = function(amount)
if amount <= balance then
balance = balance - amount
print("取款成功,余额: " .. balance)
else
print("余额不足")
end
end
}
end
local acc = createAccount(1000)
acc.deposit(500)
-- print(acc.balance) -- 报错或返回 nil,因为 balance 是私有的
__index 作为函数:自定义"读"逻辑
当 __index 是函数时,每次访问表中不存在的键,都会调用这个函数。
函数签名
function __index_func(table, key)
-- table: 你正在访问的那个表(比如 t)
-- key: 你试图访问的键(比如 t.name 中的 "name")
return value -- 返回的值就是 t.name 的结果
end
执行流程
- 代码请求
t.key。 - 发现
t里没有key。 - 发现元表里有
__index函数。 - 调用函数 :
__index_func(t, "key")。 - 函数返回什么,
t.key就等于什么。
实战:实现"只读"或"默认值"
local t = { name = "Alice" }
local mt = {
__index = function(table, key)
print("警告:你在读取一个不存在的字段 ->", key)
return "默认值"
end
}
setmetatable(t, mt)
print(t.name) -- Alice (直接存在,不触发 __index)
print(t.money)
-- 1. t 没有 money
-- 2. 打印 "警告..."
-- 3. 输出 "默认值"
__newindex 作为函数:自定义"写"逻辑
当 __newindex 是函数时,每次给表中不存在的键赋值,都会调用这个函数。注意:此时赋值操作被拦截了,原表不会自动发生变化。
函数签名
function __newindex_func(table, key, value)
-- table: 你正在赋值的那个表
-- key: 键
-- value: 你想赋的值
-- 在这里你需要手动决定怎么处理这个 value
end
执行流程
- 代码执行
t.key = 100。 - 发现
t里没有key。 - 发现元表里有
__newindex函数。 - 调用函数 :
__newindex_func(t, "key", 100)。 - 关键点 :如果函数内部什么都不写,
t.key永远不会被赋值 。你需要手动用rawset来赋值。
实战:数据校验与私有变量
local t = {}
local _private_store = {} -- 用一个隐藏表存真实数据
local mt = {
__newindex = function(table, key, value)
if key == "age" then
if type(value) == "number" and value > 0 then
-- 校验通过,手动存入隐藏表
_private_store[key] = value
print("年龄设置成功")
else
error("年龄必须是正数!")
end
else
-- 其他字段直接存入隐藏表
_private_store[key] = value
end
end,
__index = _private_store -- 读取时也去隐藏表读
}
setmetatable(t, mt)
t.age = 20 -- 输出: 年龄设置成功
-- t.age = -5 -- 报错: 年龄必须是正数!
print(t.age) -- 20 (通过 __index 从 _private_store 读到)
📌 总结
__index函数 :像一个兜底客服。你问它要东西,它没有,但它可以根据你的问题现场算一个给你,或者给你个默认回复。__newindex函数 :像一个安检员 。你想把东西带进去(赋值),它拦下来检查。如果你不手动把东西放进去(rawset或存入其他表),东西就会被丢弃。