08 - 元表与元方法 / Metatables & Metamethods
元表与元方法 / Metatables & Metamethods
元表(Metatable)是 Lua 最强大的机制之一。它允许你改变 table 的行为——运算符重载、默认值、只读保护、面向对象……全部通过元表实现。
Metatables are one of Lua’s most powerful mechanisms. They let you change table behavior — operator overloading, defaults, read-only protection, OOP — all through metatables.
🟢 基础 / Basics — 元表入门
1. 什么是元表? / What is a Metatable?
-- 每个 table 都可以关联一个"元表"
-- 元表定义了原始 table 在某些操作下的行为
local t = {}
local mt = {} -- 元表
setmetatable(t, mt) -- 将 mt 设为 t 的元表
print(getmetatable(t)) -- table: 0x...
print(getmetatable({})) -- nil(默认没有元表)
2. __index — 读取不存在的键
-- __index 在访问不存在的键时触发
local defaults = {color = "red", size = 10}
local mt = {__index = defaults}
local obj = setmetatable({}, mt)
print(obj.color) -- "red"(t.color 是 nil,触发 __index)
print(obj.size) -- 10
print(obj.weight) -- nil(__index 中也没有)
-- __index 也可以是函数
local mt2 = {
__index = function(t, key)
return "Key '" .. key .. "' not found"
end
}
local t2 = setmetatable({}, mt2)
print(t2.foo) -- "Key 'foo' not found"
3. __newindex — 写入不存在的键
-- __newindex 在给不存在的键赋值时触发
local t = {}
local mt = {
__newindex = function(t, key, value)
print("Setting " .. tostring(key) .. " = " .. tostring(value))
rawset(t, key, value) -- 必须用 rawset 避免无限递归
end
}
setmetatable(t, mt)
t.x = 10 -- Setting x = 10(触发 __newindex)
t.x = 20 -- 不触发!因为 x 已经存在了
-- 注意:__newindex 只在"新"键赋值时触发
-- 对已存在的键赋值,直接修改,不触发元方法
4. __tostring — 自定义字符串表示
local vector = {x = 3, y = 4}
local mt = {
__tostring = function(v)
return string.format("Vector(%g, %g)", v.x, v.y)
end
}
setmetatable(vector, mt)
print(vector) -- Vector(3, 4)
-- print 调用 tostring,tostring 调用 __tostring
-- 同样适用于 string.format
print(string.format("pos = %s", vector)) -- pos = Vector(3, 4)
5. 运算符重载 / Operator Overloading
local vec_mt = {}
function vec_mt.__add(a, b)
return setmetatable({x = a.x + b.x, y = a.y + b.y}, vec_mt)
end
function vec_mt.__sub(a, b)
return setmetatable({x = a.x - b.x, y = a.y - b.y}, vec_mt)
end
function vec_mt.__mul(a, b)
if type(b) == "number" then
return setmetatable({x = a.x * b, y = a.y * b}, vec_mt)
elseif type(a) == "number" then
return setmetatable({x = a * b.x, y = a * b.y}, vec_mt)
end
end
function vec_mt.__eq(a, b)
return a.x == b.x and a.y == b.y
end
function vec_mt.__lt(a, b)
return (a.x^2 + a.y^2) < (b.x^2 + b.y^2)
end
function vec_mt.__le(a, b)
return (a.x^2 + a.y^2) <= (b.x^2 + b.y^2)
end
function vec_mt.__tostring(v)
return string.format("(%g, %g)", v.x, v.y)
end
local function vec(x, y)
return setmetatable({x = x, y = y}, vec_mt)
end
local a = vec(1, 2)
local b = vec(3, 4)
print(a + b) -- (4, 6)
print(a - b) -- (-2, -2)
print(a * 3) -- (3, 6)
print(2 * b) -- (6, 8)
print(a == b) -- false
print(a < b) -- true
print(tostring(a)) -- (1, 2)
🟡 进阶 / Intermediate — 元表实用模式
1. 默认值表 / Default Value Table
-- 模式一:默认值为固定值
local function withDefault(t, default)
return setmetatable(t, {
__index = function(_, _) return default end
})
end
local scores = withDefault({}, 0)
scores["Alice"] = 95
print(scores["Alice"]) -- 95
print(scores["Bob"]) -- 0(默认值)
-- 模式二:默认值为表(每次返回新表)
local function autoTable(defaultFn)
local t = {}
return setmetatable(t, {
__index = function(self, key)
local value = defaultFn(key)
rawset(self, key, value)
return value
end
})
end
-- 自动为每个键创建空列表
local groups = autoTable(function() return {} end)
table.insert(groups["fruits"], "apple")
table.insert(groups["fruits"], "banana")
table.insert(groups["vegs"], "carrot")
-- groups = {fruits={"apple","banana"}, vegs={"carrot"}}
2. 只读表 / Read-Only Table
local function readOnly(t)
return setmetatable({}, {
__index = t,
__newindex = function(_, k, _)
error("Cannot modify read-only table: " .. tostring(k), 2)
end,
__pairs = function() return pairs(t) end,
__ipairs = function() return ipairs(t) end,
__len = function() return #t end,
__metatable = "locked", -- 阻止 getmetatable
})
end
local config = readOnly({host = "localhost", port = 8080})
print(config.host) -- "localhost"
-- config.port = 3000 -- Error: Cannot modify read-only table: port
-- getmetatable(config) -- "locked"
3. 代理表 / Proxy Pattern
-- 代理模式:用一个中间表控制对真实数据的访问
local function private(t)
local proxy = {}
setmetatable(proxy, {
__index = function(_, k)
print("[Access] " .. tostring(k))
return t[k]
end,
__newindex = function(_, k, v)
print("[Modify] " .. tostring(k) .. " = " .. tostring(v))
t[k] = v
end,
})
return proxy
end
local user = private({name = "Alice", age = 30})
print(user.name) -- [Access] name → Alice
user.age = 31 -- [Modify] age = 31
4. __call — 让表可调用
-- __call 让 table 可以像函数一样被调用
local mt = {
__call = function(self, x, y)
return self.x * x + self.y * y
end
}
local dot = setmetatable({x = 3, y = 4}, mt)
print(dot(1, 2)) -- 3*1 + 4*2 = 11
-- 实用场景:构造器模式
local class_mt = {
__call = function(cls, ...)
local instance = setmetatable({}, cls)
if instance.init then
instance:init(...)
end
return instance
end
}
local Dog = setmetatable({}, class_mt)
Dog.__index = Dog
function Dog:init(name, breed)
self.name = name
self.breed = breed
end
function Dog:speak()
return self.name .. " says: Woof!"
end
local buddy = Dog("Buddy", "Golden Retriever")
print(buddy:speak()) -- Buddy says: Woof!
🔴 高级 / Advanced — 元表链与内部机制
1. 元表链 / Metatable Chain
-- 元方法的查找会沿着元表链进行
-- Metamethod lookup follows the metatable chain
local base = {type = "base"}
local base_mt = {
__index = base,
__tostring = function() return "base object" end
}
local child = setmetatable({name = "child"}, base_mt)
local child_mt = {
__index = child,
-- 没有定义 __tostring,会向上查找
}
local obj = setmetatable({}, child_mt)
print(obj.name) -- "child"(通过 child_mt -> child)
print(obj.type) -- "base"(通过 child_mt -> child -> base_mt -> base)
print(tostring(obj)) -- "base object"(通过 child_mt -> base_mt -> __tostring)
-- 注意:__index 的查找链 vs __tostring 的查找链
-- __index: t -> t 的 metatable -> __index -> ...
-- __tostring: 直接从 t 的 metatable 查找,不再往下找
2. rawget / rawset / rawequal
-- rawget(t, k) — 直接读取,绕过 __index
-- rawset(t, k, v) — 直接写入,绕过 __newindex
-- rawequal(a, b) — 直接比较,绕过 __eq
local t = {}
local mt = {
__index = function(_, k) return "default" end,
__newindex = function() error("blocked") end,
}
setmetatable(t, mt)
-- 普通访问触发元方法
print(t.x) -- "default"
-- t.y = 1 -- Error: blocked
-- raw 操作绕过元方法
rawset(t, "y", 1) -- 直接写入
print(rawget(t, "y")) -- 1(直接读取)
print(t.y) -- 1(__index 不会触发,因为 y 已存在)
-- rawequal
local a = setmetatable({}, {__eq = function() return true end})
local b = {}
print(a == b) -- true(触发 __eq)
print(rawequal(a, b)) -- false(直接比较引用)
3. __gc — 析构器 / Destructor
-- __gc 在对象被垃圾回收时调用
-- __gc is called when the object is garbage collected
local mt = {
__gc = function(self)
print("Releasing resource: " .. self.name)
end
}
do
local resource = setmetatable({name = "file_handle"}, mt)
print("Resource created")
end -- resource 超出作用域
collectgarbage() -- 触发 GC
-- 输出: "Releasing resource: file_handle"
-- 注意事项:
-- 1. 只有 table 和 userdata 可以有 __gc
-- 2. __gc 中不应抛出错误
-- 3. __gc 的执行顺序不确定
-- 4. Lua 5.4 中 __gc 只会调用一次(设置 metatable 后取消再恢复不会重新触发)
4. __len — 自定义 # 运算符
-- __len 改变 # 运算符的行为
-- 注意:Lua 5.1 对 table 不调用 __len,只对 string 调用
local sparse = setmetatable({[1]="a", [5]="e", [10]="j"}, {
__len = function(t)
local max = 0
for k in pairs(t) do
if type(k) == "number" and k > max then
max = k
end
end
return max
end
})
print(#sparse) -- 10(不是 2!)
5. __concat — 自定义连接
local mt = {
__concat = function(a, b)
if type(a) == "table" then a = table.concat(a, ",") end
if type(b) == "table" then b = table.concat(b, ",") end
return a .. b
end
}
local list = setmetatable({1, 2, 3}, mt)
print(list .. " is a list") -- "1,2,3 is a list"
小结 / Summary
| 层级 | 你需要知道的 / What You Need to Know |
|---|---|
| 🟢 基础 | setmetatable/getmetatable、__index、__newindex、__tostring、运算符 |
| 🟡 进阶 | 默认值表、只读表、代理模式、__call 让表可调用 |
| 🔴 高级 | 元表链查找、rawget/rawset/rawequal、__gc 析构器、__len |
下一章:面向对象 / OOP