开发者社区> jamesjxiao> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

Metatable和Metamethod

简介:   Metatable和Metamethod是用来干啥的?它们可以使得表a和b的表达式“a + b”变得有意义,其中metatable使两个不相关的表a和b之间可以进行操作,而操作的具体行为比如说"+"由metamethod来具体定义。
+关注继续查看

  Metatable和Metamethod是用来干啥的?它们可以使得表a和b的表达式“a + b”变得有意义,其中metatable使两个不相关的表a和b之间可以进行操作,而操作的具体行为比如说"+"由metamethod来具体定义。

  Metatable和Metamethod大多数地方都翻译成“元表”和“元函数”,这是一种直译,相当不直观。根据Metatable的用法,我倾向于将Metatable翻译成关联表,Metamethod翻译成关联函数。通过给两个table设置Metatable可以使两个table产生联系,然后对两个table进行一些操作,具体的操作行为由Metamethod来定义。下面的例子中,在对表t1和t2设置关联表mt,并在mt中定义关联函数__add后,就可以对这两个表进行"+"相加操作了。

t1 = {1, 2, 3}
t2 = {4,5,6,7,8}

mt = {}
mt.__add = function(a, b)
    local ret = 0
    for _, v in pairs(a) do
        ret = ret + v
    end
    for _, v in pairs(b) do
        ret = ret + v
    end
    return ret
end

setmetatable(t1, mt)
setmetatable(t2, mt)
print(t1 + t2)

从上面的代码中可以看到关联表就是一个表,而关联函数就是一个函数。当碰到表达式"t1+t2"时,Lua首先查找他们的关联表,找到关联表mt后,会在mt中找与相加操作对应的关联函数__add,找到__add后就将t1和t2作为参数来执行该函数,最后返回结果。

下面是一个使用关联表来对集合(用table实现的集合)进行操作的示例,实例中定义了集合的并集、交集、比较等运行:

Set = {}

--专门用来作为metatable,定义在Set里面以免影响外部的命名空间
Set.mt = {}    

--转化为string
Set.tostring = function (set)
    local s = "{"
    local sep = " "
    for e in pairs(set) do
        s = s .. sep .. e
        sep = ", "
    end
    return s.."}"
end

--打印
Set.print = function(s)
    print(Set.tostring(s))
end

Set.mt.__tostring = Set.tostring

--新建一个集合
Set.new = function (t)
    local set = {}
    setmetatable(set, Set.mt)        --指定所创建集合的metatable
    for _, l in ipairs(t) do set[l] = true end
    return set
end

--并集
Set.union = function (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

--给metatable增加__add函数(metamethod),当Lua试图对两个集合相加时,将调用这个函数,以两个相加的表作为参数
Set.mt.__add = Set.union

--交集
Set.intersection = function (a,b)
    local res = Set.new{}
    for k in pairs(a) do
        res[k] = b[k]
    end
    return res
end
--定义集合相乘操作为求交集
Set.mt.__mul = Set.intersection

--先定义"<="操作,然后基于此定义"<"和"="
Set.mt.__le = function (a, b)
    for k in pairs(a) do
        if not b[k] then return false end
    end
    return true
end

--小于
Set.mt.__lt = function(a, b)
    return a<=b and not (b <= a)
end

--等于
Set.mt.__eq = function(a, b)
    return a <= b and b <= a
end

--测试
s1 = Set.new{1, 2, 3}
s2 = Set.new{10, 20, 30, 40, 50}
print(getmetatable(s1))
print(getmetatable(s2))
s3 = s1 + s2    --等同于Set.union(s1, s2)
print(s3)
print(s3 * s2)

print(s1 <= s3)
print(s1 == s3)
print(s1 < s3)
print(s1 >= s3)
print(s1 > s3)

--起保护作用,getmetatable将返回这个域的值,而setmettable将会出错
Set.mt.__metatable = "not your business"

print(getmetatable(s1))
setmetatable(s1, {})

  当Lua试图对两个表进行相加时,他会检查两个表是否有一个表有Metatable,并且检查Metatable是否有__add域。如果找到则调用这个__add函数(所谓的Metamethod)计算结果。当两个表有不同的Metatable时,以谁的为准呢?Lua选择metamethod的原则:

  (1)如果第一个参数存在带有__add域的metatable,Lua使用它作为metamethod,和第二个参数无关;

  (2)否则,第二个参数存在带有__add域的metatable,Lua使用它作为metamethod;

  (3)否则,报错。

  Lua中定义的常用的Metamethod如下所示:

  算术运算符的Metamethod:__add(加运算)、__mul(乘)、__sub(减)、__div(除)、__unm(负)、__pow(幂),__concat(定义连接行为)。

  关系运算符的Metamethod:__eq(等于)、__lt(小于)、__le(小于等于),其他的关系运算自动转换为这三个基本的运算。

  库定义的Metamethod:__tostring(tostring函数的行为)、__metatable(对表getmetatable和setmetatable的行为)。

  注意:__metatable不是函数,而是一个变量。假定你想保护你的集合使其使用者既看不到也不能修改metatables。如果你对metatable设置了__metatable的值getmetatable将返回这个域的值,而调用用setmetatable将会出错:

  注意:相等比较从来不会抛出错误,如果两个对象有不同的metamethod,比较的结果为false,甚至可能不会调用metamethod。这也是模仿了Lua的公共的行为,因为Lua总是认为字符串和数字是不等的,而不去判断它们的值。仅当两个有共同的metamethod的对象进行相等比较的时候,Lua才会调用对应的metamethod。

  print总是调用tostring来格式化它的输出,tostring会首先检查对象是否存在一个带有__tostring域的metatable。

   表相关的Metamethod:

  (1)__index metamethod:在继承中使用较多。当访问表不存在的一个域时,会触发Lua解释器去查找__index metamethod,如果不存在,则返回nil,否则由__index metamethod返回结果。

Window = {x = 0, y = 0, width = 100, height = 100}

mt = {}
mt.__index = function(table, key)
    return Window[key]
end

w = {x = 10, y = 20}
setmetatable(w, mt)
print(w.width)

  可以看到w没有width域,但有关联表mt,且关联表有__index,因此w.width会触发mt.__index的调用(Lua会将w作为第一个参数、width作为第二个参数来调用该函数)。

  __index除了作为一个函数,还可以直接作为一个表来使用当__index是一个表时,Lua会直接在这个表中查找width域。因此代码也可以像这样来写:

Window = {x = 0, y = 0, width = 100, height = 100}

mt = {}
mt.__index = Window

w = {x = 10, y = 20}
setmetatable(w, mt)
print(w.width)

   rawget(table, index)函数获取表中指定域的值,该函数可以绕过metamethod,直接返回表的域信息,看下面这个例子:

Window = {x = 0, y = 0, width = 100, height = 100}

mt = {}
mt.__index = function(table, key) return Window[key] end

w = {x = 10, y = 20}
setmetatable(w, mt)

print(w.width)                    --100
print(rawget(w, "width"))        --nil
print(rawget(w, "x"))            --10

  看上面倒数第二行,rawget(w, "width")访问不存在的域不会触发查找__index。

  (2)__newindex metamethod:__newindex metamethod用来对表更新,__index则用来对表访问。

  当给表的一个不存在的域赋值时(比如w.add = 1),会触发Lua查找__newindex,如果不存在__newindex,则像一般的赋值行为一样导致表添加了一个域。

  (1)不存在__newindex,则像一般的赋值行为一样导致表添加了一个域(w多了一个域add,值为1)

  (2)存在__newindex,则不进行赋值操作,而是由__newindex拦截了赋值操作,并且将(table、域名、值)作为参数调用__newindex。

  也就是说,__newindex可以使得任何对表的添加元素的行为都要经过__newindex,这确实是一个很好的把关。

  rawset(t, k, v)函数也是一个等同于赋值的操作(w.add = 1相当于rawset(w, "add", 1)),但调用该函数可以绕过metamethod,即不会导致__newindex的调用:

mt = {}
mt.__newindex = function(table, key, value)
    rawset(table, key, value)        --这里不能写成table.key = value;因为这个给不存在域的赋值操作又会导致__newindex的调用,因而陷入死循环
end

w = {x = 10, y = 20}
setmetatable(w, mt)

w.add = 1
print(w.add)            -- 1

   和__index一样,__newindex也可以是一个表,如果__newindex是一个表,会导致对指定的那个表而不是原始的表进行赋值操作。

Window = {x = 0, y = 0, width = 100, height = 100}

mt = {}
mt.__newindex = Window

w = {x = 10, y = 20}
setmetatable(w, mt)

w.add = 1
print(w.add)                --nil
print(Window.add)            --1

  赋值操作导致Windows添加了一个元素add,而w不影响。

  当__index和__newindex混合使用时,一定要注意区分每个行为都干了什么事情:

Window = {x = 0, y = 0, width = 100, height = 100}

mt = {}
mt.__index = Window
mt.__newindex = Window

w = {x = 10, y = 20}
setmetatable(w, mt)

w.add = 1
print(w.add)                --1
print(Window.add)            --1

  __newindex为表时,w.add=1表示给Window添加了add,但通过__index,w也能访问到Window的add。

  再看下面这个例子:

Window = {x = 0, y = 0, width = 100, height = 100}

mt = {}
mt.__index = function(table, key)
    return Window[key]
end
mt.__newindex = function(table, key, value)
    rawset(table, key, value)
end

w = {x = 10, y = 20}
setmetatable(w, mt)

w.add = 1
print(w.add)            -- 1
print(Window.add)        -- nil

  __newindex为函数时,w.add直接给w添加了add域,但Window并不存在add。所以结论是:当__newindex是函数时,给目标表w添加域;当__newindex是表时,给指向表添加域。

   关于__index和__newindex一定要注意区分,什么时候进入__index,什么时候进入__newindex:

Window = {x = 0, y = 0, width = 100, height = 100}

mt = {}
mt.__index = function(table, key)
    print("going here __index")
    return Window[key]
end

w = {x = 10, y = 20}
setmetatable(w, mt)

s = w.width                            -- going here __index 访问语句会进入__index
w.width = 1                              -- 赋值语句不会进入__index,这里会导致w表添加width域
print(w.width)                          -- 1
print(rawget(w, "width"))               -- 1
print(Window.width)                    -- 100

  访问语句进入__index,赋值语句不进入__index。

mt = {}
mt.__newindex = function(table, key, value)
    print("going here __newindex")
    rawset(table, key, value)
end

w = {x = 10, y = 20}
setmetatable(w, mt)

s = w.add                             -- 访问语句不会进入__newindex
w.add = 1                            -- going here __newindex 赋值语句进入__newindex
print(w.add)                        -- 1
print(rawget(w, "add"))            -- 1

  赋值语句进入_newindex,访问语句不进入__newindex。

   结合上面这两句话就很容易理解下面这个例子了:

Window = {x = 0, y = 0, width = 100, height = 100}

mt = {}
mt.__index = function(table, key)
    print("going here __index")
    return Window[key]
end
mt.__newindex = function(table, key, value)
    print("going here __newindex")
    rawset(table, key, value)
end

w = {x = 10, y = 20}
setmetatable(w, mt)

s = w.width                            -- going here __index
w.width = 1                            -- going here __newindex
print(w.width)                        -- 1
print(rawget(w, "width"))            -- 1

  倒数第三条语句w.width = 1不会进入__index,所以会导致给w表添加新的域width。也就是说__index逻辑不会影响__newindex的判断,虽然__index可以访问到域width,但__newindex依然仍未w没有width域。

  这些概念非常的绕,而且Lua是一种弱类型化语言,所以对于很多概念的具体行为你一定要自己多加测试,不能够想当然。

 

 

 

 

 

 

 

 

 

 

 

 

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
写给高效能产品经理的七个建议
阿里云智能高级产品专家「戚亚东(花名:才云)」通过日常的工作观察和面试经验,总结了关于高效能产品经理的七个建议。
0 0
5个人如何1年交付了120+项目?分享我在阿里云做交付的工作手记
谨以此文,分享一些我加入阿里云后,我和我所在团队的成长经历。这里既有我个人如何从程序员成长为一个技术经理,也有我的团队如何把事情越做越大的过程和思考,希望能够帮到有需要的人。
0 0
表格存储 Tablestore 十年发展总结
表格存储在 2009 年阿里云成立之初即立项研发,受 Google Bigtable 的启发我们决定自研一个类似的分布式表存储。在 2012 年 1 月表格存储云服务正式上线公测,所以到今年是作为云服务上线满 10 年。这篇文章接下来会先整体介绍下表格存储 Tablestore,之后会分享下在技术层面产品这几年的功能演进、技术架构演进以及稳定性优化相关的工作,以及在业务层面我们定义的核心应用场景和一些典型案例。
0 0
一文梳理Code Review方法论与实践总结
作为卓越工程文化的一部分,Code Review其实一直在进行中,只是各团队根据自身情况张驰有度,松紧可能也不一,这里简单梳理一下CR的方法和团队实践。
0 0
我理解的测试开发与实践总结——新人篇
写这篇文章的目的是为了能够更好的帮助刚入职的新人了解这个岗位和自己的工作,也想谈谈自己工作一年来对这个领域的了解程度,做一个小小总结吧~
0 0
轻松使用阿里达摩院开源在魔搭社区上的CLUE语义匹配模型
本文将介绍达摩院NLP团队在魔搭社区(ModelScope)上开源的语义匹配模型及其使用方法。
0 0
React开发的设计模式及原则
设计模式是最常见的,通用问题的可复用解决方案的归纳总结,通常被认为是解决该类问题的最佳实践,使用设计模式能帮助我们写出更容易维护,更健壮的代码。设计模式有很多,通常它们都会遵循一些共同的设计原则,接下来我们一起回顾下React社区里出现过的一些设计模式,以及它们所遵循的设计原则。
0 0
谈谈《流浪地球2》中如何正确的“重启互联网”
DNS是互联网域名和IP转换,寻址调度的重要的基础设施,是互联网中少有的相对中心化的环节。《流浪地球2》中有一个重启互联网,重启(DNS)根服务器的情节,过程惊心动魄,但是从互联网专业视角看仍有一些小瑕疵。观影后笔者撰写小文,聊聊如何正确的“重启互联网”。
0 0
整理一下VR&AR的现状以及未来
想要和大家聊一下VR和AR在场景中的实现,和我自己的一点看法。
0 0
设计稳定的微服务系统时不得不考虑的场景
我们的生产环境经常会出现一些不稳定的情况,如: 1、大促时瞬间洪峰流量导致系统超出最大负载,load 飙高,系统崩溃导致用户无法下单 2、“黑马”热点商品击穿缓存,DB 被打垮,挤占正常流量 3、调用端被不稳定服务拖垮,线程池被占满,导致整个调用链路卡死 这些不稳定的场景可能会导致严重后果。大家可能想问:如何做到均匀平滑的用户访问?如何预防流量过大或服务不稳定带来的影响?
0 0
+关注
jamesjxiao
游戏领域耕耘者~
文章
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载