Lua 元表 元方法

元表

元表,就是一个普通的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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值