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 = {, , }
t2 = {,,,,} mt = {}
mt.__add = function(a, b)
local ret =
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{, , }
s2 = Set.new{, , , , }
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 = , y = , width = , height = }

mt = {}
mt.__index = function(table, key)
return Window[key]
end w = {x = , y = }
setmetatable(w, mt)
print(w.width)

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

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

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

mt = {}
mt.__index = Window w = {x = , y = }
setmetatable(w, mt)
print(w.width)

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

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

mt = {}
mt.__index = function(table, key) return Window[key] end w = {x = , y = }
setmetatable(w, mt) print(w.width) --
print(rawget(w, "width")) --nil
print(rawget(w, "x")) --

  看上面倒数第二行,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 = , y = }
setmetatable(w, mt) w.add =
print(w.add) --

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

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

mt = {}
mt.__newindex = Window w = {x = , y = }
setmetatable(w, mt) w.add =
print(w.add) --nil
print(Window.add) --

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

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

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

mt = {}
mt.__index = Window
mt.__newindex = Window w = {x = , y = }
setmetatable(w, mt) w.add =
print(w.add) --
print(Window.add) --

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

  再看下面这个例子:

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

mt = {}
mt.__index = function(table, key)
return Window[key]
end
mt.__newindex = function(table, key, value)
rawset(table, key, value)
end w = {x = , y = }
setmetatable(w, mt) w.add =
print(w.add) --
print(Window.add) -- nil

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

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

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

mt = {}
mt.__index = function(table, key)
print("going here __index")
return Window[key]
end w = {x = , y = }
setmetatable(w, mt) s = w.width    -- going here __index 访问语句会进入__index
w.width = -- 赋值语句不会进入__index,这里会导致w表添加width域

print(w.width)   --
print(rawget(w, "width"))    --
print(Window.width)     --

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

mt = {}
mt.__newindex = function(table, key, value)
print("going here __newindex")
rawset(table, key, value)
end w = {x = , y = }
setmetatable(w, mt) s = w.add -- 访问语句不会进入__newindex
w.add = -- going here __newindex 赋值语句进入__newindex
print(w.add) --
print(rawget(w, "add")) --

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

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

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

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 = , y = }
setmetatable(w, mt) s = w.width -- going here __index
w.width = -- going here __newindex
print(w.width) --
print(rawget(w, "width")) --

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

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

Metatable和Metamethod的更多相关文章

  1. Metatable和Metamethod(转)

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

  2. [Lua快速了解一下]Lua的MetaTable和MetaMethod

    MetaTable和MetaMethod是Lua中的重要的语法,MetaTable主要是用来做一些类似于C++重载操作符式的功能. 两个分数 fraction_a = {numerator=, den ...

  3. lua中的metatable和metamethod

    --元表和元方法给lua里的值设定一些操作,让我们可以对这些操作自定义 --创建一个新的table变量时,它是不存在元表的 --在Lua中,只能设置table的元表,其他类型的值的元表,只能通过C代码 ...

  4. lua metatable和metamethod元表和元方法

    Lua中提供的元表是用于帮助Lua数据变量完成某些非预定义功能的个性化行为,如两个table的相加.假设a和b都是table,通过元表可以定义如何计算表达式a+b.当Lua试图将两个table相加时, ...

  5. Lua 与 Redis

    Lua 与 Redis 标签: Java与NoSQL 从 2.6版本 起, Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis - 案例-实现访问频率限制: 实现访问者 $ip 在一定的 ...

  6. C/C++ Lua Parsing Engine

    catalog . Lua语言简介 . 使用 Lua 编写可嵌入式脚本 . VS2010编译Lua . 嵌入和扩展: C/C++中执行Lua脚本 . 将C++函数导出到Lua引擎中: 在Lua脚本中执 ...

  7. [转]LUA 学习笔记

    Lua 学习笔记 入门级 一.环境配置 方式一: 1.资源下载http://www.lua.org/download.html 2.用src中的源码创建了一个工程,注释调luac.c中main函数,生 ...

  8. 转:Lua简明教程

    需要注意的是:lua中的变量如果没有特殊说明,全是全局变量,那怕是语句块或是函数里. 这里很奇怪,为什么在函数内部声明的变量默认也是global的呢? 函数的返回值 和Go语言一样,可以一条语句上赋多 ...

  9. [2017.02.07] Lua入门学习记录

    #!/home/auss/Projects/Qt/annotated/lua -- 这是第一次系统学习Lua语言 --[[ 参考资料: 1. [Lua简明教程](http://coolshell.cn ...

随机推荐

  1. Leetcode 303 Range Sum Query - Immutable

    题意:查询一个数组在(i,j]范围内的元素的和. 思路非常简单,做个预处理,打个表就好 拓展:可以使用树状数组来完成该统计,算法复杂度为(logn),该数据结构强力的地方是实现简单,而且能完成实时更新 ...

  2. Spark安装指南

      一.Windows环境安装Spark 1.安装Java环境:jdk-8u101-windows-x64 配置环境变量: (1)增加变量名:JAVA_HOME 变量值:C:\Program File ...

  3. 为何在font-family属性中设置多个值

    通常前端开发中会对body标签中设置font-family属性多个值,例如: body{padding:0;margin:0;font-size:12px;text-align:left;font-f ...

  4. C#Winform程序如何发布并自动升级(图解)

    C#Winform程序如何发布并自动升级(图解)     有不少朋友问到C#Winform程序怎么样配置升级,怎么样打包,怎么样发布的,在这里我解释一下打包和发布 关于打包的大家可以看我的文章C# w ...

  5. lampp 在linux ubuntu下自动开机启动

    lampp 在linux ubuntu下自动开机启动 lampp在linux下是不会自动启动的.需要手工处理.如下: 假如,你的lampp安装在 /opt/lampp 目录下,那么可以如下处理: 1. ...

  6. Asp.net WebAPI 单元测试

    现在Asp.net webapi 运用的越来越多,其单元而是也越来越重要.一般软件开发都是多层结构,上层调用下层的接口,而各层的实现人员不同,一般大家都只写自己对应单元测试.对下层的依赖我们通过IOC ...

  7. Linux 中的grep命令单引号,不加任何参数以及双引号的作用

    单引号: 可以说是所见即所得:即将单引号内的内容原样输出,或者描述为单引号里面看到的是什么就会输出什么.单引号''是全引用,被单引号括起的内容不管是常量还是变量者不会发生替换. 双引号: 把双引号内的 ...

  8. SMON功能(二):合并空闲区间

    SMON的作用还包括合并空闲区间(coalesces free extent) 触发场景 早期Oracle采用DMT字典管理表空间,不同于今时今日的LMT本地管理方式,DMT下通过对FET$和UET$ ...

  9. [转] js == 与 === 的区别

    1.对于string,number等基础类型,==和===是有区别的 1)不同类型间比较,==之比较“转化成同一类型后的值”看“值”是否相等,===如果类型不同,其结果就是不等 2)同类型比较,直接进 ...

  10. selenium实战练习之:粉丝反馈表单的自动化脚本

    链接 粉丝反馈表 要求 服务质量5颗星 喜欢的内容选择各种公开课 对交流群的意见需要填写 留下自己正确的联系方式 点击提交 断言 不需要断言,能符合上面的要求成功提交就可以