文章目录

Lua 元表:metatable 与 __index

发布于 2026-04-06 16:42:46 · 浏览 13 次 · 评论 0 条

Lua 元表:metatable 与 __index

Lua 的表本质上是键值对的集合。通过元表,可以改变表的默认行为,实现类似于面向对象编程中的操作符重载、继承等特性。核心在于理解 metatable 的设置与 __index 的查找逻辑。


1. 设置与读取元表

元表本质上也是一个普通的表。通过特定的函数将一个表设置为另一个表的元表,从而改变后者的行为。

  1. 使用 setmetatable(table, metatable) 函数 将第二个参数设置的表作为第一个参数的元表。此操作会修改原始表,并返回该表。

    local myTable = {}
    local myMetatable = {}
    setmetatable(myTable, myMetatable)
  2. 使用 getmetatable(table) 函数 获取指定表的元表。如果表没有元表,返回 nil

    print(getmetatable(myTable)) -- 输出: table: 0x... (地址)

2. __index 元方法的基础逻辑

__index 是元表中最常用的键。当访问表中不存在的字段时,Lua 会查找元表中的 __index 键。

查找流程遵循以下逻辑:

flowchart TD A["Start: Access table[key]"] --> B{"Key exists in table?"} B -- "Yes" --> C["Return table value"] B -- "No" --> D{"Table has metatable?"} D -- "No" --> E["Return nil"] D -- "Yes" --> F{"Metatable has __index?"} F -- "No" --> E F -- "Yes" --> G{"Type of __index?"} G -- "Table" --> H["Lookup key in __index table"] G -- "Function" --> I["Call __index(table, key)"]

场景一:__index 为表

如果 __index 的值是一个表,Lua 会在该表中查找对应的键。

  1. 定义 一个父表 parent,包含一个 money 字段。

  2. 定义 一个子表 child,设置为空表。

  3. 设置 child 的元表为 parent,并将元表的 __index 指向 parent 自身。

    local parent = { money = 100 }
    local child = {}
    
    -- 设置元表
    setmetatable(child, { __index = parent })
    
    -- 访问 child 中不存在的 money 字段
    print(child.money) -- 输出: 100
  4. 执行 读取操作。Lua 在 child 中找不到 money,转而查找元表的 __index(即 parent 表),最终返回 100

场景二:__index 为函数

如果 __index 的值是一个函数,Lua 会调用该函数,并将表和键作为参数传递进去。这允许动态计算结果。

  1. 定义 一个表 smartTable

  2. 设置 其元表,其中 __index 为一个函数。

  3. 函数内部 实现 自定义逻辑。

    local smartTable = {}
    
    local mt = {
        __index = function(table, key)
            print("尝试访问不存在的键: " .. key)
            return "默认值"
        end
    }
    
    setmetatable(smartTable, mt)
    
    print(smartTable.name) 
    -- 输出: 
    -- 尝试访问不存在的键: name
    -- 默认值

3. 实现面向对象编程 (OOP)

利用 __index 可以轻松实现类和继承。Lua 没有内置的类概念,但通过元表机制可以模拟。

模拟类与继承

  1. 声明 一个表作为类 Animal

  2. 定义 构造函数 new

  3. 构造函数内部 设置 元表,将 __index 指向 Animal 自身。这使得新对象可以访问类中定义的方法。

    local Animal = {}
    Animal.__index = Animal
    
    function Animal:new(name)
        local obj = { name = name }
        setmetatable(obj, Animal)
        return obj
    end
    
    function Animal:speak()
        print(self.name .. " makes a sound.")
    end
    
    -- 创建实例
    local dog = Animal:new("Buddy")
    dog:speak() -- 输出: Buddy makes a sound.
  4. 分析 执行过程:

    • dog:speak() 被调用。
    • Lua 在 dog 表中查找 speak,未找到。
    • Lua 查找 dog 的元表(即 Animal)。
    • 元表有 __index,且指向 Animal 自身。
    • Lua 在 Animal 中找到 speak 并执行。

实现继承

  1. 创建 子类 Dog

  2. 设置 Dog 的元表为 Animal,并将 __index 也指向 Animal

  3. 重写 父类方法。

    local Dog = {}
    -- Dog 继承自 Animal
    setmetatable(Dog, { __index = Animal })
    
    function Dog:new(name, breed)
        local obj = Animal:new(name) -- 调用父类构造函数
        obj.breed = breed
        setmetatable(obj, Dog)      -- 设置实例的元表为 Dog
        return obj
    end
    
    function Dog:speak()
        print(self.name .. " barks!")
    end
    
    local myDog = Dog:new("Max", "Labrador")
    myDog:speak() -- 输出: Max barks!

4. __newindex 与数据保护

__newindex 是另一个重要的元方法。当尝试向表中 添加 新键值对时,如果元表有 __newindex,Lua 将不执行赋值,而是调用该方法。

  1. 利用 __newindex 实现 只读表。

  2. 创建 一个代理表,代理对原始表的访问。

  3. 拦截 所有写入操作。

    function readOnly(t)
        local proxy = {}
        local mt = {
            __index = t,          -- 读取时查源表
            __newindex = function(table, key, value)
                error("错误: 该表是只读的,不允许修改。")
            end
        }
        setmetatable(proxy, mt)
        return proxy
    end
    
    local days = readOnly{"Sunday", "Monday", "Tuesday"}
    
    -- 尝试修改
    days[1] = "Holiday" 
    -- 报错: stdin:X: 错误: 该表是只读的,不允许修改。

5. 原始访问与设置

在某些情况下(例如在 __index__newindex 函数内部),需要绕过元表机制,直接操作表数据。

  1. 使用 rawget(table, key) 函数 直接获取表中键对应的值,无视 __index 元方法。

  2. 使用 rawset(table, key, value) 函数 直接在表中设置键值对,无视 __newindex 元方法。

    local t = {}
    local mt = {
        __index = function() return "来自元表" end
    }
    setmetatable(t, mt)
    
    print(t.name)       -- 输出: 来自元表
    print(rawget(t, "name")) -- 输出: nil

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文