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<b
    return a <= b and not (b <= a)
end

-- 等于,运算符==,即集合a与b完全相等
function Set.equal(a, b)
    -- 利用lessEqual实现,两个集合互为子集时,意味着相等
    return a <= b and b <= a
end

-- 实现<、<=、==之后,在后面,将三个函数添加到mt的元表元方法上。
mt.__le = Set.lessEqual
mt.__lt = Set.less
mt.__eq = Set.equal

-- 之后进行测试
local a = Set.new { 1, 2, 3, 4 }
local b = Set.new { 3, 4 }
print(b <= 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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,733评论 0 38
  • 前言 元表对应的英文是metatable,元方法是metamethod。我们都知道,在C++中,两个类是无法直接相...
    胤醚貔貅阅读 990评论 0 2
  • 前言 元表对应的英文是metatable,元方法是metamethod。我们都知道,在C++中,两个类是无法直接相...
    BobWong阅读 1,035评论 0 9
  • 一.通用规范 这篇文章写的前端通用规则,另一篇文章写了关于react和vue的规范,react和vue开发规范文档...
    lemonzoey阅读 1,876评论 2 2
  • 刘振宁死了。 被发现的时候已经是第二天。据说是因为酒后在冬夜里睡去,活生生给冻死的。奇怪的是,人们发现他离开的时候...
    陇女凌希阅读 516评论 0 3