在 Lua 中,表(table)是最核心的数据结构。通过为其设置“元表”(metatable),我们可以改变表在某些操作下的默认行为。其中,__index
和 __newindex
是两个最常用的元方法,而 rawget
和 rawset
是与之配套的“绕过元表”的操作函数。理解它们的作用和关系,有助于更灵活、安全地使用 Lua 表。
一、__index
—— 读取不存在的键时的行为
当访问一个表中不存在的键时(例如 t.key
或 t["key"]
),如果该表设置了元表,且元表中包含 __index
字段,Lua 就会调用它。
__index
可以是一个函数,也可以是一个表。
示例 1:__index
是函数
local t = {}
local mt = {
__index = function(tbl, key)
print("访问了不存在的键:", key)
return nil
end
}
setmetatable(t, mt)
print(t.name) -- 输出: 访问了不存在的键: name
-- 然后输出: nil
示例 2:__index
是表(常用于“默认值”或“继承”)
local defaults = { x = 0, y = 0 }
local obj = {}
setmetatable(obj, { __index = defaults })
print(obj.x) -- 0(从 defaults 获取)
print(obj.z) -- nil(defaults 里也没有)
注意:如果键在表中已存在,则不会触发
__index
。
二、__newindex
—— 写入不存在的键时的行为
当给一个表中原本不存在的键赋值时(例如 t.key = value
),如果该表有元表且定义了 __newindex
,Lua 会调用它,而不是直接赋值。
和 __index
一样,__newindex
也可以是函数或表。
示例 1:__newindex
是函数(常用于拦截、校验、日志)
local t = {}
local mt = {
__newindex = function(tbl, key, value)
print("尝试设置:", key, "=", value)
-- 可选择是否真正写入
rawset(tbl, key, value) -- 使用 rawset 避免递归
end
}
setmetatable(t, mt)
t.name = "Alice" -- 输出: 尝试设置: name = Alice
print(t.name) -- Alice
示例 2:__newindex
是表(常用于“写入重定向”)
local storage = {}
local proxy = {}
setmetatable(proxy, { __newindex = storage })
proxy.x = 10
proxy.y = 20
print(proxy.x, proxy.y) -- nil, nil(proxy 本身没写入)
print(storage.x, storage.y) -- 10, 20(写入到了 storage)
注意:
- 如果键已存在,赋值不会触发
__newindex
。- 在
__newindex
函数内直接写tbl[key] = value
会导致无限递归,应使用rawset
。
三、rawget
与 rawset
—— 绕过元表的直接操作
有时我们希望跳过元表机制,直接对表进行读写。这时就需要 rawget
和 rawset
。
rawget(t, key)
→ 直接获取t[key]
,不触发__index
rawset(t, key, value)
→ 直接设置t[key] = value
,不触发__newindex
示例:
local t = { x = 1 }
local mt = {
__index = function() return "from __index" end,
__newindex = function() error("禁止写入") end
}
setmetatable(t, mt)
print(t.y) -- "from __index"
print(rawget(t, "y")) -- nil(直接读,不触发 __index)
-- t.z = 99 -- 会报错:禁止写入
rawset(t, "z", 99) -- ✅ 成功,绕过 __newindex
print(t.z) -- 99
这两个函数在元方法内部操作表时尤其重要,可以避免触发元表导致的递归或副作用。
四、常见使用场景小结
场景 | 推荐方案 |
---|---|
提供默认值或实现继承 | __index = 表 |
拦截读取做日志或计算 | __index = 函数 |
实现只读表 | __newindex = 报错函数 |
数据校验或属性封装 | __newindex = 校验函数 + rawset |
避免元表干扰或递归 | 使用 rawget / rawset |
在元方法内部安全读写表 | 必须使用 rawget / rawset |
五、注意事项
__index
和__newindex
只对“不存在的键”生效。- 在
__index
或__newindex
函数中操作表时,务必使用rawget
/rawset
,否则可能触发递归。 - 元方法不会被继承 —— 子表必须显式设置自己的元表。
- 性能敏感的代码中,频繁触发元方法会有开销,可考虑用
rawget
/rawset
优化。
总结
__index
、__newindex
、rawget
、rawset
是 Lua 元表机制中最基础也最实用的组成部分。它们不复杂,但在构建配置系统、对象模型、数据代理、调试工具时非常有用。
发表回复