第 11 章 元表与元方法
第 11 章 元表与元方法
元表定义了表在特定操作下的行为。访问不存在的键、执行加法、比较两个表或连接字符串时,Lua 会检查相应元方法。借助元表可以实现默认值、继承、代理对象和运算符重载。
元表基础
每个表可以关联另一个表作为元表:
local object = {}
local meta = {}
setmetatable(object, meta)
print(getmetatable(object) == meta) -- true
元方法是在元表中具有固定名称的字段,例如 __index、__add、__tostring。普通字段不会自动生效:
local point = { x = 1, y = 2 }
setmetatable(point, {
__tostring = function(value)
return string.format("Point(%d, %d)", value.x, value.y)
end
})
print(tostring(point)) -- Point(1, 2)
setmetatable 返回传入的第一个表。除非通过 __metatable 保护,否则 getmetatable 可以读取元表。
算术与比较元方法
算术运算
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({ x = x, y = y }, Vector)
end
Vector.__add = function(left, right)
return Vector.new(left.x + right.x, left.y + right.y)
end
Vector.__mul = function(vector, scalar)
return Vector.new(vector.x * scalar, vector.y * scalar)
end
local result = Vector.new(1, 2) + Vector.new(3, 4)
print(result.x, result.y) -- 4 6
local scaled = Vector.new(1, 2) * 3
print(scaled.x, scaled.y) -- 3 6
Lua 5.4 常见的算术元方法包括:
| 元方法 | 操作 |
|---|---|
__add |
+ |
__sub |
- |
__mul |
* |
__div |
/ |
__idiv |
// |
__mod |
% |
__pow |
^ |
__unm |
一元 - |
__band、__bor、__bxor |
位运算 |
__bnot、__shl、__shr |
一元按位非与移位 |
二元元方法会先看左操作数的元表,再尝试右操作数。两个值的类型和元方法设计应在文档中明确,否则重载会让代码难以推理。
比较运算
local Version = {}
Version.__index = Version
function Version.new(major, minor)
return setmetatable({ major = major, minor = minor }, Version)
end
Version.__eq = function(left, right)
return left.major == right.major and left.minor == right.minor
end
Version.__lt = function(left, right)
if left.major == right.major then
return left.minor < right.minor
end
return left.major < right.major
end
local first = Version.new(5, 4)
local second = Version.new(5, 4)
local third = Version.new(5, 5)
print(first == second) -- true
print(first < third) -- true
实现了 __lt 和 __eq 后,可以定义 __le 提供小于等于行为。比较元方法不应产生副作用,也应保持等价关系一致。
__index 与继承
访问表中不存在的键时,Lua 会读取元表的 __index。它既可以是表,也可以是函数:
local defaults = {
host = "127.0.0.1",
port = 8080
}
local config = setmetatable({}, {
__index = defaults
})
print(config.host) -- 127.0.0.1
config.port = 9090
print(config.port) -- 9090
print(defaults.port) -- 8080
函数形式的 __index 可以记录访问或生成默认值:
local lazyConfig = setmetatable({}, {
__index = function(target, key)
local value = "generated:" .. key
rawset(target, key, value)
return value
end
})
rawget 和 rawset 绕过元方法,适合实现底层逻辑:
print(rawget(config, "host")) -- nil
多层原型链
local base = { baseMethod = function() return "base" end }
local middle = setmetatable({}, { __index = base })
local leaf = setmetatable({}, { __index = middle })
print(leaf.baseMethod()) -- base
原型链过深会让查找和调试变复杂。工程中建议控制层级,并避免循环引用。
__newindex 与访问拦截
当给表中不存在的键赋值时会调用 __newindex。已有的键仍直接赋值,除非对象是代理:
local source = {}
local proxy = setmetatable({}, {
__index = source,
__newindex = function(_, key, value)
source[key] = value
end
})
proxy.name = "Lua"
print(source.name) -- Lua
函数形式可以实现字段校验:
local function createSettings()
local values = {}
local allowed = { timeout = true, retries = true }
return setmetatable({}, {
__index = values,
__newindex = function(_, key, value)
if not allowed[key] then
error("unknown setting: " .. tostring(key), 2)
end
values[key] = value
end
})
end
__call 与 __tostring
__call 让表可以像函数一样被调用:
local counter = setmetatable({ value = 0 }, {
__call = function(self, step)
self.value = self.value + (step or 1)
return self.value
end
})
print(counter()) -- 1
print(counter(5)) -- 6
__tostring 应返回字符串,供 tostring、日志和错误消息使用。不要让 __tostring 递归调用自身或抛出新的复杂错误。
只读表与代理对象
代理表可以把真实数据隐藏起来,同时提供受控访问:
local function readonly(data)
return setmetatable({}, {
__index = data,
__newindex = function()
error("attempt to modify read-only table", 2)
end
})
end
local settings = readonly({ host = "localhost" })
print(settings.host)
-- settings.host = "example.com" -- 抛出错误
这只保护通过代理进行的写入。若调用者仍能拿到原始 data,就可以绕过代理。因此构造函数应只暴露代理。
保护元表
设置 __metatable 后,普通代码无法替换或读取元表:
local protected = setmetatable({}, {
__metatable = "locked"
})
print(getmetatable(protected)) -- locked
-- setmetatable(protected, {}) -- 抛出错误
它只能提高误用成本,不是安全边界。Lua 沙箱必须同时限制可调用的库和函数。