元表与元方法

  • Lua 中每个值都有一套预定义的操作集,表示这个值可以有什么操作,这个操作集就是这个值的元表。
  • 对一个值进行某种操作,lua 首先会查找它的元表看看有没有对应的字段,如果找到了字段,则调用该字段的值,也就是元方法,它是一个函数。
    比如进行 a+b 操作时,先查找 a 或 b 的元表中有没有字段 __add,如果有这个字段,调用该字段对应的元方法,否则操作不合法。
  • table 和 userdata 可以有各自独立的 metatable,其它数据类型共享其类型所属的单一 metatable
  • 字符串默认有一个元表,其它数据类型默认没有元表,新创建的 table 也没有元表,可以使用 setmetatable 来为 table 设置元表。
print(getmetatable("hello")) -- 003E7050
print(getmetatable("lua")) -- 003E7050
print(getmetatable(12)) -- nil
print(getmetatable({})) -- nil
t = {}
print(getmetatable(t)) -- nil
setmetatable(t, {})
print(getmetatable(t)) --0048A700

算术类的元方法

  • 在元表中,每种算术运算符都有对应的字段名。

加法(__add)、减法(__sub)、乘法(__mul)、除法(__div)、
取模(__mod)、取幂(__pow)、取相反数(__unm)、连接(__concat)

  • 如果我们想给某个值赋予它本来没有的运算,先给它设置一个元表,然后修改元表,给对应操作的字段设置元方法
    比如我们想给集合(table)赋予相加操作,表示两个集合求并集
-- 定义元表
local mt = {}

Set = {}

Set.new = function(l)
    local set = {}
    -- 设置元表
    setmetatable(set, mt)
    for _, v in ipairs(l) do
        set[v] = true
    end
    return set
end

Set.union = function(a, b)
    local res = {}
    for k in pairs(a) do
        res[k] = true
    end
    for k in pairs(b) do
        res[k] = true
    end
    return res
end

Set.intersection = function(a, b)
    local res = {}
    for k in pairs(a) do
        if b[k] then
            res[k] = true
        end
    end
    return res
end

Set.tostring = function(a)
    local l = {}
    for k in pairs(a) do
        l[#l + 1] = k
    end
    return "{" .. table.concat(l, ',') .. "}"
end

Set.print = function(a)
    print(Set.tostring(a))
end

-- 给元表添加元方法
mt.__add = Set.union
mt.__mul = Set.intersection

set1 = Set.new({1, 2, 3, 4, 5, 6})
set2 = Set.new({2, 4, 6, 8})
print(getmetatable(set1) == getmetatable(set2))

-- set3 = Set.union(set1, set2)
set3 = set1 + set2
Set.print(set3)
set3 = set1 * set2
Set.print(set3)

关系类的元方法

  • 关系类的元方法有下面三个,其它的关系运算都可以转成这三种

等于(__eq)、小于(__lt)、小于等于(__le)

  • 关于类的元方法不能应用于混合的类型,如果两个对象的元方法不同,对它们进行小于比较会出错,如果做等于比较,元方法不同直接返回 false,元方法相同才会进行比较
mt.__le = function(a, b)
    for k, v in pairs(a) do
        if not b[k] then
            return false
        end
    end
    return true
end

mt.__lt = function(a, b)
    return a <= b and not (b <= a)
end

mt.__eq = function(a, b)
    return a <= b and b <= a
end

set1 = Set.new({2, 4, 6})
set2 = Set.new({1, 2, 3, 4, 5, 6})
print(set1 < set2) -- true
print(set1 > set2) -- false
print(set1 <= set2) -- true
print(set1 == set2) -- false
print(set1 == Set.new({2, 4, 6})) -- true
print(set1 == {2, 4, 6}) -- false
-- print(1 < "hello")  -- error

其它库定义的元方法

  • __tostring 字段定义了一个对象在使用 print 输出时的格式
  • __metatable 字段保存了元表的值,如果修改了这个字段的值,元表就被保护起来,获取元的值将得到新设置的值,而设置元表的值将会出错
mt.__tostring = Set.tostring
-- 下面两条语句等价
print(set1)
print(Set.tostring(set1))

-- 保护元表
mt.__metatable = "不能访问"
s = Set.new({})
print(getmetatable(s))
-- setmetatable(s, {}) -- error

table 访问的元方法

__index 元方法

  • 当访问一个 table 不存在的字段时,解释器会首先查找 table 有没有 __index 元方法,如果有则调用 __index 元方法,如果没有才返回 nil
  • __index 的值也可以是一个 table
  • 使用 rawget 可以对 table 进行原始访问,即不判断 __index 字段
Window = {}

-- 定义原型
Window.propotype = {x = 10, y = 10, width = 100, height = 100}

-- 定义元表
Window.mt = {}
-- __index 的值可以是函数
Window.mt.__index = function(t, k)
    return Window.propotype[k]
end
-- __index 的值也可以是 table
-- Window.mt.__index = Window.propotype

-- 构造函数
Window.new = function(o)
    setmetatable(o, Window.mt)
    return o
end

win = Window.new({x = 0, y = 0})
print(win.x, win.width) -- 0 100
print(win.x, rawget(win, width)) -- 0 nil

__newindex 元方法

  • 当设置一个 table 不存在的字段的值时,解释器会首先查找 table 有没有 __newindex 字段,如果有则调用对应的元方法,如果没有才给 table 设置新字段
  • __newindex 也可以是函数或 table
  • 使用 rawset 可绕过 __newindex 直接对 table 赋值
Window.mt.__newindex = function(t, k, v)
    Window.propotype[k] = v
end

win["color"] = "red"
print(win.color, rawget(win, "color")) -- red nil
rawset(win, "weight", 8)
print(win.weight, rawget(win, "weight")) -- 8 8

设置 table 的默认值

  • 如果没有为 table 元表的 __index 字段赋值,那么 table 的字段默认值就是 nil;可以通过 __index 修改 table 的默认值

给每个 table 新建一个元表

使用这种方式很简单,只需要让 __index 元方法固定返回一个值即可;但这种方式必须为每个 table 新创建一个元表,开销比较大

setDefault = function(t, value)
    local mt = {__index = function()
            return value
        end}
    setmetatable(t, mt)
end

local t = {}
print(t.x)  -- nil
setDefault(t, 10)
print(t["x"]) -- 10

让不同的 table 共享一个元表

由于默认值是与元方法 __index 关联在一起的,因此无法直接在元方法中保存和返回多个默认值;可以将默认值保存在各个 table 本身,然后使用一个 key 在元方法中访问
- 如果不考虑名字冲突的话,可使用 _ 来表示这个 key

-- 共同的元表
local mt = {__index = function(t)
        return t.___
    end}

setDefault = function(t, value)
    -- 默认值保存在 table 本身
    t.___ = value
    setmetatable(t, mt)
end
  • 如果担心默认值的 key 与 table 的其它字段有名字冲突,使用一个新 table 来当作默认值的 key 以确保名字唯一
local key = {}
mt = {__index = function(t)
        return t[key]
    end}

setDefault = function(t, value)
    -- 默认值保存在 table 本身
    t[key] = value
    setmetatable(t, mt)
end

跟踪 table 的访问

只有一个 table 为空表时,才能保证每次访问 table 的字段才能定位到 __index 或 __newindex;因此可以创建一个空的 table 作为原 table 的代理,然后在这个空的 table 的 __index 和 __newindex 中访问原 table 的字段

给每个 table 新建一个代理 table

setAgent = function(t)
    local agent = {}
    local mt = {__index = function(_, k)
            print("正在访问元素" .. tostring(k))
            local value = t[k]
            if value == nil then
                print("元素为空,返回默认值")
                value = 10
            end
            return value
        end, __newindex = function(_, k, v)
            print("给元素" .. tostring(k) .. "赋值为" .. tostring(v))
            t[k] = v
        end}
    setmetatable(agent, mt)
    return agent
end

local t = {x = 10, y = 20}
t = setAgent(t)
print(t.x)
print(t.w)
t.w = 100
print(t.w)

通过函数 setAgent 给表 t 设置了一个代理空表,然后把这个空表赋给 t,之后对表 t 的操作都是通过这个空表来操作的,通过这个空表的 __index 和 __newindex 来操作原来的表

让不同的代理 table 共享一个元表

使用跟默认值一样的方式,把原来的 table 保存到各个代理本身,然后元表通过 key 来访问

local index = {}
local mt = {__index = function(t, k)
        print("正在访问元素" .. tostring(k))
        local value = t[index][k]
        if value == nil then
            print("元素为空,返回默认值")
            value = 10
        end
        return value
    end, __newindex = function(t, k, v)
        print("给元素" .. tostring(k) .. "赋值为" .. tostring(v))
        t[index][k] = v
    end}

setAgent = function(t)
    local agent = {}
    agent[index] = t
    setmetatable(agent, mt)
    return agent
end

只读的 table

通过代理很容易实现只读的 table,只需要在设置值的时候返回一个错误即可

readOnly = function(t)
    local mt = {__index = t, __newindex = function(t, k, v)
            error("the table is read only!", 2)
        end}
    local agent = {}
    setmetatable(agent, mt)
    return agent
end

t = readOnly(t)
t.x = 10    -- error