什么是元表
元表(metatable)是 Lua 中一种强大的机制,允许你改变表在运行时的行为。每个表都可以关联一个元表,当对该表执行特定操作(如算术运算、索引访问等)时,Lua 会检查元表中是否定义了对应的元方法(metamethod),如果存在则调用它。
local t = {}
local mt = {}
setmetatable(t, mt)
print(getmetatable(t) == mt) -- true
每个表可以通过 setmetatable 函数设置元表,通过 getmetatable 函数获取元表。元表本身也是一个普通的 Lua 表,其特殊之处在于键名以双下划线 __ 开头。
算术元方法
Lua 提供了完整的算术元方法,当对表执行算术运算而表本身不支持该运算时,Lua 会查找对应的元方法:
| 元方法 | 对应操作 | 说明 |
|---|---|---|
__add | + | 加法 |
__sub | - | 减法 |
__mul | * | 乘法 |
__div | / | 除法 |
__mod | % | 取模 |
__pow | ^ | 幂运算 |
__unm | - (一元) | 取负 |
__idiv | // | 整除 |
-- 向量加法示例
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({x = x, y = y}, Vector)
end
function Vector:__add(other)
return Vector.new(self.x + other.x, self.y + other.y)
end
function Vector:__sub(other)
return Vector.new(self.x - other.x, self.y - other.y)
end
function Vector:__mul(scalar)
return Vector.new(self.x * scalar, self.y * scalar)
end
function Vector:__tostring()
return string.format("Vector(%.2f, %.2f)", self.x, self.y)
end
local v1 = Vector.new(1, 2)
local v2 = Vector.new(3, 4)
print(v1 + v2) -- Vector(4.00, 6.00)
print(v1 - v2) -- Vector(-2.00, -2.00)
print(v1 * 3) -- Vector(3.00, 6.00)
关系元方法
关系元方法用于重载比较运算符:
| 元方法 | 对应操作 | 说明 |
|---|---|---|
__eq | == | 相等 |
__lt | < | 小于 |
__le | <= | 小于等于 |
function Vector:__eq(other)
return self.x == other.x and self.y == other.y
end
function Vector:__lt(other)
-- 按向量长度比较
return (self.x^2 + self.y^2) < (other.x^2 + other.y^2)
end
function Vector:__le(other)
return (self.x^2 + self.y^2) <= (other.x^2 + other.y^2)
end
local a = Vector.new(1, 1)
local b = Vector.new(2, 2)
print(a < b) -- true
print(a == Vector.new(1, 1)) -- true
注意 Lua 不提供 __gt 和 __ge 元方法,Lua 会自动将 a > b 转换为 b < a,将 a >= b 转换为 b <= a。
索引元方法
索引元方法是 Lua 中最常用的元方法,是实现面向对象编程和模块系统的基础。
__index 元方法
当访问表中不存在的键时,Lua 会查找 __index 元方法:
local proxy = {}
local data = {name = "Lua", version = "5.4"}
setmetatable(proxy, {
__index = function(t, key)
print("访问键: " .. key)
return data[key]
end
})
print(proxy.name) -- 访问键: name → Lua
print(proxy.version) -- 访问键: version → 5.4
print(proxy.missing) -- 访问键: missing → nil
__index 也可以是一个表,此时 Lua 直接在该表中查找:
local defaults = {color = "white", size = 10}
local widget = setmetatable({}, {__index = defaults})
print(widget.color) -- white
print(widget.size) -- 10
__newindex 元方法
当对表中不存在的键赋值时,Lua 会查找 __newindex 元方法:
local readonly = {}
local data = {x = 1, y = 2}
setmetatable(readonly, {
__index = data,
__newindex = function(t, key, value)
error("尝试修改只读表: " .. key, 2)
end
})
print(readonly.x) -- 1
readonly.z = 3 -- 报错: 尝试修改只读表: z
调用元方法
__call 元方法使表可以像函数一样被调用,这在实现工厂模式和函数对象时非常有用:
local Counter = {}
Counter.__index = Counter
function Counter.new(start)
local self = setmetatable({count = start or 0}, Counter)
return self
end
function Counter:__call()
self.count = self.count + 1
return self.count
end
local c = Counter.new(10)
print(c()) -- 11
print(c()) -- 12
print(c()) -- 13
类型转换元方法
__tostring 元方法控制表在字符串化时的行为:
local Point = {}
Point.__index = Point
function Point.new(x, y)
return setmetatable({x = x, y = y}, Point)
end
function Point:__tostring()
return string.format("(%g, %g)", self.x, self.y)
end
function Point:__concat(other)
return tostring(self) .. " → " .. tostring(other)
end
local p1 = Point.new(0, 0)
local p2 = Point.new(3, 4)
print(p1) -- (0, 0)
print(p1 .. p2) -- (0, 0) → (3, 4)
用元表实现继承
元表是实现类继承的核心工具。下面展示一个完整的单继承实现:
-- 基类创建函数
local function class(base)
local cls = {}
cls.__index = cls
if base then
setmetatable(cls, {__index = base})
cls.__base = base
end
function cls:new(...)
local instance = setmetatable({}, self)
if instance.__init then
instance:__init(...)
end
return instance
end
return cls
end
-- 定义基类 Animal
local Animal = class()
function Animal:__init(name, sound)
self.name = name
self.sound = sound
end
function Animal:speak()
return self.name .. " says " .. self.sound
end
-- 定义子类 Dog
local Dog = class(Animal)
function Dog:__init(name)
Animal.__init(self, name, "Woof")
self.tricks = {}
end
function Dog:learn_trick(trick)
table.insert(self.tricks, trick)
end
function Dog:show_tricks()
return self.name .. " knows: " .. table.concat(self.tricks, ", ")
end
local dog = Dog:new("Rex")
print(dog:speak()) -- Rex says Woof
dog:learn_trick("sit")
dog:learn_trick("shake")
print(dog:show_tricks()) -- Rex knows: sit, shake
__gc 元方法:终结器
__gc 元方法在表被垃圾回收时调用,类似于其他语言的析构函数:
local Resource = {}
Resource.__index = Resource
function Resource.new(name)
local self = setmetatable({
name = name,
handle = io.tmpfile()
}, Resource)
return self
end
function Resource:close()
if self.handle then
self.handle:close()
self.handle = nil
print(self.name .. " 已释放")
end
end
function Resource:__gc()
self:close()
end
do
local res = Resource.new("数据库连接")
-- 离开作用域后,垃圾回收时自动调用 __gc
end
collectgarbage() -- 数据库连接 已释放
实战:实现一个完整的 Set 类
综合使用多种元方法,实现一个功能完备的集合类:
local Set = {}
Set.__index = Set
function Set.new(items)
local self = setmetatable({items = {}}, Set)
if items then
for _, v in ipairs(items) do
self.items[v] = true
end
end
return self
end
-- 并集: +
function Set:__add(other)
local result = Set.new()
for k in pairs(self.items) do result.items[k] = true end
for k in pairs(other.items) do result.items[k] = true end
return result
end
-- 交集: *
function Set:__mul(other)
local result = Set.new()
for k in pairs(self.items) do
if other.items[k] then result.items[k] = true end
end
return result
end
-- 差集: -
function Set:__sub(other)
local result = Set.new()
for k in pairs(self.items) do
if not other.items[k] then result.items[k] = true end
end
return result
end
-- 相等: ==
function Set:__eq(other)
for k in pairs(self.items) do
if not other.items[k] then return false end
end
for k in pairs(other.items) do
if not self.items[k] then return false end
end
return true
end
-- 长度: #
function Set:__len()
local count = 0
for _ in pairs(self.items) do count = count + 1 end
return count
end
-- 字符串化
function Set:__tostring()
local items = {}
for k in pairs(self.items) do
items[#items + 1] = tostring(k)
end
table.sort(items)
return "{" .. table.concat(items, ", ") .. "}"
end
local a = Set.new({1, 2, 3, 4})
local b = Set.new({3, 4, 5, 6})
print(a + b) -- {1, 2, 3, 4, 5, 6}
print(a * b) -- {3, 4}
print(a - b) -- {1, 2}
print(#a) -- 4
print(a == Set.new({1, 2, 3, 4})) -- true
注意事项
使用元表时需要注意以下几点:
- 元方法的查找遵循原型链:如果元表中没有定义,会继续查找元表的元表
rawget和rawset函数可以绕过元方法,直接访问/修改表- 过多的元方法调用会带来性能开销,在高频调用场景中需要权衡
- Lua 5.2+ 引入了
__pairs和__ipairs元方法,可以自定义迭代行为 - 元表不能防止
table.insert、table.remove等标准库函数绕过元方法
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。