<返回目录     Powered by claud/xia兄

第9课: 元表与元方法

什么是元表

元表工作原理:

元表是Lua中实现元编程的核心机制。每个表都可以关联一个元表,当对表执行某些操作时,Lua会检查元表中是否定义了对应的元方法。如果定义了,就调用该元方法;否则使用默认行为。元表查找遵循"元方法查找规则":先查找表本身的元表,如果找不到则递归查找元表的元表。这种机制允许我们改变表的行为,实现运算符重载、面向对象、代理模式等高级功能。

元表(Metatable)是Lua中的一个强大特性,允许改变表的行为。通过元表可以实现运算符重载、面向对象等高级功能。

-- 元表的基本概念
-- 元表是一个普通的表
-- 通过setmetatable设置元表
-- 通过getmetatable获取元表
-- 元方法是以双下划线开头的特殊字段

-- 元表的应用场景:
-- 1. 运算符重载
-- 2. 面向对象编程
-- 3. 只读表/只写表
-- 4. 代理模式
-- 5. 默认值
-- 6. 访问控制
-- 7. 类型检查

设置和获取元表

-- 创建表和元表
local t = {}
local mt = {}

-- 设置元表
setmetatable(t, mt)

-- 获取元表
local meta = getmetatable(t)
print(meta == mt)  -- true

-- 链式设置
local t2 = setmetatable({}, {})

-- 保护元表
local t3 = setmetatable({}, {
    __metatable = "受保护的元表"
})
print(getmetatable(t3))  -- 受保护的元表
-- setmetatable(t3, {})  -- 错误:无法修改受保护的元表

__index元方法

-- __index:访问不存在的键时调用
local defaults = {
    name = "未知",
    age = 0
}

local person = {}
setmetatable(person, {__index = defaults})

print(person.name)  -- 未知 (从defaults获取)
print(person.age)   -- 0

person.name = "张三"
print(person.name)  -- 张三 (从person自身获取)

-- __index也可以是函数
local t = {}
setmetatable(t, {
    __index = function(table, key)
        return "键 " .. key .. " 不存在"
    end
})

print(t.anything)  -- 键 anything 不存在

__newindex元方法

-- __newindex:设置不存在的键时调用
local t = {}
local storage = {}

setmetatable(t, {
    __newindex = function(table, key, value)
        print("设置键: " .. key .. " = " .. tostring(value))
        storage[key] = value
    end,
    __index = storage
})

t.name = "张三"  -- 输出: 设置键: name = 张三
print(t.name)    -- 张三

-- 只读表
function readOnly(t)
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function(table, key, value)
            error("表是只读的")
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

local data = {x = 10, y = 20}
local ro = readOnly(data)
print(ro.x)  -- 10
-- ro.x = 100  -- 错误:表是只读的

算术运算符重载

-- 向量类
local Vector = {}
Vector.__index = Vector

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

-- __add:加法运算符
function Vector.__add(a, b)
    return Vector.new(a.x + b.x, a.y + b.y)
end

-- __sub:减法运算符
function Vector.__sub(a, b)
    return Vector.new(a.x - b.x, a.y - b.y)
end

-- __mul:乘法运算符
function Vector.__mul(a, scalar)
    if type(a) == "number" then
        a, scalar = scalar, a
    end
    return Vector.new(a.x * scalar, a.y * scalar)
end

-- __tostring:转字符串
function Vector.__tostring(v)
    return string.format("(%d, %d)", v.x, v.y)
end

local v1 = Vector.new(3, 4)
local v2 = Vector.new(1, 2)

print(v1 + v2)    -- (4, 6)
print(v1 - v2)    -- (2, 2)
print(v1 * 2)     -- (6, 8)
print(2 * v1)     -- (6, 8)

比较运算符重载

-- __eq:等于
-- __lt:小于
-- __le:小于等于

local Set = {}
Set.__index = Set

function Set.new(list)
    local set = {}
    for _, v in ipairs(list) do
        set[v] = true
    end
    setmetatable(set, Set)
    return set
end

function Set.__eq(a, b)
    for k in pairs(a) do
        if not b[k] then return false end
    end
    for k in pairs(b) do
        if not a[k] then return false end
    end
    return true
end

local s1 = Set.new({1, 2, 3})
local s2 = Set.new({3, 2, 1})
local s3 = Set.new({1, 2})

print(s1 == s2)  -- true
print(s1 == s3)  -- false

__call元方法

-- __call:让表可以像函数一样调用
local Calculator = {}

function Calculator.new()
    local calc = {result = 0}
    setmetatable(calc, {
        __call = function(self, operation, value)
            if operation == "add" then
                self.result = self.result + value
            elseif operation == "sub" then
                self.result = self.result - value
            elseif operation == "mul" then
                self.result = self.result * value
            end
            return self.result
        end
    })
    return calc
end

local calc = Calculator.new()
print(calc("add", 10))  -- 10
print(calc("mul", 5))   -- 50
print(calc("sub", 20))  -- 30

__concat元方法

-- __concat:连接运算符
local List = {}
List.__index = List

function List.new(...)
    local list = {...}
    setmetatable(list, List)
    return list
end

function List.__concat(a, b)
    local result = List.new()
    for _, v in ipairs(a) do
        table.insert(result, v)
    end
    for _, v in ipairs(b) do
        table.insert(result, v)
    end
    return result
end

function List.__tostring(list)
    return "[" .. table.concat(list, ", ") .. "]"
end

local l1 = List.new(1, 2, 3)
local l2 = List.new(4, 5, 6)
print(l1 .. l2)  -- [1, 2, 3, 4, 5, 6]

__len元方法

-- __len:长度运算符
local CountedTable = {}

function CountedTable.new()
    local t = {_count = 0}
    setmetatable(t, {
        __index = CountedTable,
        __newindex = function(table, key, value)
            if key ~= "_count" then
                if rawget(table, key) == nil and value ~= nil then
                    rawset(table, "_count", table._count + 1)
                elseif rawget(table, key) ~= nil and value == nil then
                    rawset(table, "_count", table._count - 1)
                end
            end
            rawset(table, key, value)
        end,
        __len = function(table)
            return table._count
        end
    })
    return t
end

local t = CountedTable.new()
t.a = 1
t.b = 2
t.c = 3
print(#t)  -- 3

t.b = nil
print(#t)  -- 2

__pairs和__ipairs元方法

-- __pairs:pairs迭代器(Lua 5.4+)
-- __ipairs:ipairs迭代器(Lua 5.4+)

local OrderedTable = {}

function OrderedTable.new()
    local t = {
        _keys = {},
        _values = {}
    }
    setmetatable(t, {
        __index = OrderedTable,
        __newindex = function(table, key, value)
            if rawget(table, "_values")[key] == nil then
                table.insert(table._keys, key)
            end
            table._values[key] = value
        end,
        __pairs = function(table)
            local i = 0
            return function()
                i = i + 1
                local key = table._keys[i]
                if key then
                    return key, table._values[key]
                end
            end
        end
    })
    return t
end

local ot = OrderedTable.new()
ot.c = 3
ot.a = 1
ot.b = 2

for k, v in pairs(ot) do
    print(k, v)  -- 按插入顺序输出
end

__mode元方法

-- __mode:弱引用表
-- "k":弱键
-- "v":弱值
-- "kv":弱键和弱值

-- 弱键表
local weakKeys = setmetatable({}, {__mode = "k"})
local key = {}
weakKeys[key] = "value"
key = nil
collectgarbage()
print(next(weakKeys))  -- nil (键已被回收)

-- 弱值表
local weakValues = setmetatable({}, {__mode = "v"})
local value = {data = "something"}
weakValues["key"] = value
value = nil
collectgarbage()
print(weakValues["key"])  -- nil (值已被回收)

-- 对象缓存实现
local Cache = {}
Cache.__index = Cache

function Cache.new()
    return setmetatable({
        store = setmetatable({}, {__mode = "v"}),
        hits = 0,
        misses = 0
    }, Cache)
end

function Cache:get(key)
    local value = self.store[key]
    if value then
        self.hits = self.hits + 1
        return value
    else
        self.misses = self.misses + 1
        return nil
    end
end

function Cache:set(key, value)
    self.store[key] = value
end

function Cache:stats()
    return string.format("Hits: %d, Misses: %d", self.hits, self.misses)
end

__gc元方法

-- __gc:垃圾回收时调用(Lua 5.4+)

local FileHandle = {}
FileHandle.__index = FileHandle

function FileHandle.new(filename, mode)
    local self = setmetatable({
        file = io.open(filename, mode) or error("无法打开文件"),
        filename = filename
    }, FileHandle)
    
    -- 设置__gc元方法
    setmetatable(self, {
        __index = FileHandle,
        __gc = function(obj)
            if obj.file then
                obj.file:close()
                print("文件已关闭:", obj.filename)
                obj.file = nil
            end
        end
    })
    
    return self
end

function FileHandle:write(data)
    self.file:write(data)
end

function FileHandle:read(...)
    return self.file:read(...)
end

function FileHandle:close()
    if self.file then
        self.file:close()
        self.file = nil
    end
end

-- 使用
local fh = FileHandle.new("test.txt", "w")
fh:write("Hello, World!")
fh = nil  -- 文件会自动关闭
collectgarbage()  -- 触发垃圾回收

高级元表应用

-- 1. 延迟计算表
local LazyTable = {}

function LazyTable.new()
    local t = {}
    setmetatable(t, {
        __index = function(table, key)
            local value = computeValue(key)  -- 计算值
            rawset(table, key, value)  -- 缓存结果
            return value
        end
    })
    return t
end

-- 2. 只读表(深度保护)
function deepReadOnly(t)
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function()
            error("表是只读的")
        end,
        __metatable = "受保护的元表"
    }
    
    for k, v in pairs(t) do
        if type(v) == "table" then
            proxy[k] = deepReadOnly(v)
        else
            proxy[k] = v
        end
    end
    
    setmetatable(proxy, mt)
    return proxy
end

-- 3. 访问日志表
function LoggedTable(t, logger)
    local proxy = {}
    local mt = {
        __index = function(table, key)
            local value = rawget(t, key)
            logger("get", key, value)
            return value
        end,
        __newindex = function(table, key, value)
            logger("set", key, value)
            rawset(t, key, value)
        end,
        __pairs = function()
            return pairs(t)
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

local logged = LoggedTable({a = 1, b = 2}, function(op, key, value)
    print(string.format("%s %s = %s", op, key, tostring(value)))
end)

print(logged.a)  -- get a = 1
logged.b = 3     -- set b = 3

-- 4. 类型检查表
function TypedTable(schema)
    local t = {}
    local mt = {
        __newindex = function(table, key, value)
            local expectedType = schema[key]
            if expectedType and type(value) ~= expectedType then
                error(string.format("键 '%s' 应该是 %s 类型", key, expectedType))
            end
            rawset(table, key, value)
        end
    }
    setmetatable(t, mt)
    return t
end

local user = TypedTable({
    name = "string",
    age = "number",
    active = "boolean"
})

user.name = "张三"
user.age = 25
-- user.age = "25"  -- 错误:键 'age' 应该是 number 类型

实战:面向对象实现

-- 使用元表实现类和继承
local Animal = {}
Animal.__index = Animal

function Animal.new(name)
    local self = setmetatable({}, Animal)
    self.name = name
    return self
end

function Animal:speak()
    print(self.name .. " 发出声音")
end

-- 继承
local Dog = setmetatable({}, {__index = Animal})
Dog.__index = Dog

function Dog.new(name, breed)
    local self = setmetatable(Animal.new(name), Dog)
    self.breed = breed
    return self
end

function Dog:speak()
    print(self.name .. " 汪汪叫")
end

function Dog:fetch()
    print(self.name .. " 去捡球")
end

local dog = Dog.new("旺财", "金毛")
dog:speak()   -- 旺财 汪汪叫
dog:fetch()   -- 旺财 去捡球
常用元方法列表:
练习题:
  1. 实现一个矩阵类,支持加法和乘法运算
  2. 创建一个只读表,禁止修改任何值
  3. 实现一个自动记录访问次数的表
  4. 使用元表实现一个简单的类继承系统