Lua 元表
Lua - 元表
Section titled “Lua - 元表”元表(Metatable)是 Lua 中一个强大的特性,它允许你在对表(table)或用户数据(userdata)执行某些操作时改变它们的行为。每个表都可以关联一个元表。元表本身就是一个普通的 Lua 表,其中包含一些特定的键(称为元方法,metamethods),这些元方法定义了原始表应该如何表现。
涉及元表的核心概念包括:
- 修改表在算术、顺序和连接运算符方面的行为。
- 实现面向对象特性,如继承 (
__index)。 - 控制对表字段的访问 (
__index,__newindex)。 - 自定义表如何转换为字符串 (
__tostring) 或像函数一样被调用 (__call)。
设置和获取元表
Section titled “设置和获取元表”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 元方法
Section titled “__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 元方法
Section titled “__index 元方法”__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. 然后是 0print(my_obj.name) -- 触发 __index:输出:Accessing missing key 'name', returning default. 然后是 Defaultprint(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 元方法
Section titled “__newindex 元方法”__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" -- 更新现有键,不触发 __newindexprint("主机:", 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: 8080rawset(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 -- 触发 __newindexprint(t_raw.new_val) -- 输出:100(在 __newindex 打印消息之后)用于操作符的元方法
Section titled “用于操作符的元方法”你可以定义元方法来重载表的算术、位运算(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 vecend
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)其他重要的元方法
Section titled “其他重要的元方法”__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_addend
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 作为 selfprint("调用结果:", result) -- Output: Result of call: 15 (5 + 10)实际应用: 元表是 Lua 中面向对象编程(Object-Oriented Programming, OOP)的基础。它们也用于创建领域特定语言(DSL,Domain-Specific Languages)、代理表(proxy tables)、只读表以及实现具有特定行为的自定义数据结构。