0Pricing
Lua Academy · 课时

__index 和 __newindex

使用 __index 和 __newindex 元方法拦截字段读取和写入。

__index 和 __newindex 是 CoddyKit 上的免费 Lua Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Lua Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Lua Academy 课程共包含 4 节课。

__index 回顾

当 Lua 尝试读取表中不存在的键时,会触发 __index。它可以是一个表(用于原型查找),也可以是一个函数(用于动态计算)。这是 Lua 中最常用的元方法,它支持继承、默认值和延迟初始化。

local defaults = {timeout=30, retries=3}
local config = setmetatable({timeout=60}, {__index=defaults})

print(config.timeout)   -- 60 (own value)
print(config.retries)   -- 3  (from defaults)
print(config.missing)   -- nil (not in either)

触发 __newindex

当 Lua 尝试写入表中尚不存在的键时,会触发 __newindex。如果该键已经存在,赋值会直接进行,不会触发 __newindex。

local proxy = setmetatable({}, {
  __newindex = function(t, k, v)
    print("New key:", k, "=", v)
    rawset(t, k, v)  -- actually store it
  end
})

proxy.x = 10    -- New key: x = 10
proxy.x = 20    -- no trigger! x already exists
print(proxy.x)  -- 20

只读表

使用 __newindex 创建只读表。拦截所有写入操作并引发错误。结合使用 __index,可以在不将值存储到表本身的情况下提供这些值。

local function readOnly(t)
  return setmetatable({}, {
    __index = t,
    __newindex = function(_, k, _)
      error("attempt to write to read-only table key: " .. tostring(k), 2)
    end
  })
end

local cfg = readOnly({host="localhost", port=8080})
print(cfg.host)    -- localhost
-- cfg.host = "x"  -- ERROR: attempt to write to read-only table

跟踪更改

使用 __newindex 跟踪表的所有更改,这对于调试、记录更改或响应式数据绑定很有用。将数据存储在隐藏的“后备”表中,并在日志中记录更改。

local log = {}
local data = {}
local tracked = setmetatable({}, {
  __index = data,
  __newindex = function(_, k, v)
    log[#log+1] = {key=k, old=data[k], new=v}
    data[k] = v
  end
})

tracked.name = "Alice"
tracked.age = 30
tracked.name = "Bob"

for _, entry in ipairs(log) do
  print(entry.key, entry.old, "->", entry.new)
end

代理对象

代理会包装另一个对象,以拦截所有读取和写入操作。使用空表作为代理,并让 __index 和 __newindex 都指向函数。真实数据存储在单独的表中。

local function makeProxy(target)
  return setmetatable({}, {
    __index = function(_, k) return target[k] end,
    __newindex = function(_, k, v)
      print("Setting", k, "on target")
      target[k] = v
    end,
  })
end

local real = {x=10}
local p = makeProxy(real)
print(p.x)   -- 10 (reads from real)
p.y = 20     -- Setting y on target
print(real.y) -- 20

__index 链(继承)

链接多个 __index 元表可以创建原型继承。当对象中找不到某个键时,Lua 会依次检查类、父类,等等。每一层都需要自己的元表,并让 __index 指向下一层。

local Base  = {type = "base",  version = 1}
local Child = setmetatable({type = "child"}, {__index = Base})
local Obj   = setmetatable({}, {__index = Child})

print(Obj.type)     -- base? No: child (Child has it)
print(Obj.version)  -- 1 (from Base)
print(Obj.type)     -- child (Child overrides Base.type)

使用 __index 进行延迟初始化

使用函数形式的 __index,在首次访问时计算并缓存开销较大的值。第一次读取时,计算该值并将其直接存储在表中(从而绕过后续的 __index 调用)。

local lazy = setmetatable({}, {
  __index = function(t, k)
    if k == "expensiveData" then
      print("Computing...")
      local result = {1,2,3,4,5}   -- simulate work
      rawset(t, k, result)
      return result
    end
  end
})

print(lazy.expensiveData)  -- Computing...  then table
print(lazy.expensiveData)  -- (no "Computing" this time)

使用 __newindex 进行模式验证

使用 __newindex 强制执行模式:只允许设置预定义的键,或设置类型正确的值。这样可以为 Lua 表提供轻量级的类型安全保障。

local schema = {name="string", age="number", active="boolean"}

local function schemaTable()
  local store = {}
  return setmetatable({}, {
    __index = store,
    __newindex = function(_, k, v)
      local expected = schema[k]
      if not expected then error("unknown key: "..k,2) end
      if type(v) ~= expected then
        error(k.." must be "..expected..", got "..type(v),2)
      end
      store[k] = v
    end
  })
end

local user = schemaTable()
user.name = "Alice"
user.age  = 30
-- user.age = "thirty"  -- ERROR

__index 与 rawget

在 __index 函数内部,使用 rawget 从表中读取数据,而不要再次触发 __index(否则会导致无限递归)。在元方法内部操作实际的表存储时,请始终使用 rawget/rawset。

local counter = setmetatable({count=0}, {
  __index = function(t, k)
    -- Use rawget to avoid recursion
    local c = rawget(t, "count")
    rawset(t, "count", c + 1)
    return rawget(t, k)
  end
})

print(counter.count)    -- 0 (direct, no __index)
print(counter.missing)  -- nil
print(counter.count)    -- 1 (incremented)

组合使用 __index 和 __newindex

最强大的模式是:使用同时具有 __index 和 __newindex 的空代理表,并由隐藏的数据表提供支持。这样可以完全控制读取和写入操作,从而实现更改检测、延迟加载和访问控制等功能。

local function observable(init)
  local data = init or {}
  local listeners = {}
  local obj = setmetatable({}, {
    __index = data,
    __newindex = function(_, k, v)
      local old = data[k]
      data[k] = v
      for _, cb in ipairs(listeners) do cb(k,old,v) end
    end
  })
  obj._onchange = function(_, cb) listeners[#listeners+1]=cb end
  return obj
end

local obs = observable({score=0})
obs:_onchange(function(k,o,n) print(k,o,"->",n) end)
obs.score = 100  -- score  0 -> 100

快速检查

在什么情况下不会触发 __newindex?

回顾:__index 和 __newindex

要点:

  • __index:读取不存在的键时触发;可以是表或函数
  • __newindex:仅在写入新键时触发
  • 在元方法内部使用 rawget/rawset 以避免递归
  • 常见模式:只读表、更改跟踪、延迟初始化、模式验证
  • 空代理表 + 后备表 = 完整拦截读写操作

常见问题解答

「__index 和 __newindex」课时是免费的吗?

是的 — 「__index 和 __newindex」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Lua Academy 课程的其余内容,请升级到 CoddyKit PRO。 Lua Academy 课程共包含 4 节课。

「__index 和 __newindex」这节课中我会学到什么?

使用 __index 和 __newindex 元方法拦截字段读取和写入。 你通过在浏览器中直接运行的动手代码来练习 Lua Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Lua Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Lua Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「__index 和 __newindex」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Lua Academy 课中编写并运行代码吗?

能。每节 Lua Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 元表简介
  2. __index 和 __newindex
  3. 算术元方法
  4. __tostring 和 __len
← 返回 Lua Academy