Lua 元表 元方法

来源:互联网 发布:河南省大数据谷 编辑:程序博客网 时间:2024/04/29 06:14

元表

元表,就是一个普通的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: 00CA9270print(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 x1end

预定义的元方法

通常,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.vendsetmetatable(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 = mtt = { v1 = 1,v2 = 2}a = {}b = {}setmetatable(a,mt)print(getmetatable(a))     -- table: 00A0B5B0, 可以获取到元表mtmt.__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: 00AAB560local mt = {}mt.__tostring = function (  )    return "call metatable's __tostring function"endsetmetatable(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而不是本身的__tostringprint(a)            -- table: 00E3B308setmetatable(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"endlocal t = {}setmetatable(t,mt)--t中没有a这个字段,触发__index方法print(t.a)          -- __call index function

table的__index元方法默认为nil

local mt = {}       -- table: 00C1E7D8local  t = {}       -- table: 00C1E9B8print(t.__index)    -- nil t.__index = mtprint(t.__index)    -- table: 00C1E7D8

如果table没有__index元方法,但是table的元表有__index元方法,那么解释器会去查找该表的元表的__index元方法并调用,返回元表的__index元方法指向的table的字段值
分析一下下列代码为什么分别输出nil和2?

-- 代码段1local mt = {v1=1,v2=2,v3=3}local a={}setmetatable(a,mt)-- mt.__index =mtprint(a.v2)         -- nil-- 代码段2local 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]endsetmetatable(t,prototype) print(t)                    -- table: 00A8CB50print(t.width)              -- 100

__index元方法不只可以是函数,也可以是一个table,当Lua访问table中不存在的字段的时候,Lua将重新访问元方法指定的table,最终结果就是以__index=key的key取索引这张表的结果

local mt = {}local default= { a = 123 }mt.__index = defaultlocal 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")endlocal 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")endlocal 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"endlocal t = {}setmetatable(t,mt)-- local temp = t.a --t中没有a这个键,触发__index方法print(t.a)                -- __call index functionprint(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是定义了的字段endlocal t= { b = 20}setmetatable(t,mt)t.a = 10print(t.a)                        -- 10

参考:
www.voidcn.com/article/p-shdqiiwc-bbs.html
www.jellythink.com/archives/511

原创粉丝点击