Lua之元表和元方法
Lua之元表和元方法
- 元表
- 元方法
- __add
- __index
- __newindex
- ps
- 忽略元表:rawget和rawset
- rawget
- rawset
元表
Lua中的每个值都可以有一个元表!这个元表就是一个普通的Lua表,它用于定义原始值在特定操作下的行为。
如果你想改变一个值在特定操作下的行为,你可以在它的元表中设置对应域。例如当你对非数字值做加操作时,Lua会检查该值的元表中的"__add"域下的函数。如果能找到,Lua则调用这个函数来完成加这个操作。
在Lua中,你不可以改变表以外其它类型的值的元表(除非你使用调试库),若想改变这些非表类型的值的元表,请使用C API。
表和完全用户数据有独立的元表(当然,多个表和用户数据可以共享同一个元表)。其它类型的值按类型共享元表,也就是说所有的数字都共享同一个元表,所有的字符串共享另一个元表等等。默认情况下,值是没有元表的,但字符串库在初始化的时候为字符串类型设置了元表。
元表决定了一个对象在数学运算、位运算、比较、连接、取长度、调用、索引时的行为。元表还可以定义一个函数,当表对象或用户数据对象在垃圾回收时调用它。
使用getmetatable函数来获取任何值的元表,使用setmetatable来替换一张表的元表。
local testStr = "hello, fightsyj"
local testTbl = {name = "fightsyj", age = 666}
-- getmetatable获取对象的元表
print("testStr`s metatable is ->", getmetatable(testStr)) -- 字符串库在初始化的时候为字符串类型设置了元表
print("testTbl`s metatable is ->", getmetatable(testTbl))
local testMetaTbl = {}
-- setmetatable设置元表,返回设置元表之后的对象
local finalTbl = setmetatable(testTbl, testMetaTbl)
dump(finalTbl, "finalTbl->")
print("testTbl`s metatable is ->", getmetatable(testTbl))
--[[
testStr`s metatable is -> table: 00AA9780
testTbl`s metatable is -> nil
- "finalTbl->" = {
- "age" = 666
- "name" = "fightsyj"
- }
testTbl`s metatable is -> table: 00DF1E60
]]
元方法
元表中的键对应着不同的事件名,键关联的那些值被称为元方法。在上面那个例子中引用的事件为"add",完成加操作的那个函数就是元方法。
接下来会给出一张元表可以控制的事件的完整列表。每个操作都用对应的事件名来区分。每个事件的键名用加有"__“前缀的字符串来表示。例如"add"操作的键名为字符串”__add"。需要注意的是Lua从元表中直接获取元方法,访问元表中的元方法永远不会触发另一次元方法。下面的代码模拟了Lua从一个对象obj中获取一个元方法的过程:
rawget(getmetatable(obj) or {}, “__” … event_name)
对于一元操作符(取负、求长度、位反),元方法调用的时候,第二个参数是个哑元,其值等于第一个参数。这样处理仅仅是为了简化Lua的内部实现(这样处理可以让所有的操作都和二元操作一致),这个行为有可能在将来的版本中移除。(使用这个额外参数的行为都是不确定的)
元方法 | 描述 |
---|---|
add | + 操作。如果任何不是数字的值(包括不能转换为数字的字符串)做加法,Lua就会尝试调用元方法。首先Lua检查第一个操作数(即使它是合法的),如果这个操作数没有为"__add"事件定义元方法,Lua就会接着检查第二个操作数。一旦Lua找到了元方法,它将把两个操作数作为参数传入元方法,元方法的结果(调整为单个值)作为这个操作的结果。如果找不到元方法,将抛出一个错误。 |
sub | - 操作。行为和"add"操作类似。 |
mul | * 操作。行为和"add"操作类似。 |
div | / 操作。行为和"add"操作类似。 |
mod | % 操作。行为和"add"操作类似。 |
pow | ^ (次方)操作。行为和"add"操作类似。 |
unm | - (取负)操作。行为和"add"操作类似。 |
idiv | // (向下取整除法)操作。行为和"add"操作类似。 |
band | & (按位与)操作。行为和"add"操作类似,不同的是Lua会在任何一个操作数无法转换为整数时尝试取元方法。 |
bor | | (按位或)操作。行为和"band"操作类似。 |
bxor | ~ (按位异或)操作。行为和"band"操作类似。 |
bnot | ~ (按位非)操作。行为和"band"操作类似。 |
shl | << (左移)操作。行为和"band"操作类似。 |
shr | >> (右移)操作。行为和"band"操作类似。 |
concat | .. (连接)操作。行为和"add"操作类似,不同的是Lua在任何操作数既不是一个字符串,也不是数字(数字总能转换为对应的字符串)的情况下尝试元方法。 |
len | # (取长度)操作。如果对象不是字符串Lua会尝试它的元方法。如果有元方法,则调用它并将对象以参数形式传入,而返回值(被调整为单个)则作为结果。如果对象是一张表且没有元方法,Lua使用表的取长度操作。其它情况,均抛出错误。 |
eq | == (等于)操作。和 “add” 操作行为类似,不同的是Lua仅在两个值都是表或都是完全用户数据,且它们不是同一个对象时才尝试元方法。调用的结果总会被转换为布尔量。 |
lt | < (小于)操作。和"add"操作行为类似,不同的是Lua仅在两个值不全为整数也不全为字符串时才尝试元方法。调用的结果总会被转换为布尔量。 |
le | <= (小于等于)操作。和其它操作不同,小于等于操作可能用到两个不同的事件。首先,像"lt"操作的行为那样,Lua在两个操作数中查找"__le"元方法。如果一个元方法都找不到,就会再次查找"__lt"事件,它会假设a <= b等价于not (b < a)。而其它比较操作符类似,其结果会被转换为布尔量。 |
index | 索引table[key]。当table不是表或是表table中不存在key这个键时,这个事件被触发。此时,会读出table相应的元方法。尽管名字取成这样,这个事件的元方法其实可以是一个函数也可以是一张表。如果它是一个函数,则以table和key作为参数调用它。如果它是一张表,最终的结果就是以key取索引这张表的结果。(这个索引过程是走常规的流程,而不是直接索引,所以这次索引有可能引发另一次元方法。) |
newindex | 索引赋值table[key] = value。和索引事件类似,它发生在table不是表或是表table中不存在key这个键的时候。此时,会读出table相应的元方法。同索引过程那样,这个事件的元方法既可以是函数,也可以是一张表。如果是一个函数,则以table、key以及value为参数传入。如果是一张表,Lua对这张表做索引赋值操作。(这个索引过程是走常规的流程,而不是直接索引赋值,所以这次索引赋值有可能引发另一次元方法。)一旦有了"newindex"元方法,Lua就不再做最初的赋值操作。(如果有必要,在元方法内部可以调用rawset来做赋值。) |
call | 函数调用操作func(args)。当Lua尝试调用一个非函数的值的时候会触发这个事件(即func不是一个函数)。查找func的元方法,如果找得到,就调用这个元方法,func作为第一个参数传入,原来调用的参数(args)后依次排在后面。 |
mode | 弱表属性,赋予一张表弱引用属性。 |
gc | 在对象被GC的时候,会先调用元表里面的"gc"域。 |
tostring | 当调用tostring(obj)的时候,会先查找obj的元方法中的"__tostring",如果有就调用,没有就会打印obj的内存位置。 |
pairs | 迭代器的元方法,在执行pairs(t)的时候,会先找表t的元方法"__pairs",如果有就以t为参数调用他,如果没有,就返回三个值next函数,t已经nil。 |
metatable | 函数setmetatable和getmetatable会触发"__metatable"元方法。当Lua中的值拥有该元方法时,getmetatable就会返回这个字段的值,而setmetatable则会引发一个错误。因此我们可以使用"__metatable"元方法来保护任意值的元表,这样值的元表就不会被随意修改了。 |
下面着重介绍一下__add、__index和__newindex这三个元方法。
__add
__add类似于C++中的运算符重载,对算术运算符+进行重载,重新定义+的操作行为!
local testTbl1 = {name = "fightsyj", age = 666}
local testTbl2 = {sex = "Male"}
-- local testTbl3 = testTbl1 + testTbl2 -- 报错
local testMetaTbl = {}
testMetaTbl.__add = function(tbl1, tbl2)
for key, value in pairs(tbl2) do
tbl1[key] = value
end
return tbl1
end
setmetatable(testTbl1, testMetaTbl)
local testTbl3 = testTbl1 + testTbl2
dump(testTbl3)
--[[
- "<var>" = {
- "age" = 666
- "name" = "fightsyj"
- "sex" = "Male"
- }
]]
__index
查询表中不存在的元素时触发!这个事件的元方法可以是一个函数也可以是一张表。如果它是一个函数,则以table和key作为参数调用它。如果它是一张表,最终的结果就是以key取索引这张表的结果。
- __index方法是一个表
local testTbl = {name = "fightsyj", age = 666}
local testMetaTbl = {}
testMetaTbl.__index = {sex = "Male"}
setmetatable(testTbl, testMetaTbl)
print(testTbl.name, testTbl.sex, testTbl.place)
-- fightsyj Male nil
- __index方法是一个函数
local testTbl = {name = "fightsyj", age = 666}
local testMetaTbl = {}
testMetaTbl.__index = function(tbl, key)
return string.format("%s is not exist !", key)
end
setmetatable(testTbl, testMetaTbl)
print(testTbl.name)
print(testTbl.sex)
--[[
fightsyj
sex is not exist !
]]
Lua查找一个表元素时的规则,其实就是如下3个步骤:
1.在表中查找,如果找到,返回该元素,找不到则继续;
2.判断该表是否有元表,如果没有元表,返回nil,有元表则继续;
3.判断元表有没有__index方法,如果__index方法为nil,则返回nil;如果__index方法是一个表,则重复 1、2、3;如果__index方法是一个函数,则返回该函数的返回值。
__newindex
更新表中不存在的元素时触发!这个事件的元方法既可以是函数,也可以是一张表。如果是一个函数,则以table、key以及value作为参数传入。如果是一张表,则对这张表做索引赋值操作。
- __newindex方法是一个表
local testTbl = {name = "fightsyj"}
local newindex = {}
local testMetaTbl = {__newindex = newindex}
setmetatable(testTbl, testMetaTbl)
testTbl.name = "fightsyj2"
testTbl.age = 666
dump(testTbl, "testTbl->")
dump(testMetaTbl, "testMetaTbl->")
--[[
- "testTbl->" = {
- "name" = "fightsyj2"
- }
- "testMetaTbl->" = {
- "__newindex" = {
- "age" = 666
- }
- }
]]
- __newindex方法是一个函数
local testTbl = {name = "fightsyj"}
local newindex = function(tbl, key, value)
print(tbl, key, value)
end
local testMetaTbl = {__newindex = newindex}
setmetatable(testTbl, testMetaTbl)
testTbl.age = 666
dump(testTbl, "testTbl->")
dump(testMetaTbl, "testMetaTbl->")
--[[
table: 00A291E0 age 666
- "testTbl->" = {
- "name" = "fightsyj"
- }
- "testMetaTbl->" = {
- "__newindex" = function: 00A32688
- }
]]
ps
- 一旦有了"newindex"元方法,Lua就不再做最初的赋值操作;
- "newindex"元方法用来对表进行更新(类似set),"index"元方法则用来对表进行查询(类似get);
忽略元表:rawget和rawset
有时候我们希望直接改动或获取表中的值时,就需要rawget和rawset方法了。
rawget
rawget可以让你直接获取到表中索引的实际值,而不通过元表的__index元方法。
local testTbl = {name = "fightsyj", age = 666}
local testMetaTbl = {}
testMetaTbl.__index = {sex = "Male"}
setmetatable(testTbl, testMetaTbl)
-- 通过rawget直接获取testTbl中sex对应的值,不会触发元表的__index事件
local sexValue = rawget(testTbl, "sex")
print(sexValue) -- nil
rawset
rawset可以让你直接为表中索引的赋值,而不通过元表的__newindex元方法。
local testTbl = {name = "fightsyj"}
local newindex = {}
local testMetaTbl = {__newindex = newindex}
setmetatable(testTbl, testMetaTbl)
-- 通过rawset直接对testTbl进行赋值操作,不会触发元表的__newindex事件
rawset(testTbl, "age", 666)
dump(testTbl, "testTbl->")
dump(testMetaTbl, "testMetaTbl->")
--[[
- "testTbl->" = {
- "age" = 666
- "name" = "fightsyj"
- }
- "testMetaTbl->" = {
- "__newindex" = {
- }
- }
]]
参考:
Lua 5.3 参考手册
Lua查找表元素过程
Lua中的元表与元方法学习总结