强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

08 - 元表与元方法 / Metatables & Metamethods

元表与元方法 / Metatables & Metamethods

元表(Metatable)是 Lua 最强大的机制之一。它允许你改变 table 的行为——运算符重载、默认值、只读保护、面向对象……全部通过元表实现。

Metatables are one of Lua’s most powerful mechanisms. They let you change table behavior — operator overloading, defaults, read-only protection, OOP — all through metatables.


🟢 基础 / Basics — 元表入门

1. 什么是元表? / What is a Metatable?

-- 每个 table 都可以关联一个"元表"
-- 元表定义了原始 table 在某些操作下的行为

local t = {}
local mt = {}        -- 元表
setmetatable(t, mt)  -- 将 mt 设为 t 的元表

print(getmetatable(t))    -- table: 0x...
print(getmetatable({}))   -- nil(默认没有元表)

2. __index — 读取不存在的键

-- __index 在访问不存在的键时触发
local defaults = {color = "red", size = 10}
local mt = {__index = defaults}

local obj = setmetatable({}, mt)

print(obj.color)    -- "red"(t.color 是 nil,触发 __index)
print(obj.size)     -- 10
print(obj.weight)   -- nil(__index 中也没有)

-- __index 也可以是函数
local mt2 = {
    __index = function(t, key)
        return "Key '" .. key .. "' not found"
    end
}
local t2 = setmetatable({}, mt2)
print(t2.foo)    -- "Key 'foo' not found"

3. __newindex — 写入不存在的键

-- __newindex 在给不存在的键赋值时触发
local t = {}
local mt = {
    __newindex = function(t, key, value)
        print("Setting " .. tostring(key) .. " = " .. tostring(value))
        rawset(t, key, value)    -- 必须用 rawset 避免无限递归
    end
}
setmetatable(t, mt)

t.x = 10    -- Setting x = 10(触发 __newindex)
t.x = 20    -- 不触发!因为 x 已经存在了

-- 注意:__newindex 只在"新"键赋值时触发
-- 对已存在的键赋值,直接修改,不触发元方法

4. __tostring — 自定义字符串表示

local vector = {x = 3, y = 4}
local mt = {
    __tostring = function(v)
        return string.format("Vector(%g, %g)", v.x, v.y)
    end
}
setmetatable(vector, mt)

print(vector)    -- Vector(3, 4)
-- print 调用 tostring,tostring 调用 __tostring

-- 同样适用于 string.format
print(string.format("pos = %s", vector))    -- pos = Vector(3, 4)

5. 运算符重载 / Operator Overloading

local vec_mt = {}

function vec_mt.__add(a, b)
    return setmetatable({x = a.x + b.x, y = a.y + b.y}, vec_mt)
end

function vec_mt.__sub(a, b)
    return setmetatable({x = a.x - b.x, y = a.y - b.y}, vec_mt)
end

function vec_mt.__mul(a, b)
    if type(b) == "number" then
        return setmetatable({x = a.x * b, y = a.y * b}, vec_mt)
    elseif type(a) == "number" then
        return setmetatable({x = a * b.x, y = a * b.y}, vec_mt)
    end
end

function vec_mt.__eq(a, b)
    return a.x == b.x and a.y == b.y
end

function vec_mt.__lt(a, b)
    return (a.x^2 + a.y^2) < (b.x^2 + b.y^2)
end

function vec_mt.__le(a, b)
    return (a.x^2 + a.y^2) <= (b.x^2 + b.y^2)
end

function vec_mt.__tostring(v)
    return string.format("(%g, %g)", v.x, v.y)
end

local function vec(x, y)
    return setmetatable({x = x, y = y}, vec_mt)
end

local a = vec(1, 2)
local b = vec(3, 4)

print(a + b)       -- (4, 6)
print(a - b)       -- (-2, -2)
print(a * 3)       -- (3, 6)
print(2 * b)       -- (6, 8)
print(a == b)      -- false
print(a < b)       -- true
print(tostring(a)) -- (1, 2)

🟡 进阶 / Intermediate — 元表实用模式

1. 默认值表 / Default Value Table

-- 模式一:默认值为固定值
local function withDefault(t, default)
    return setmetatable(t, {
        __index = function(_, _) return default end
    })
end

local scores = withDefault({}, 0)
scores["Alice"] = 95
print(scores["Alice"])    -- 95
print(scores["Bob"])      -- 0(默认值)

-- 模式二:默认值为表(每次返回新表)
local function autoTable(defaultFn)
    local t = {}
    return setmetatable(t, {
        __index = function(self, key)
            local value = defaultFn(key)
            rawset(self, key, value)
            return value
        end
    })
end

-- 自动为每个键创建空列表
local groups = autoTable(function() return {} end)
table.insert(groups["fruits"], "apple")
table.insert(groups["fruits"], "banana")
table.insert(groups["vegs"], "carrot")
-- groups = {fruits={"apple","banana"}, vegs={"carrot"}}

2. 只读表 / Read-Only Table

local function readOnly(t)
    return setmetatable({}, {
        __index = t,
        __newindex = function(_, k, _)
            error("Cannot modify read-only table: " .. tostring(k), 2)
        end,
        __pairs = function() return pairs(t) end,
        __ipairs = function() return ipairs(t) end,
        __len = function() return #t end,
        __metatable = "locked",    -- 阻止 getmetatable
    })
end

local config = readOnly({host = "localhost", port = 8080})
print(config.host)    -- "localhost"
-- config.port = 3000  -- Error: Cannot modify read-only table: port
-- getmetatable(config) -- "locked"

3. 代理表 / Proxy Pattern

-- 代理模式:用一个中间表控制对真实数据的访问
local function private(t)
    local proxy = {}
    setmetatable(proxy, {
        __index = function(_, k)
            print("[Access] " .. tostring(k))
            return t[k]
        end,
        __newindex = function(_, k, v)
            print("[Modify] " .. tostring(k) .. " = " .. tostring(v))
            t[k] = v
        end,
    })
    return proxy
end

local user = private({name = "Alice", age = 30})
print(user.name)    -- [Access] name  →  Alice
user.age = 31       -- [Modify] age = 31

4. __call — 让表可调用

-- __call 让 table 可以像函数一样被调用
local mt = {
    __call = function(self, x, y)
        return self.x * x + self.y * y
    end
}

local dot = setmetatable({x = 3, y = 4}, mt)
print(dot(1, 2))    -- 3*1 + 4*2 = 11

-- 实用场景:构造器模式
local class_mt = {
    __call = function(cls, ...)
        local instance = setmetatable({}, cls)
        if instance.init then
            instance:init(...)
        end
        return instance
    end
}

local Dog = setmetatable({}, class_mt)
Dog.__index = Dog

function Dog:init(name, breed)
    self.name = name
    self.breed = breed
end

function Dog:speak()
    return self.name .. " says: Woof!"
end

local buddy = Dog("Buddy", "Golden Retriever")
print(buddy:speak())    -- Buddy says: Woof!

🔴 高级 / Advanced — 元表链与内部机制

1. 元表链 / Metatable Chain

-- 元方法的查找会沿着元表链进行
-- Metamethod lookup follows the metatable chain

local base = {type = "base"}
local base_mt = {
    __index = base,
    __tostring = function() return "base object" end
}

local child = setmetatable({name = "child"}, base_mt)
local child_mt = {
    __index = child,
    -- 没有定义 __tostring,会向上查找
}
local obj = setmetatable({}, child_mt)

print(obj.name)    -- "child"(通过 child_mt -> child)
print(obj.type)    -- "base"(通过 child_mt -> child -> base_mt -> base)
print(tostring(obj)) -- "base object"(通过 child_mt -> base_mt -> __tostring)

-- 注意:__index 的查找链 vs __tostring 的查找链
-- __index: t -> t 的 metatable -> __index -> ...
-- __tostring: 直接从 t 的 metatable 查找,不再往下找

2. rawget / rawset / rawequal

-- rawget(t, k)    — 直接读取,绕过 __index
-- rawset(t, k, v) — 直接写入,绕过 __newindex
-- rawequal(a, b)  — 直接比较,绕过 __eq

local t = {}
local mt = {
    __index = function(_, k) return "default" end,
    __newindex = function() error("blocked") end,
}
setmetatable(t, mt)

-- 普通访问触发元方法
print(t.x)       -- "default"
-- t.y = 1        -- Error: blocked

-- raw 操作绕过元方法
rawset(t, "y", 1)    -- 直接写入
print(rawget(t, "y")) -- 1(直接读取)
print(t.y)            -- 1(__index 不会触发,因为 y 已存在)

-- rawequal
local a = setmetatable({}, {__eq = function() return true end})
local b = {}
print(a == b)          -- true(触发 __eq)
print(rawequal(a, b))  -- false(直接比较引用)

3. __gc — 析构器 / Destructor

-- __gc 在对象被垃圾回收时调用
-- __gc is called when the object is garbage collected

local mt = {
    __gc = function(self)
        print("Releasing resource: " .. self.name)
    end
}

do
    local resource = setmetatable({name = "file_handle"}, mt)
    print("Resource created")
end    -- resource 超出作用域

collectgarbage()    -- 触发 GC
-- 输出: "Releasing resource: file_handle"

-- 注意事项:
-- 1. 只有 table 和 userdata 可以有 __gc
-- 2. __gc 中不应抛出错误
-- 3. __gc 的执行顺序不确定
-- 4. Lua 5.4 中 __gc 只会调用一次(设置 metatable 后取消再恢复不会重新触发)

4. __len — 自定义 # 运算符

-- __len 改变 # 运算符的行为
-- 注意:Lua 5.1 对 table 不调用 __len,只对 string 调用

local sparse = setmetatable({[1]="a", [5]="e", [10]="j"}, {
    __len = function(t)
        local max = 0
        for k in pairs(t) do
            if type(k) == "number" and k > max then
                max = k
            end
        end
        return max
    end
})

print(#sparse)    -- 10(不是 2!)

5. __concat — 自定义连接

local mt = {
    __concat = function(a, b)
        if type(a) == "table" then a = table.concat(a, ",") end
        if type(b) == "table" then b = table.concat(b, ",") end
        return a .. b
    end
}

local list = setmetatable({1, 2, 3}, mt)
print(list .. " is a list")    -- "1,2,3 is a list"

小结 / Summary

层级你需要知道的 / What You Need to Know
🟢 基础setmetatable/getmetatable、__index、__newindex、__tostring、运算符
🟡 进阶默认值表、只读表、代理模式、__call 让表可调用
🔴 高级元表链查找、rawget/rawset/rawequal、__gc 析构器、__len

下一章:面向对象 / OOP