Lua中,表、元表、对象、类的解析
原文地址:Lua中,表、元表、对象、类的解析
欢迎参观我的网站:无敌牛 – 技术/著作/典籍/分享等
一、表(table)—— Lua 的唯一复合数据结构
1.1 本质
- Lua 中唯一的内置复合数据结构
- 可以同时作为:
- 数组(array)
- 字典(map / hash / associative array)
- 对象(object)
- 模块(module)
- 结构体(struct)
- 命名空间(namespace)
1.2 基本语法
local t = {name = "Alice", -- 字符串键age = 25, -- 字符串键,等价于 ["age"] = 25[true] = "yes", -- 任意类型键(除 nil)[2] = "first", -- 数字键,插入到第2个位置(索引从1开始),此位置向上至2位置的原有数据,移动到最末尾sayHello = function(self) -- 方法print("Hello, I'm " .. self.name)end
}
1.3 操作方式
t.name = "Bob" -- 设置字段
print(t.age) -- 读取字段
t:sayHello() -- 调用方法(: 自动传 self)
t["sayHello"](t) -- 等价写法for k, v in pairs(t) do -- 遍历print(k, v)
end
1.4 表的核心作用
存储数据 + 行为(方法) → 成为“对象”的基础
二、元表(metatable)—— 表的“行为说明书”
2.1 本质
- 元表本身也是一个表
- 它包含“元方法(metamethods)”,用于定义“当对原表执行某些操作时,应该怎么做”
2.2 如何设置/获取
setmetatable(table, metatable) -- 设置
getmetatable(table) -- 获取
只有 table、userdata、thread 可以设元表(Lua 5.4+ 支持更多类型,但一般只用于 table)
2.3 常用元方法
元方法 | 触发时机 | 用途 |
---|---|---|
__index | t[key] 且 key 不存在 | 默认值、继承、代理 |
__newindex | t[key] = value 且 key 不存在 | 权限控制、日志、只读表 |
__add | a + b | 自定义加法 |
__call | f(...) | 让表像函数一样被调用 |
__tostring | print(t) 或 tostring(t) | 自定义字符串表示 |
__len | #t | 自定义长度 |
__eq , __lt , __le | == , < , <= | 自定义比较 |
2.4 示例:__index
实现默认值
local defaults = { x = 0, y = 0, color = "white" }
local obj = {}
setmetatable(obj, { __index = defaults })print(obj.x) -- 0 (从 defaults 获取)
obj.x = 10
print(obj.x) -- 10 (现在 obj 自己有 x)
2.5 示例:__call
实现可调用对象
local counter = { count = 0 }
local mt = {__call = function(self, n)self.count = self.count + (n or 1)return self.countend
}
setmetatable(counter, mt)print(counter()) -- 1
print(counter(5)) -- 6
2.6 元表的核心作用
控制表的行为 —— 运算符重载、属性代理、函数调用、继承机制等
三、对象(object)—— 用表模拟的对象
3.1 什么是对象?
在 Lua 中,对象 = 表 + 方法 +(可选)元表
local obj = {x = 10,y = 20,move = function(self, dx, dy)self.x = self.x + dxself.y = self.y + dyend
}obj:move(5, 5) -- : 语法糖,自动传 self
print(obj.x, obj.y) -- 15 25
3.2 对象的核心特征
- 状态(数据):
x
,y
- 行为(方法):
move
- 标识(identity):每个表是独立对象
3.3 如何“构造”对象?
方式 1:直接字面量
local obj = { x = 10, y = 20 }
方式 2:工厂函数
local function newPoint(x, y)return {x = x or 0,y = y or 0,move = function(self, dx, dy)self.x = self.x + dxself.y = self.y + dyend}
endlocal p = newPoint(10, 20)
方式 3:用元表实现“类式”构造(见下文)
四、类(class)—— 用表 + 元表模拟的“类”
Lua 没有内置类,但可以用“原型继承”模拟:
4.1 基本“类”结构
-- “类”定义(其实是原型对象)
local Point = {}-- 定义 __tostring 行为(注意:这是给“实例”用的,不是给类本身)
local mt = {__tostring = function(self)return string.format("Point(%d, %d)", self.x, self.y)end
}-- 构造函数
function Point:new(x, y)local obj = {x = x or 0,y = y or 0}-- 关键:让实例(obj表)的元表要包含 __tostring 和 __indexsetmetatable(obj, {__index = self, -- 初始化 __index 为 Point (继承Point的成员)。__tostring = mt.__tostring -- 直接设置元方法})return obj
end-- 方法
function Point:move(dx, dy)self.x = self.x + dxself.y = self.y + dy
end-- 可选:让类本身(Point表)也支持 tostring(调试用)
setmetatable(Point, {__tostring = function()return "Class Point"end,__call = Point.new -- 支持 Point(10, 20) 语法
})
4.2 使用“类”
-- 测试
local p1 = Point:new(10, 20)
print(p1) -- Point(10, 20)
p1:move(5, 5)
print(p1) -- Point(15, 25)local p2 = Point(30, 40) -- 使用 __call
print(p2) -- Point(30, 40)print(Point) -- Class Point (可选功能)
4.3 继承怎么实现?
-- 子类
local ColorPoint = Point:new() -- 创建子类原型
ColorPoint.color = "red"-- 设置 ColorPoint ,使用 __index 继承 Point
setmetatable(ColorPoint, { __index = Point })-- 定义 __instance_mt
ColorPoint.__instance_mt = {__index = function(self, key)-- 首先在 ColorPoint 中查找,把所有 父类 Point 的函数全部继承过来。local value = ColorPoint[key]if value ~= nil thenreturn valueend-- 如果没找到,再到 Point 中查找return Point[key]end,__tostring = function(self)return string.format("ColorPoint(%d, %d, %s)", self.x, self.y, self.color)end
}-- 子类构造函数
function ColorPoint:new(x, y, color)local obj = Point:new(x, y) -- 调用父类构造函数obj.color = color or self.color-- 关键:重新设置元表为 ColorPoint 的实例元表setmetatable(obj, self.__instance_mt)return obj
end-- 可选:给 ColorPoint 类本身设置元表
setmetatable(ColorPoint, {__tostring = function() return "Class ColorPoint" end,__call = function(self, ...)return self:new(...)end -- 修正:使用匿名函数调用 self:new(...)
})-- 子类测试
local cp = ColorPoint(50, 60, "blue")
print(cp) -- ColorPoint(50, 60, blue) ← 终于正确了!
cp:move(10, 10)
print(cp) -- ColorPoint(60, 70, blue) ← move 继承自 Point!print(ColorPoint) -- Class ColorPoint
4.4 “类”的本质
一个“原型表” + 一个元表(用
__index
指向原型表)
- 对象创建时,把自己的元表的
__index
指向“类” - 当访问对象不存在的字段时,Lua 会去“类”里找 → 实现“方法继承”
五、使用规则 & 最佳实践
5.1 表的使用规则
- 用作数据容器、对象、模块
- 避免混用数组和字典部分(除非必要)
- 键尽量用字符串或数字(避免用布尔、函数、表作键,除非有特殊需求)
- 使用
:
定义和调用方法,自动管理self
5.2 元表的使用规则
- 只用于定义行为,不用于存储业务数据
- 多个对象可共享同一个元表(节省内存)
__index
最常用 —— 实现继承、默认值、代理__newindex
用于拦截写操作 —— 实现只读表、日志、验证__call
让对象/类可调用 —— 实现工厂、闭包__tostring
用于调试和日志 —— 强烈推荐为对象实现
5.3 对象的使用规则
- 状态(数据)存在对象自身
- 方法存在“类”或原型中(通过
__index
继承) - 使用
obj:method()
而不是obj.method(obj)
- 避免在对象中存储函数(除非是闭包或动态生成),以节省内存
5.4 类的使用规则
- “类”本身也是一个对象(原型)
- 构造函数通常叫
new
或create
- 使用
setmetatable(obj, { __index = self })
实现继承 - 方法定义在“类”上,对象通过
__index
继承 - 支持多层继承(
__index
链) - 可选:实现
__call
让类像函数一样被调用
5.5 性能注意事项
- 频繁触发
__index
/__newindex
有性能开销 → 避免在热路径使用 - 共享元表 → 节省内存
- 方法放在“类”中 → 避免每个对象复制函数
5.6 调试建议
- 使用
getmetatable(obj)
查看对象的元表 - 使用
debug.getmetatable
(如果元表被保护) - 为对象实现
__tostring
,方便 print 调试 - 避免过度使用元表 —— 会让代码行为“不直观”
六、关系图谱总结
+----------------------+
| 元表 |
| (metatable) |
| - __index → 类 | ← 定义“继承”行为
| - __call | ← 定义“可调用”行为
| - __tostring | ← 定义“打印”行为
+----------↑----------+|| setmetatable(obj, mt)|
+----------↓----------+
| 表 |
| (table) |
| - x = 10 | ← 数据(状态)
| - y = 20 |
| - move = function | ← 方法(行为)→ 通常放“类”里,通过 __index 继承
+----------↑----------+|| 是|
+----------↓----------+
| 对象 | ← 对象 = 表 + 元表
+----------↑----------+|| 从...创建|
+----------↓----------+
| 类 | ← 类 = 原型表 + 元表(通常 __index 指向自己)
| (其实是原型对象) |
+----------------------+
七、一句话总结
在 Lua 中:
- 表 是数据和方法的容器;
- 元表 是表的行为控制器;
- 对象 是一个表(带状态) + 可选元表(带行为);
- 类 是一个特殊的表(原型) + 元表(用
__index
实现继承),用于创建和管理对象。