【语法算法】
元方法 __index 设为函数的本质就这一行:
setmetatable(t, { __index = function(t, k) return source[k] end })
读取表中不存在的键时,Lua 调用 __index 函数——代理转发和懒加载的全部机制就这一行。可这一行的前后各有一个分界:前面是"键存在时不触发",后面是"返回什么由函数决定"。今天拆开。
一、触发、转发、缓存三段
__index 为函数的三段。触发:读取表中不存在的键时——Lua 先查表本身——查不到就查元表的 __index——__index 是函数就调用它——键存在时直接返回不经过 __index。转发:__index 函数里可以把读取转发到另一个表——源表的键值通过代理表的 __index 透传给调用方——调用方读代理表就像读源表一样。缓存:转发的结果可以缓存在代理表里——下次读同一个键就不用再转发了——直接从代理表返回缓存值——懒加载的全部机制就是"第一次读触发转发并缓存,后续读直接返回缓存"。
graph TD
A["t.foo"] --> B{"t中有foo?"}
B -->|是| C["直接返回"]
B -->|否| D["查元表__index"]
D -->|函数| E["调用__index(t, 'foo')"]
E --> F["从source读取"]
F --> G["缓存到t"]
E --> H["返回source.foo"]
二、代理转发和懒加载的正确写法
local function lazyProxy(source)
local cache = {}
return setmetatable({}, {
__index = function(t, k)
local v = source[k]
if v ~= nil then
t[k] = v -- 缓存: 首次读后写入代理表
end
return v
end
})
end
local config = { host = "127.0.0.1", port = 3306 }
local lazy = lazyProxy(config)
print(lazy.host) -- 第一次: 走__index转发并缓存
print(lazy.host) -- 第二次: 直接从代理表返回
lazyProxy 返回的代理表一开始是空的——首次读取任何键都走 __index 转发到源表——转发结果写入代理表自身——下次读同一个键时直接从代理表命中——这就是懒加载的全部本质。
三、缓存写入的纪律和 blind spots
缓存写入用 t[k] = v 而不是 rawset(t, k, v)——因为代理表自身没有设置 __newindex——普通赋值不会触发 __index——所以写入是安全的。但如果代理表也设了 __newindex,写入就会触发 __newindex——__newindex 如果又把写入转发回源表——就形成了无限循环——这就是元表编程里最常见的"元方法互调"事故。避免的方法是 __newindex 里用 rawset 绕过元表直接写入——和 __index 里的 rawget 配对使用——读写都走原始操作就永远不会触发元方法递归。最后一条纪律:懒加载的缓存表不要设 __mode——弱引用缓存会在 GC 后丢失——懒加载的缓存应该用强引用表——缓存丢了就失去了懒加载的意义。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【游戏功能】 再抛一个老问题:奶量是固定好还是浮动好?固定回五十的术式,满血喝是五十、残血喝也是五十,账目清楚,可总差点意思…
【游戏功能】 前阵子一场"永动机事故"在群里传了半天:某个会喷火的精英怪被玩家围了一整晚,火喷了八个钟头没停——后台一看,它…
【游戏功能】 mode=mode==='red'?'blue':'red' ——先拆这一行。一个变量存着当前纹色,三元运算符…
【游戏功能】 上一版埋了个查了半宿的隐蔽报错:测试组报"玩家血条归零了人还站着",后台的死亡日志一条没有,复活接口也没被调过…
【游戏功能】 先抛一个坑:增益为什么只能自己挂?怒气、护体、神行,攻略里全在教你"什么时候开自己的招",可怪身上明晃晃挂着的…
【游戏功能】 昨天夜里一场线上事故把"反纹"顶上了台面:一个玩家把反纹挂在自己身上站桩挂机,怪的火球过来他没倒,反倒躺下的是…