元表允许当遇到未知操作时,改变值的行为。例如,使用元表,可以定义表a与表b的关系运算a+b。当lua尝试两个表相加时,会检查是否其中一个有元表并且元表是否有__add字段。

元表在面向对象的术语中是一种受限的类。正如类一样,元表定义实例行为。但是元表有比类更多的限制。仅仅给预定义的操作给定行为,并且元表没有继承。

lua中的每个值可以有元表。表和用户数据有个人的元表。其它类型的值共享单个元表。lua总是创建没有元表的新表。

获取表的元表使用getmetatable,设置使用setmetatable。lua中仅仅可以设置表的元表。要控制其它类型值的元表,必须使用c代码或者debub库。字符串库为字符串设置了元表。其他类型默认是没有元表。

print(getmetatable("hi"))
print(getmetatable("xuxu"))
print(getmetatable(10))
print(getmetatable(print))

输出为:
table: 0000000000dfa170
table: 0000000000dfa170
nil
nil

任何表都可以是任何值的元表。一组相关的表可以共享一个共同的元表,此元素描述了它们的公共行为。表可以是它自己的元表,描述它自己的个人行为。

1、算术元方法

Set模块定义如下

local Set = {}

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

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

    return res
end

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

    return res
end

function Set.tostring(set)
    local l = {}
    for e in pairs(set) do
        l[#l + 1] = tostring(e)
    end

    return "{" .. table.concat(l, ", ") .. "}"
end

return Set

希望使用算术加法来计算两个集合的交集,此时使用元表来实现。 在模块中定义元表,在创建时设置元表数据

local Set = {}

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

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

    return res
end

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

    return res
end

function Set.tostring(set)
    local l = {}
    for e in pairs(set) do
        l[#l + 1] = tostring(e)
    end

    return "{" .. table.concat(l, ", ") .. "}"
end

return Set

创建的集合会共用同一个元表。

local Set = require('set')
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
print(getmetatable(s1))
print(getmetatable(s2))

输出为:
table: 0000000000c89f30
table: 0000000000c89f30

使用+来表示两个集合的并集,*来计算两个集合的交集。即使用元表的__add和__mul

local Set = {}

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

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

    return res
end

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

    return res
end

function Set.tostring(set)
    local l = {}
    for e in pairs(set) do
        l[#l + 1] = tostring(e)
    end

    return "{" .. table.concat(l, ", ") .. "}"
end

mt.__add = Set.union
mt.__mul = Set.intersection
return Set

样例代码为

local Set = require('set')
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}

s3 = s1 + s2
print(Set.tostring(s3))
print(Set.tostring(s3 * s1))

输出为:
{1, 20, 10, 30, 50}
{20, 50, 10, 30}

支持的算术操作有

加法

__add

乘法

__mul

减法

__sub

浮点数除法

__div

整数除法

__idiv

取反操作

__unm

求模

__mod

求幂

__pow

支持的位操作有

位与

__band

位或

__bor

异或

__bxor

位取反

__bnot

左移

__shl

右移

__shr

2、关系运算元方法

支持的操作有

等于

__eq

小于

__lt

小于等于

__le

lua会将a~=b翻译成 not (a == b),a > b翻译成b < a, a >=b 翻译成 b <= a

local Set = {}

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

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

    return res
end

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

    return res
end

function Set.tostring(set)
    local l = {}
    for e in pairs(set) do
        l[#l + 1] = tostring(e)
    end

    return "{" .. table.concat(l, ", ") .. "}"
end

mt.__add = Set.union
mt.__mul = Set.intersection

mt.__le = function (a, b)
    for k 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

return Set

样例代码如下:

local Set = require('set')
s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2)
print(s1 < s2)
print(s1 >= s1)
print(s1 > s1)
print(s1 == s2 * s1)

输出为:
true
true
true
false
true

3、库定义的元方法

由库来定义元表中的字段。以集合的tostring为例

local Set = {}

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

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

    return res
end

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

    return res
end

function Set.tostring(set)
    local l = {}
    for e in pairs(set) do
        l[#l + 1] = tostring(e)
    end

    return "{" .. table.concat(l, ", ") .. "}"
end

mt.__add = Set.union
mt.__mul = Set.intersection

mt.__le = function (a, b)
    for k 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

mt.__tostring = Set.tostring

return Set

样例代码如下

local Set = require('set')
s1 = Set.new{10, 4, 5}
print(s1)

输出为:
{4, 5, 10}

如果希望保护元表,用户既不能看到也不能修改元表,可以通过设置元表的__metatable,getmetatable会返回该字段值,但是setmetatable会抛出异常。

local Set = {}

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

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

    return res
end

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

    return res
end

function Set.tostring(set)
    local l = {}
    for e in pairs(set) do
        l[#l + 1] = tostring(e)
    end

    return "{" .. table.concat(l, ", ") .. "}"
end

mt.__add = Set.union
mt.__mul = Set.intersection

mt.__le = function (a, b)
    for k 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

mt.__tostring = Set.tostring

mt.__metatable = "not your business"
return Set

样例代码如下:
 

local Set = require('set')
s1 = Set.new{10, 4, 5}
print(getmetatable(s1))
setmetatable(s1, {})

输出为:
not your business
E:\lua-5.4.2_Win64_bin\lua54.exe: test.lua:9: cannot change a protected metatable
stack traceback:
	[C]: in function 'setmetatable'
	test.lua:9: in main chunk
	[C]: in ?

4、表访问元方法

访问和修改表中缺省字段

4.1 __index元方法

当访问表中缺省字段时,如果没有提供__index元方法,直接返回 nil,否则元方法提供结果。

__index元方法不一定是函数,也可以是表。

4.2 __newindex元方法

当对一个缺省的字段赋值时,会查找__newindex元方法,解释器会调用它而不是赋值。与__index元方法类似,如果元方法是表,解释器会在表中赋值。