Lua 中的 __index、__newindex、rawget 与 rawset  介绍

Lua 中的 __index、__newindex、rawget 与 rawset 介绍

在 Lua 中,表(table)是最核心的数据结构。通过为其设置“元表”(metatable),我们可以改变表在某些操作下的默认行为。其中,__index__newindex 是两个最常用的元方法,而 rawgetrawset 是与之配套的“绕过元表”的操作函数。理解它们的作用和关系,有助于更灵活、安全地使用 Lua 表。


一、__index —— 读取不存在的键时的行为

当访问一个表中不存在的键时(例如 t.keyt["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

三、rawgetrawset —— 绕过元表的直接操作

有时我们希望跳过元表机制,直接对表进行读写。这时就需要 rawgetrawset

  • 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

五、注意事项

  1. __index__newindex 只对“不存在的键”生效
  2. __index__newindex 函数中操作表时,务必使用 rawget / rawset,否则可能触发递归。
  3. 元方法不会被继承 —— 子表必须显式设置自己的元表。
  4. 性能敏感的代码中,频繁触发元方法会有开销,可考虑用 rawget / rawset 优化。

总结

__index__newindexrawgetrawset 是 Lua 元表机制中最基础也最实用的组成部分。它们不复杂,但在构建配置系统、对象模型、数据代理、调试工具时非常有用。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注