Lua元表和元方法详解

深入理解Lua元表(metatable)和元方法(metamethod)机制,掌握表运算重载、继承实现和高级设计模式。

什么是元表

元表(metatable)是 Lua 中一种强大的机制,允许你改变表在运行时的行为。每个表都可以关联一个元表,当对该表执行特定操作(如算术运算、索引访问等)时,Lua 会检查元表中是否定义了对应的元方法(metamethod),如果存在则调用它。

local t = {}
local mt = {}
setmetatable(t, mt)
print(getmetatable(t) == mt)  -- true

每个表可以通过 setmetatable 函数设置元表,通过 getmetatable 函数获取元表。元表本身也是一个普通的 Lua 表,其特殊之处在于键名以双下划线 __ 开头。

算术元方法

Lua 提供了完整的算术元方法,当对表执行算术运算而表本身不支持该运算时,Lua 会查找对应的元方法:

元方法对应操作说明
__add+加法
__sub-减法
__mul*乘法
__div/除法
__mod%取模
__pow^幂运算
__unm- (一元)取负
__idiv//整除
-- 向量加法示例
local Vector = {}
Vector.__index = Vector

function Vector.new(x, y)
    return setmetatable({x = x, y = y}, Vector)
end

function Vector:__add(other)
    return Vector.new(self.x + other.x, self.y + other.y)
end

function Vector:__sub(other)
    return Vector.new(self.x - other.x, self.y - other.y)
end

function Vector:__mul(scalar)
    return Vector.new(self.x * scalar, self.y * scalar)
end

function Vector:__tostring()
    return string.format("Vector(%.2f, %.2f)", self.x, self.y)
end

local v1 = Vector.new(1, 2)
local v2 = Vector.new(3, 4)
print(v1 + v2)       -- Vector(4.00, 6.00)
print(v1 - v2)       -- Vector(-2.00, -2.00)
print(v1 * 3)        -- Vector(3.00, 6.00)

关系元方法

关系元方法用于重载比较运算符:

元方法对应操作说明
__eq==相等
__lt<小于
__le<=小于等于
function Vector:__eq(other)
    return self.x == other.x and self.y == other.y
end

function Vector:__lt(other)
    -- 按向量长度比较
    return (self.x^2 + self.y^2) < (other.x^2 + other.y^2)
end

function Vector:__le(other)
    return (self.x^2 + self.y^2) <= (other.x^2 + other.y^2)
end

local a = Vector.new(1, 1)
local b = Vector.new(2, 2)
print(a < b)   -- true
print(a == Vector.new(1, 1))  -- true

注意 Lua 不提供 __gt__ge 元方法,Lua 会自动将 a > b 转换为 b < a,将 a >= b 转换为 b <= a

索引元方法

索引元方法是 Lua 中最常用的元方法,是实现面向对象编程和模块系统的基础。

__index 元方法

当访问表中不存在的键时,Lua 会查找 __index 元方法:

local proxy = {}
local data = {name = "Lua", version = "5.4"}

setmetatable(proxy, {
    __index = function(t, key)
        print("访问键: " .. key)
        return data[key]
    end
})

print(proxy.name)     -- 访问键: name → Lua
print(proxy.version)  -- 访问键: version → 5.4
print(proxy.missing)  -- 访问键: missing → nil

__index 也可以是一个表,此时 Lua 直接在该表中查找:

local defaults = {color = "white", size = 10}
local widget = setmetatable({}, {__index = defaults})
print(widget.color)  -- white
print(widget.size)   -- 10

__newindex 元方法

当对表中不存在的键赋值时,Lua 会查找 __newindex 元方法:

local readonly = {}
local data = {x = 1, y = 2}

setmetatable(readonly, {
    __index = data,
    __newindex = function(t, key, value)
        error("尝试修改只读表: " .. key, 2)
    end
})

print(readonly.x)       -- 1
readonly.z = 3          -- 报错: 尝试修改只读表: z

调用元方法

__call 元方法使表可以像函数一样被调用,这在实现工厂模式和函数对象时非常有用:

local Counter = {}
Counter.__index = Counter

function Counter.new(start)
    local self = setmetatable({count = start or 0}, Counter)
    return self
end

function Counter:__call()
    self.count = self.count + 1
    return self.count
end

local c = Counter.new(10)
print(c())  -- 11
print(c())  -- 12
print(c())  -- 13

类型转换元方法

__tostring 元方法控制表在字符串化时的行为:

local Point = {}
Point.__index = Point

function Point.new(x, y)
    return setmetatable({x = x, y = y}, Point)
end

function Point:__tostring()
    return string.format("(%g, %g)", self.x, self.y)
end

function Point:__concat(other)
    return tostring(self) .. " → " .. tostring(other)
end

local p1 = Point.new(0, 0)
local p2 = Point.new(3, 4)
print(p1)          -- (0, 0)
print(p1 .. p2)    -- (0, 0) → (3, 4)

用元表实现继承

元表是实现类继承的核心工具。下面展示一个完整的单继承实现:

-- 基类创建函数
local function class(base)
    local cls = {}
    cls.__index = cls

    if base then
        setmetatable(cls, {__index = base})
        cls.__base = base
    end

    function cls:new(...)
        local instance = setmetatable({}, self)
        if instance.__init then
            instance:__init(...)
        end
        return instance
    end

    return cls
end

-- 定义基类 Animal
local Animal = class()

function Animal:__init(name, sound)
    self.name = name
    self.sound = sound
end

function Animal:speak()
    return self.name .. " says " .. self.sound
end

-- 定义子类 Dog
local Dog = class(Animal)

function Dog:__init(name)
    Animal.__init(self, name, "Woof")
    self.tricks = {}
end

function Dog:learn_trick(trick)
    table.insert(self.tricks, trick)
end

function Dog:show_tricks()
    return self.name .. " knows: " .. table.concat(self.tricks, ", ")
end

local dog = Dog:new("Rex")
print(dog:speak())          -- Rex says Woof
dog:learn_trick("sit")
dog:learn_trick("shake")
print(dog:show_tricks())    -- Rex knows: sit, shake

__gc 元方法:终结器

__gc 元方法在表被垃圾回收时调用,类似于其他语言的析构函数:

local Resource = {}
Resource.__index = Resource

function Resource.new(name)
    local self = setmetatable({
        name = name,
        handle = io.tmpfile()
    }, Resource)
    return self
end

function Resource:close()
    if self.handle then
        self.handle:close()
        self.handle = nil
        print(self.name .. " 已释放")
    end
end

function Resource:__gc()
    self:close()
end

do
    local res = Resource.new("数据库连接")
    -- 离开作用域后,垃圾回收时自动调用 __gc
end
collectgarbage()  -- 数据库连接 已释放

实战:实现一个完整的 Set 类

综合使用多种元方法,实现一个功能完备的集合类:

local Set = {}
Set.__index = Set

function Set.new(items)
    local self = setmetatable({items = {}}, Set)
    if items then
        for _, v in ipairs(items) do
            self.items[v] = true
        end
    end
    return self
end

-- 并集: +
function Set:__add(other)
    local result = Set.new()
    for k in pairs(self.items) do result.items[k] = true end
    for k in pairs(other.items) do result.items[k] = true end
    return result
end

-- 交集: *
function Set:__mul(other)
    local result = Set.new()
    for k in pairs(self.items) do
        if other.items[k] then result.items[k] = true end
    end
    return result
end

-- 差集: -
function Set:__sub(other)
    local result = Set.new()
    for k in pairs(self.items) do
        if not other.items[k] then result.items[k] = true end
    end
    return result
end

-- 相等: ==
function Set:__eq(other)
    for k in pairs(self.items) do
        if not other.items[k] then return false end
    end
    for k in pairs(other.items) do
        if not self.items[k] then return false end
    end
    return true
end

-- 长度: #
function Set:__len()
    local count = 0
    for _ in pairs(self.items) do count = count + 1 end
    return count
end

-- 字符串化
function Set:__tostring()
    local items = {}
    for k in pairs(self.items) do
        items[#items + 1] = tostring(k)
    end
    table.sort(items)
    return "{" .. table.concat(items, ", ") .. "}"
end

local a = Set.new({1, 2, 3, 4})
local b = Set.new({3, 4, 5, 6})
print(a + b)   -- {1, 2, 3, 4, 5, 6}
print(a * b)   -- {3, 4}
print(a - b)   -- {1, 2}
print(#a)      -- 4
print(a == Set.new({1, 2, 3, 4}))  -- true

注意事项

使用元表时需要注意以下几点:

  • 元方法的查找遵循原型链:如果元表中没有定义,会继续查找元表的元表
  • rawgetrawset 函数可以绕过元方法,直接访问/修改表
  • 过多的元方法调用会带来性能开销,在高频调用场景中需要权衡
  • Lua 5.2+ 引入了 __pairs__ipairs 元方法,可以自定义迭代行为
  • 元表不能防止 table.inserttable.remove 等标准库函数绕过元方法

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页