Skip to content

Lua 元表

元表(Metatable)是 Lua 中一个强大的特性,它允许你在对表(table)或用户数据(userdata)执行某些操作时改变它们的行为。每个表都可以关联一个元表。元表本身就是一个普通的 Lua 表,其中包含一些特定的键(称为元方法,metamethods),这些元方法定义了原始表应该如何表现。

涉及元表的核心概念包括:

  • 修改表在算术、顺序和连接运算符方面的行为。
  • 实现面向对象特性,如继承 (__index)。
  • 控制对表字段的访问 (__index, __newindex)。
  • 自定义表如何转换为字符串 (__tostring) 或像函数一样被调用 (__call)。
  • setmetatable(table, metatable): 将 metatable 设置为 table 的元表。返回 table。如果 metatable 是 nil,则移除 table 的元表。
  • getmetatable(table): 返回 table 的元表,如果 table 没有元表或其元表包含 __metatable 字段(隐藏了元表)则返回 nil。
local my_table = {}
local my_metatable = { info = "This is a metatable" }
setmetatable(my_table, my_metatable)
local retrieved_mt = getmetatable(my_table)
print(retrieved_mt.info) -- Output: This is a metatable

如果一个元表有一个名为 __metatable 的字段,getmetatable 将返回该字段的值,而不是实际的元表。这可以用来保护元表不被外部访问或修改。

local protected_table = {}
local actual_metatable = { __metatable = "Access Denied! This is not the real metatable." }
setmetatable(protected_table, actual_metatable)
print(getmetatable(protected_table)) -- Output: Access Denied! This is not the real metatable.

__index 元方法在你尝试访问表中不存在的键(即值为 nil 的键)时被触发。

它可以是一个函数或另一个表:

  • 如果 __index 是一个函数: Lua 会调用该函数,并将原始表和缺失的键作为参数传递:func(table, key)。函数的返回值就是访问的结果。
  • 如果 __index 是一个表: Lua 会转而在这个 __index 表中查找该键。这通常用于实现继承。

使用 __index 作为函数(默认值)的示例:

local defaults = { x = 0, y = 0, name = "Default" }
local mt_index_func = {
__index = function(table, key)
print("访问缺失的键 '" .. key .. "',返回默认值。")
return defaults[key] -- Return from a defaults table
end
}
local my_obj = { x = 10, z = 100 } -- y 和 name 字段不存在
setmetatable(my_obj, mt_index_func)
print(my_obj.x) -- Output: 10 (exists in my_obj)
print(my_obj.y) -- 触发 __index:输出:Accessing missing key 'y', returning default. 然后是 0
print(my_obj.name) -- 触发 __index:输出:Accessing missing key 'name', returning default. 然后是 Default
print(my_obj.z) -- Output: 100 (exists in my_obj)
print(my_obj.missing) -- 触发 __index:输出:Accessing missing key 'missing', returning default. 然后是 nil(因为 'missing' 不在 defaults 中)

使用 __index 作为表(简单继承)的示例:

local Parent = { greeting = "Hello from Parent" }
Parent.speak = function(self)
print(self.greeting)
end
local mt_index_table = { __index = Parent } -- 如果键不在 Child 中,则在 Parent 中查找
local Child = { specific_greeting = "Hi!" }
setmetatable(Child, mt_index_table)
print(Child.greeting) -- 输出:Hello from Parent(通过 __index 在 Parent 中找到)
Child:speak() -- 输出:Hello from Parent(通过 __index 找到方法)
print(Child.specific_greeting) -- 输出:Hi!(直接在 Child 中找到)

__newindex 元方法在你尝试为一个表中尚不存在的键(即在赋值前值为 nil 的键)赋值时被触发。

它可以是一个函数或另一个表:

  • 如果 __newindex 是一个函数: Lua 会调用该函数,并将原始表、键和被赋的值作为参数传递:func(table, key, value)。该函数负责存储值,可能包含验证或重定向逻辑。
  • 如果 __newindex 是一个表: Lua 会转而在这个 __newindex 表中执行赋值操作,而不是在原始表中执行。

使用 __newindex 作为函数(只读表或日志记录)的示例:

local data_store = {}
local mt_newindex_func = {
__newindex = function(table, key, value)
print("尝试将 '" .. key .. "' 设置为 '" .. tostring(value) .. "'")
-- 若要使表对新键只读:
-- error("Cannot add new keys to this table.", 2)
-- 或记录日志并存储在单独的表中:
print("将 '" .. key .. "' 存储在单独的 data_store 中。")
data_store[key] = value
end,
__index = function(table, key) -- 如果已记录,则从 data_store 中检索
return data_store[key]
end
}
local my_config = { host = "localhost" } -- 一个实例
setmetatable(my_config, mt_newindex_func)
my_config.host = "127.0.0.1" -- 更新现有键,不触发 __newindex
print("主机:", my_config.host) -- Output: Host: 127.0.0.1
my_config.port = 8080 -- 新键,触发 __newindex
-- Output: Attempting to set 'port' to '8080'
-- Storing 'port' in separate data_store.
print("从 my_config 获取的端口:", my_config.port) -- 输出:Port from my_config: 8080(通过 __index)
print("从 data_store 获取的端口:", data_store.port) -- Output: Port from data_store: 8080

rawset(table, key, value) 和 rawget(table, key)

Section titled “rawset(table, key, value) 和 rawget(table, key)”

这些函数允许你直接设置或获取表中的值,绕过 __index 和 __newindex 元方法。这在元方法的实现中很有用,可以避免无限递归。

-- 在 __newindex 元方法内部的示例:
local mt_raw = {
__newindex = function(tbl, k, v)
print("__newindex 被调用,键:", k)
rawset(tbl, k, v) -- 使用 rawset 实际将值存储到原始表中
end
}
local t_raw = {}
setmetatable(t_raw, mt_raw)
t_raw.new_val = 100 -- 触发 __newindex
print(t_raw.new_val) -- 输出:100(在 __newindex 打印消息之后)

你可以定义元方法来重载表的算术、位运算(Lua 5.3+)、连接和比较操作符。

元方法操作符/操作
__add(a, b)加法 (+)
__sub(a, b)减法 (-)
__mul(a, b)乘法 (*)
__div(a, b)浮点除法 (/)
__idiv(a, b)整除 (//) (Lua 5.3+)
__mod(a, b)取模 (%)
__pow(a, b)幂运算 (^)
__unm(a)一元减法(取负,-a)
__concat(a, b)连接 (..)
__len(a)长度操作符 (#a)
__eq(a, b)相等 (==)。注意:如果没有提供 __eq,则 a == b 仅当 a 和 b 是同一个对象时为真。对于 ~=,Lua 使用 __eq 的结果的非。
__lt(a, b)小于 (<)
__le(a, b)小于或等于 (<=)。如果未提供,Lua 会尝试 not (b < a)。
__band(a,b)按位与 (&) (Lua 5.3+)
__bor(a,b)按位或 (|) (Lua 5.3+)
__bxor(a,b)按位异或 (~) (Lua 5.3+)
__bnot(a)按位非 (~a) (Lua 5.3+)
__shl(a,b)按位左移 (<<) (Lua 5.3+)
__shr(a,b)按位右移 (>>) (Lua 5.3+)

示例:使用 __add 实现向量加法

local Vector = {}
Vector.__index = Vector -- 用于方法(如果存在)
function Vector:new(x, y)
local vec = {x = x or 0, y = y or 0}
setmetatable(vec, Vector)
return vec
end
Vector.__add = function(vec1, vec2)
return Vector:new(vec1.x + vec2.x, vec1.y + vec2.y)
end
Vector.__tostring = function(vec)
return "Vector(" .. vec.x .. ", " .. vec.y .. ")"
end
local v1 = Vector:new(10, 20)
local v2 = Vector:new(5, 7)
local v3 = v1 + v2 -- 触发 Vector.__add
print(v1) -- Output: Vector(10, 20)
print(v2) -- Output: Vector(5, 7)
print(v3) -- Output: Vector(15, 27)
  • __call(table, arg1, arg2, ...): 允许表像函数一样被调用:myTable(arg1, arg2)。元方法的第一个参数是表本身,后面是调用时传递的参数。
  • __tostring(table): 由 tostring()(通常也由 print())调用,用于获取表的字符串表示。必须返回一个字符串。
  • __gc(table): 终结器。在表即将被垃圾回收时调用。对于释放外部资源很有用。(默认情况下对用户数据可用;对于表,仅当表是可被垃圾回收的且设置了此元方法时有效 在 Lua 5.2 之前仅对用户数据有效,现在如果表可通过弱引用表回收或没有其他引用,也可以设置 __gc)。通常,对于表来说,这更复杂,并且涉及 C API 或特定于可回收表的情况。

使用 __call 和 __tostring 的示例:

local CallableAdder = {
value_to_add = 0
}
CallableAdder.__call = function(self, num_to_add)
return self.value_to_add + num_to_add
end
CallableAdder.__tostring = function(self)
return "CallableAdder (adds " .. self.value_to_add .. " by default)"
end
local my_adder = { value_to_add = 5 } -- 一个实例
setmetatable(my_adder, CallableAdder)
print(my_adder) -- 触发 __tostring:输出:CallableAdder (adds 5 by default)
local result = my_adder(10) -- 触发 __call:my_adder 作为 self
print("调用结果:", result) -- Output: Result of call: 15 (5 + 10)

实际应用: 元表是 Lua 中面向对象编程(Object-Oriented Programming, OOP)的基础。它们也用于创建领域特定语言(DSL,Domain-Specific Languages)、代理表(proxy tables)、只读表以及实现具有特定行为的自定义数据结构。