元表
元表,就是一个普通的Lua table,它用于初始化的设定值在特定操作下的行为。对于值的每个操作,Lua 都将其关联上一个对应的索引,当 Lua 需要对一个值发起这些操作中的一个时, 它会去检查值的元表里是否有对应索引。如果有,则索引对应的值(元方法)将控制 Lua 怎样去执行这个操作。例如,在非数字值做加法的时候,Lua本身没有内置的处理方法,那么就会检查该值元表的__add域下的函数,如果能找到,Lua就调用这个函数来完成操作。元表除了定义一新的操作之外,还可以通过元表修改预定义元方法的行为表现,比对数字值的加法,我们也可以通过元表重载其操作,让该操作表现我们自定义的行为。
元表中的索引(或称键、字段)对应不同的事件名,通常以双下划线“__”开头(比如__add),索引对应的值(可以是table或者是函数)被称为元方法,比如事件相加,索引__add对应的完成加操作的那个函数就是元方法
getmetatable setmetatable
可以使用getmetatable来获取任何值的元表
getmatatable(x) --getmetatable返回一个table,这个table是参数x的元表
Lua在创建新的table的时候不会创建元表
local x = { 123 }
print(getmetatable(x)) -- nil
不只是table,Lua在创建其他类型的值时也不会创建元表,除了特例——字符串,对于字符串,标准的字符串程序库为字符串设置了一个元表,而其他元素在默认情况下都没有元表
print(getmetatable("Hello World")) -- table: 00CA9270
print(getmetatable(10)) -- nil
可以使用setmetatable设置任何一张表为另一个表的元表,或者替换一张表的元素
local x1 = {123}
setmetatable(x,x1)
if getmetatable(x)==x1 then
print("metatable of x is x1") -- metatable of x is x1
end
预定义的元方法
通常,Lua中每个值都有一套预定义的操作集合,表示这个值默认可以有什么操作。对于默认类型的值的每个操作,Lua都将其关联上一个预定义的键(或称索引、域名),并预定义了该索引对应的值(元方法)将控制Lua怎样去执行这个操作。
Lua中预定义的元方法有:算数类的元方法、关系类的元方法、库定义的元方法以及table访问的元方法
我们无需自己定义,就可以使用这些预定义的元方法,此外我们也可以重新定义这些Lua自定义的元方法。
算数类的元方法和关系类的元方法
对于每个算数运算符,元表都有预定义的对应域名(或称索引)与其对应
算数运算符 | 索引 |
---|---|
+ | __add(a, b) –加法 |
- | __sub(a, b) –减法 |
* | __mul(a, b) –乘法 |
/ | __div(a, b) –除法 |
% | __mod(a, b) –取模 |
^ | __pow(a, b) –乘幂 |
- | __unm(a) –相反数 |
.. | __concat(a, b) –连接 |
对于关系运算符,Lua提供了相等、小于、小于等于三种关系操作符的元方法,另外三个关系操作符,Lua没有为其提供预定义的元方法,可以通过前面三个关系运算符的取反获取。
关系运算符 | 索引 |
---|---|
== | __eq(a, b) –相等 |
< | __lt(a, b) –小于 |
<= | __le(a, b) –小于等于 |
-- 直接使用数字值预定义的‘+’索引对应的元方法
a = {v = 10}
b = {v = 20}
print(a.v+b.v) -- 30
-- 重新定义‘+’索引对应的元方法
mt = {}
mt.__add = function(a,b)
return a.v-b.v
end
setmetatable(a,mt)
setmetatable(b,mt)
print(a+b) -- -10
算数类的元方法和关系类的元方法在原理上都可以归为一类,相当于重载了运算符
对于二元操作符(比如‘+’),它有两个操作数,我们该使用哪个操作数的元方法呢?答案是:优先选择第一个操作数的元方法。
1. 如果第一个操作数有元表且元表中有该操作的索引,直接使用第一个操作数的元方法
1. 如果第一个操作数没有元表,或者元表中没有对应字段,那么就去查找第二个操作数的元表
1. 如果都没有,就报错
库定义的元方法
索引 |
---|
__metatable –用于保护元表 |
__tostring –将表转化为字符串,用于table的print |
__metatable
__metatable元方法用于保护元表,如果一个table的__metatable元方法非nil,那么我们既不能通过getmetable获取该元表,也不能通过setmetatable修改元表。
比如说mt为table a的元表,设置mt.__metatable元方法之后,不允许通过a.getmetatable的方式获取mt表,也不允许修改a的元表为其他表,(仍允许设置其他表的元表为mt),这样我们就使用__metatable元方法,将mt保护起来了
mt = { v1 = 1,v2 = 2}
mt.__index = mt
t = { v1 = 1,v2 = 2}
a = {}
b = {}
setmetatable(a,mt)
print(getmetatable(a)) -- table: 00A0B5B0, 可以获取到元表mt
mt.__metatable = "use __metatable protect mt"
print(getmetatable(a))
-- setmetatable(a,t) -- 执行这条语句会报错,因为mt.__metatable不允许修改a的元表为其他表
setmetatable(b,mt) -- 允许设置其他表的元表为mt,mt仍被保护
print(b.v1) -- 1, 可以用__index方法访问元表mt的元素值
print(getmetatable(b)) -- use __metatable protect mt, 但不可以获取元表mt
__tostring
在Lua中,我们直接print(a)(a是一个table),输出的是table的地址,如果我们要打印的是table的具体内容,那么就要改变print语句的行为。我们知道函数print(x)实际上是调用x元表的__tostring()方法来格式化输出,想让print格式化输出table的内容,我们需要自己重新定义__tostring()元方法,此时print(a)输出的就是我们格式化的内容。
重载mt的__tostring元方法,格式化输出table a
local table a={}
print(a) --table: 00AAB560
local mt = {}
mt.__tostring = function ( )
return "call metatable's __tostring function"
end
setmetatable(a,mt)
print(a) --call metatable's __tostring function
注意:print(x)时是找x元表的__tostring方法,而不是本身的__tostirng
local a = {}
-- 没有__tostring方法时,print(a)输出的table a的地址
print(a) -- table: 00E3B308
a.__tostring = function ( )
return "call a's __tostring"
end
-- a有__tostring方法,print(a)还是输出table的地址,这是因为print(x)调用的是其元表的__tostring而不是本身的__tostring
print(a) -- table: 00E3B308
setmetatable(a,a) -- 将a的元表设为它本身
-- 打印a的元表a的__tostring方法返回值
print(a) -- call a's __tostring
table访问的元方法
索引 |
---|
__index(a) –查询table |
__newindex(a) –修改table的字段 |
当我们访问table中存在的字段时,返回对应的值
local t = {a = 10}
print(t.a) -- 10
当我们访问一个table不存在的字段时,返回nil
local t = {}
print(t.a) -- nil
__index元方法
__index用于查询table中的数据,当对于一个table不存在的索引访问表中不存在的字段时,解释器会去查找一个叫__index的元方法并调用,返回元方法返回的值
__index指定的元方法可以是函数,如果是函数则以table和key作为参数调用它;如果是table,最终结果就是以__index=key的key取索引这张表的结果
local mt = {}
mt.__index = function ()
return "__call index function"
end
local t = {}
setmetatable(t,mt)
--t中没有a这个字段,触发__index方法
print(t.a) -- __call index function
table的__index元方法默认为nil
local mt = {} -- table: 00C1E7D8
local t = {} -- table: 00C1E9B8
print(t.__index) -- nil
t.__index = mt
print(t.__index) -- table: 00C1E7D8
如果table没有__index元方法,但是table的元表有__index元方法,那么解释器会去查找该表的元表的__index元方法并调用,返回元表的__index元方法指向的table的字段值
分析一下下列代码为什么分别输出nil和2?
-- 代码段1
local mt = {v1=1,v2=2,v3=3}
local a={}
setmetatable(a,mt)
-- mt.__index =mt
print(a.v2) -- nil
-- 代码段2
local mt = {v1=1,v2=2,v3=3}
local a={}
setmetatable(a,mt)
mt.__index =mt --
print(a.v2) -- 2
-- a自己的__index元方法默认为nil,由于a的元表是mt,a查找__index元方法失败就去查找mt的__index元方法,因此找到a.v2时,找到的值是mt__index指向的mt的v2字段值
__index指定的元方法可以是函数,访问表中不存在的字段时以table和key作为参数调用它
prototype = {x=0, y=0, width=100, height=100}
t = {}
prototype.__index = function ( table,key )
print("in",table,"can not find key ,call __index!") -- in table: 00A8CB50 can not find key ,call __index!
return prototype[key]
end
setmetatable(t,prototype)
print(t) -- table: 00A8CB50
print(t.width) -- 100
__index元方法不只可以是函数,也可以是一个table,当Lua访问table中不存在的字段的时候,Lua将重新访问元方法指定的table,最终结果就是以__index=key的key取索引这张表的结果
local mt = {}
local default= { a = 123 }
mt.__index = default
local t = {}
setmetatable(t,mt)
--t中没有a这个键,触发__index方法
print(t.a) -- 123
__newindex元方法
__newindex与__index类似,__newindex用于更新table中的数据,而__index用于查询table中的数据,给表中不存在的字段赋值时,与__index类似,__newindex元方法是函数时,以table key以及value作为参数传入并调用该函数进行赋值。(tip:使用后面提到的rawset的进行赋值,如果使用__newindex为未定义字段进行赋值会陷入无限循环)
-- 没有__newindex元方法
local mt = {}
local t = {}
setmetatable(t,mt)
t.a =10 --新增字段
print(t.a) -10
-- 定义了__newindex元方法
local mt = {}
mt.__newindex = function ()
print("you are trying to assign a none exist word")
end
local t = {}
setmetatable(t,mt)
t.a =10 --新增字段
print(t.a) -- you are trying to assign a none exist word
rawset和rawget
如果不想从__index查询值,不想用__index更新table,可以使用rawset和rawget,他们是不考虑元表的简单访问
rawset(table,key,value) 不考虑元表,设置table[key] = value
local mt = {} mt.__newindex = function () print("you are trying to assign a none exist word") end local t = {} setmetatable(t,mt) rawset(t,"a",10) -- rawset绕过了__newindex方法给table更新值 print(t.a) -- 10
rawset(table,key) 不考虑元表,获取table[key]值
local mt = {} mt.__index = function () return "__call index function" end local t = {} setmetatable(t,mt) -- local temp = t.a --t中没有a这个键,触发__index方法 print(t.a) -- __call index function print(rawget(t,"a")) -- nil --rawget绕过了__index方法,进行不考虑元表的简单访问
在__newindex元方法中,什么时候使用rawset?什么时候可以直接赋值?
如果对未定义字段进行赋值,在__newindex元方法中对其赋值只能使用rawset不能使用‘=’赋值,否则又会访问__newindex,陷入死循环。
-- 一旦有了__newindex,Lua就不再做最初的赋值操作,如果有必要在元方法内部可以使用rawset来做赋值 local mt = {} mt.__newindex = function ( table,key,value ) print("__call newindex") -- __call newindex print(table,key,value) -- table: 00B9CAD8 a 10 -- table.key = value; --这样不行,因为key也是非定义字段,因此又会访问__newindex,进入死循环 rawset(table,key,value) -- table.b = key -- 可以直接赋值,因为b是定义了的字段 end local t= { b = 20} setmetatable(t,mt) t.a = 10 print(t.a) -- 10
参考:
www.voidcn.com/article/p-shdqiiwc-bbs.html
www.jellythink.com/archives/511