Lua极简入门(八)——元表metatable

Lua使用元表来定义对table或者用户自定义数据的操作。在很多情况下,可以简化或者提高table或用户自定义数据的操作方式,比如对于+法操作,table是不具备该功能,两个table直接相加将发生错误

t = { 1, 2, 3 }
t1 = { 10, 20, 30, 40 }
t2 = t + t1
-->> attempt to perform arithmetic on a table value (global 't')

对于加法操作,如上述中的t + t1,在这行该表达式时,由于两者都是table,因此会先检查是否具有元表,如果有元表,则从元表中检查是否具有__add方法,如果有则调用该方法执行t+t1__add及其指向的函数,称为元方法,实现这种形式,类似于C#中的算数表达式重载。

Lua通过setmetatable进行元表设置,通过getmetatable获取元表信息。在对table进行元编程时,一般需要通过setmetatable进行元方法的设置。当对不同table实例设置同一个元表时,这些实例的元表将相同。

a = {}
t = { 1, 2, 3 }
t1 = { 10, 20, 30, 40 }
-- 刚声明table时,不具备元表。需要设置,a即为元表
print(getmetatable(t))
print(getmetatable(t1))
-->> nil
-->> nil
setmetatable(t, a)
print(getmetatable(t))
-->> table: 0000000000eca040
print(getmetatable(t1))
-->> nil
setmetatable(t1, a)
print(getmetatable(t1))
-->> table: 0000000000eca040
  • 对数组实现+元函数
-- 声明元表
local mt = {}

-- 对元表添加_add方法,用于描述+法操作
mt.__add = function(a, b)
    local res = {}
    -- 由于是数组,因此只需要记录值即可,丢弃索引值
    for _, v in pairs(a) do
        res[v] = true
    end

    for _, v in pairs(b) do
        res[v] = true
    end

    return res
end

-- 打印table
function printTable(t)
    -- 读取所有的key即可,这里是特殊操作,上面只将数组的值记录到了索引位置
    local res = {}
    for k in pairs(t) do
        res[#res + 1] = k
    end
    print("{" .. table.concat(res, ",") .. "}")
end

local t1 = { 1, 2, 3 }
local t2 = { 10, 20, 30 }

setmetatable(t1, mt)
local t3 = t1 + t2
printTable(t3)

在这个示例中,实现了元表的__add方法,其接收两个table参数,这个示例只能处理数组,不是通用table相加操作;因此在计算两个table相加时,为了简单,只记存储了value值,只要在新的table中key存在的就是两个table相加的结果。提供了测试打印table的函数,在这个里面直接取key存储新table,按数组序列存储,并将其组织成字符串,以,分割。

最后在计算前,指定了t1同元表mt相同,Lua在计算+时,先判断第一个参数t1,在其元表中发现有__add方法,则直接调用该方法执行相加操作;如果t1元表中不具备__add方法,则检查第二个变量t2,如果两者都不具备__add方法,则程序异常。

  • Table元表支持的元方法
元方法 描述
__add 加,运算符+
__mul 乘,运算符*
__sub 减,运算符-
__div 除,运算符/
__unm 取反,相反数
__mod 模,运算符%
__pow 乘幂,运算符^
__concat 连接操作,..

该表格描述了Table支持的运算符元方法,可以根据业务需求进行定制。上面针对__add方法对数组进行了实现,不过上述实现方法,较为复杂,每次需要重新定义,并设置变量元表同__add的元表一致。这里以运算符*和运算符-为示例进行重新定义。

Set = {}
-- table的metatable
local mt = {}

-- 实现创建新集合的方法
function Set.new(lst)
    local set = {}
    setmetatable(set, mt)
    for _, v in ipairs(lst) do
        set[v] = true
    end

    return set
end

-- 并集,运算符+
function Set.union(a, b)
    local res = Set.new {}
    for key in pairs(a) do
        res[key] = true
    end
    for key in pairs(b) do
        res[key] = true
    end

    return res
end

-- 交集,即运算符*,同时在a,b中都存在的元素
function Set.intersection(a, b)
    local res = Set.new {}

    for key in pairs(a) do
        res[key] = b[key]
    end

    return res
end

-- 补集,即运算符-,b在a中的相对补集,a-b,属于a但不属于b的元素
function Set.complement(a, b)
    local res = Set.new {}

    for key in pairs(a) do
        if not b[key] then
            res[key] = true
        end
    end

    return res
end

-- 连接,实现连接符
function Set.toString(a)
    local lst = {}
    for key in pairs(a) do
        lst[#lst + 1] = key
    end
    return table.concat(lst, ",")
end

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

local a = Set.new { 10, 20, 30, 40 }
local b = Set.new { 30, 40, 50, 60 }
local d = a * b
print("{" .. Set.toString(d) .. "}")
-->> {40,30}
local e = a - b
print("{" .. Set.toString(e) .. "}")
-->> {20,10}

在本例中,创建了一个新的对象,将创建数组,指定元表等方法全部集成,同时实现了交集和补集函数。在本例中,主要是利用了数组数值作为索引值,同时索引位置设置为nil时,将删除一个元素的特性;在实现table转字符串时,又重新创建了数组,从索引位置1开始存储数据。最后将交集、补集方法指定给了mt元表的__add__sub等方法,之后就可以使用运算符进行操作table。

  • 关系运算符元方法
元方法 描述
__eq 等于,==
__lt 小于,<
__le 小于等于,<=

Lua对于关系运算的元方法只定义了这三种,其他的不等于~=、大于>、大于等于>=使用上述三种进行转换。下述实现Table的关系运算符,虽然不实现>>=但根据Lua的设计,会使用<<===等实现,仍然可用。在本示例中,仍然使用前一节的Set扩展,在该对象上继续实现关系运算符。

-- 小于等于,集合的关系运算符,<=,对于集合,一般a<=b,即a是b的子集
function Set.lessEqual(a, b)
    for i in pairs(a) do
        if not b[i] then
            return false
        end
    end

    return true
end

-- 小于,集合关系运算符<。
function Set.less(a, b)
    -- 利用lessEqual实现,首先a是b的子集,同时,b不能是a的子集,即排除了b==a的情况,剩余a> true
print(b < a)
-->> true
print(a <= a)
-->> true
print(a >= a)   -- 可以使用>、>=等运算符
-->> true
-- 因为在Set的算数运算符基础上实现关系运算符,因此可以使用关系运算符和算数运算符混合使用
print(b == a * b)   -- a * b取集合a、b交集,由于b是a的子集,则a * b即为 {3,4},即获取到b
-->> true
  • 元表的其他元方法
元方法 描述
__tostring 转字符串
__call 函数调用
__index 调用索引值
__newindex 赋值
  1. __tostring

对于table在转为字符串的情况比较少,一般用于跟踪。仍然在前面的Set对象上进行测试,之前Set方法已经包含了一个toString方法,对此方法进行部分改造。

-- 连接,实现连接符
function Set.toString(a)
    local lst = {}
    for key in pairs(a) do
        lst[#lst + 1] = key
    end
    return "{" .. table.concat(lst, ",") .. "}" -- 实现{}的拼接
end
-- 将该方法添加到元方法__tostring
mt.__tostring = Set.toString

-- 使用
local a = Set.new {1, 2, 3}
print(a)
-->> {1,2,3}
  1. __call

__call元方法,可以是的一个table像函数一样调用一个新的值。实现一个table和另外一个table的元素数值相加,使用__call实现。仍在Set对象上实现。

-- __call调用,第一个参数为当前table
function Set.summation(t, ...)
    local sum = 0
    for k in pairs(t) do
        sum = sum + k
    end

    for _, v in pairs { ... } do
        sum = sum + v
    end

    return sum
end
-- 将mt的元方法指向summation方法
mt.__call = Set.summation
-- 可以调用__call方法
local a = Set.new { 1, 2, 3 }
local sum = a(4, 5) -- 此处table可以像一个函数一样调用,参数为新的table或者可变参数
print(sum)
-->> 15

注意,本例中使用Set对象,Set在初始化时,将数组的值存储在新的数组的索引处。第二个参数为可变参数,如果要接收为table需要调整代码。

  1. __index

当访问一个table中的字段时,Lua会先从table中查找该字段,如果存在,则返回该字段的值;如果没有,则检查该table是否具有元表,如果没有元表,则返回nil;如果有元表,则会从元表中查找__index元方法,如果没有该元方法,返回nil;如果有__index元方法,则从该方法中查找指定字段。__index方法可以返回一个函数、也可以返回一个table。因此查找该方法,也需要分两种情况。

local a = { name = "Lua", version = "5.3" }
local mt = setmetatable({}, { __index = a })    -- __index使用元表实现
print(mt.name)
-->> Lua
-- 采用方法实现__idnex,效果和table差不多,只不过是方法时,直接返回方法的返回值
local mt2 = setmetatable({}, {
    __index = function(mt2, key)
        return a[key]
    end
})
print(mt2.name, mt2.score)
-->> Lua    nil

在该例中,mt并不具有name属性,但是在声明时将其元方法__index指向了表a,因此当访问name字段时,mt中没有,但是mt具有元表,并且实现了__index元方法,因此会继续调用__index,此时是一个table,将会和最初查找mt的流程一致,先从table中找,后找元表__index元方法。

Lua的这种设计对于面向对象的继承,有着重要的作用。一个动物具有嘴、手、脚等肢体,这些肢体是构成动物的基本元素,但是每个具体动物的嘴、手、脚的数量可能不同。设计一个对象,实现基础的嘴、手、脚属性,在创建一个具体的动物时,指定属性的具体数值。

local animal = {}
animal.prototype = {
    leg = 0,
    mouth = 0,
    hand = 0
}
-- 创建元表
local mt = {}

function animal.new(prototype)
    -- 设置元表
    setmetatable(prototype, mt)
    -- 实现元表的__index
    mt.__index = animal.prototype

    return prototype
end

local person = animal.new({leg = 2, mouth = 1, hand = 2})
print("腿:" .. person.leg .. ",嘴:" .. person.mouth .. ",手:" .. person.hand)
-->> 腿:2,嘴:1,手:2
  1. __newindex

__newindex__index是类似的元方法,__newindex是用于对table的更新,__index是用来查询。当对一个table进行赋值时,会先查找是否具有__newindex元方法,如果有,Lua则会调用该元方法,如果没有,则执行赋值。如果__newindex是一个table,则调用赋值时,会直接将值插入到__newindex指向的table。

local animal = {}
-- 设置__newindex
local mt = setmetatable({ mouth = 1 }, { __newindex = animal })
print(mt.mouth)
-- 设置手的数量,因为具有__newindex,因此该赋值会将数据插入到animal中
mt.hand = 2
print(mt.hand, animal.hand)
-- 对原有的mouth设置
mt.mouth = 2
print(mt.mouth, animal.mouth)

你可能感兴趣的:(Lua极简入门(八)——元表metatable)