Lua里面有一个就目前来说,暂时没用过,但是觉得很有用,可能将来需要用到的东西:元表。
什么是元表,以及元表是做什么的?
就我理解,元表是一种附加属性,是需要依附在一个正儿八经表的上面的,这个表里面可以存放我们需要重定义的一些对表的操作。
比如:在C++或者Java中经常需要将一个坐标封装成一个二维向量的结构体,然后在里面重载一些加减的操作, 因为普通的加减是对于两个数字的,但是当你想让两个二维向量相加的时候,需要将X相加,然后Y相加,然后返回一个二维向量,此时普通的加号就没有办法完成我们的操作了、这个时候我们就要将加号重载一下。然后当调用加号的对象变成二维向量的时候,系统就会使用我们自己重载的加号,完成两个二维向量的加法,然后返回一个二维向量。
元表就是在Lua中做这件事的。比如现在我们访问一个表,但是我们不小心访问到了空值,那么就会返回一个nil,当我们访问表中的空值的时候,不想接收一个nil,想接收一个默认值,此时就需要就会用到元表了。
那么我们应该怎么去给一个普通表设置一个元表或者我们应该怎么获取一个普通表的元表呢?
Lua给我们提供了这么两个接口setmatetable以及getmetatable
使用方法如下:
myTable = {} -- 定义一个初始的表
myMetaTable = {} -- 再定义一个表,一会将这个表,设置为myTable的元表
print(myTable)
print(myMetaTable)
-- 这是两个接口
-- setmetatable的两个参数含义是:将第2个表设置为第1个表的元表,返回拥有元表的第一个表
-- getmetatable:获取到参数表的元表,并返回
myTable =setmetatable(myTable,myMetaTable)
metaTable_From_get = getmetatable(myTable)
-- 打印出来的这个表的地址应该和myMetaTable的地址是一样的
print(metaTable_From_get)
Tip:setmetatable的第一个参数,我们当然可以直接使用{}去构造一个表,看自己喜好了。
上面我们说了一下元表的设置和获取,下面我们说一下元表的注意事项:
之前我们举例的时候,元表设置的是一个空表,但是实际使用的时候,元表是不能设置成为空表的。元表里面会存放我们自己设置的函数,自己储存的变量,但是变量的key值,要谨慎设置为:__metatable。
可以使用,但是大多数当我们在元表里面设置这个Key值的时候,其实是不希望外部可以更改我们元表里面的元素。
当我们使用__metatable作为元表里面的某一个Key值,此时我们通过getmetatable获取的就是__metatable对应的value,不会获取元表。
-- 将元表里面添加__metatable Key值
myTable = setmetatable({"Lua","C++","Java","C#"},{__metatable = "lock"})
metaTable_From_get = getmetatable(myTable)
print(metaTable_From_get)
控制台输出:
此时我们获取元表之后,然后将元表打印,本应我们打印出元表的地址,但是此时我们打印出__metatable对应的value。
当我们的元表里面有__index关键字的时候。访问规则就有了一些变化。
什么时候会用到这个__index关键字呢?当我们放普通表里面没有的值的时候,且当普通的表有元表的时候,这个时候就会访问元表里面的__index。
访问__index,就有两种情况,
第一种:__index也是一个表,这个时候,当普通表没有我们要访问的值,就会去__index对应的表里面去找,如果__index里有这个值,就会返回这个值,如果没有也还是会返回nil。
-- 当__index 是一个表的时候
myTable = setmetatable({"Lua","C++","Java","C#"},{__index = {"metatable1","metatable2","metatable3","metatable4","metatable5"}})
metaTable_From_get = getmetatable(myTable)
print("普通表的地址")
print(myTable)
print("元表的地址")
print(metaTable_From_get)
print(myTable[1])
print(myTable[5])
运行结果:
第二种:__index是一个我们自己封装的函数,就会执行我们这个函数的逻辑。
当然,这个函数是有一点点小要求,参数必须是(table,key),table就是普通表,key就是你要访问普通表的值
-- 当__index 是一个函数的时候
myTable = setmetatable({"Lua","C++","Java","C#"},{__index = function(table,key)
print("普通表里面没有需要的值,然后通过__index记性获取")
print(table)
print(key)
end})
metaTable_From_get = getmetatable(myTable)
print("普通表的地址")
print(myTable)
print("元表的地址")
print(metaTable_From_get)
print(myTable[1])
print(myTable[10])
运行结果:
这个时候,我们观察输出,就会发现程序走了我们自己定义的__index对应的匿名函数的逻辑,但是输出还是为nil,因为我们在这个匿名函数中,没有返回值。如果你给他返回一个字符串,那么最后的那一条输出就会变成你返回的字符串了。
上面的__index关键字是控制我们去访问表的时候,起作用。
这个__newindex关键字是控制我们更新表的时候,而且是更新一个不存在的索引的时候,才会起作用
第一种:__newindex是一个表
当__newindex是一个表的时候,我们通过一个新的索引来更新普通表里面的内容的时候,其实没有更新到普通表里面,是更新到了__newindex表里面(这里我们默认这个表是有元表,且元表里面是有__newindex关键字的)
我们先来写一段代码:
newtable = {}
myMetaTable = {
__newindex = newtable
}
myTable = setmetatable({"C++","Java","C#","Go",},myMetaTable)
myTable[1] = "Lua"
print(myTable[1])
print(newtable[1])
这段代码里面我们将元表里面设置了__newindex关键字,然后将这个关键字赋值为一个表newtable,但是我们访问了普通表和newtable,结果如下:
说明我们还是改了普通表,newtable里面的内容不会变。
此时的原因是因为我们访问的是一个旧的索引,下面我们访问一下新的索引试试:
这段代码中我将访问的索引更改为5.普通表里面是没有5这个索引的,且此时我们拥有__newindex关键字,所以我们给普通表赋值"Lua"之后,然后打印普通表以及newtable的第五号元素,
结果发现我们刚才赋值的普通表里面没有“Lua”,然后打印出来nil
相反,newtable 里面打印出来“Lua”,结果就印证了,我们上述的结论。
第一种:__newindex是一个函数
当我们的__newindex关键字是一个函数的时候,这个时候,当我们给普通表赋值的时候,就会调用我们这个函数,要注意的是,此时还是必须访问新的索引,访问旧索引的话,就会出现上面的情况。
我们写段代码印证一下:
myMetaTable = {
__newindex = function(table,key,value)
print("Call __newindex")
rawset(table,key,value)
end
}
myTable = setmetatable({"C++","Java","C#","Go",},myMetaTable)
myTable[5] = "Lua"
print(myTable[5])
上面的代码我们将__newindex命名为一个函数,然后将该元表赋值给普通表,然后我们再去设置普通表一个新的索引:5,
设置完成之后,我们再去打印出这个普通表的5号元素,就会发现,有调用我们的__newindex函数。
Tip:有个需要注意的点的就是:我们在__newindex函数里面给表赋值的时候,,没有用table[key] = value,
而是用了]rawset函数,是因为如果我们使用普通的赋值方式的话,他就又会去调用__newindex函数,这样就成了死循环了。
在C++里面我们通常会为某一个类重载操作符,在Lua里面,我们就可以通过这个元表实现,下面我们就以为重载加号为例子,写一段代码:
myMetaTable = {
__add = function(table,newtable)
local m_ntable = #table
for k,v in pairs(newtable) do
m_ntable = m_ntable +1
table[m_ntable] = v
end
return table
end
}
myTable = setmetatable({"Java","C++","C#","Go"},myMetaTable)
newtable = {"PHP","Python"}
myTable = myTable + newtable
for k,v in pairs(myTable) do
print(v)
end
在上面的代码里面,我们使用关键字:__add来代表我们对表的+操作,然后我们自己定义一个函数,用来代替本来的加法操作。
然后我们将重载好加号的元表设置给myTable,这个时候我们将myTable和newTable相加,然后输出一个新表myTable。
这个时候我们就可以看到,newTable里面的内容,被相加到myTable里面去了。
当然,我们也可以将两个表相加的顺序改成myTable = newtable + myTable,也可以 正确输出,只不过新表里面的顺序变了而已。
上面就是对于表的加号的重载,当然,对于普通的运算有:加减乘除、与非或。分别对应的关键字如下:
模式 | 描述 |
---|---|
__add | 对应的运算符 '+'. |
__sub | 对应的运算符 '-'. |
__mul | 对应的运算符 '*'. |
__div | 对应的运算符 '/'. |
__mod | 对应的运算符 '%'. |
__unm | 对应的运算符 '-'. |
__concat | 对应的运算符 '..'. |
__eq | 对应的运算符 '=='. |
__lt | 对应的运算符 '<'. |
__le | 对应的运算符 '<='. |
实现的方法,和上面的大同小异。这里就不单独讲解了。
call顾名思义就是调用 ,当调用表的时候,Lua就会使用这个函数,前提是你的元表里面有这个__call的定义。
我们平时访问表。就是通过[]的形式来访问,而且一次只能访问一个元素。
但是当我们为元表定义了__call关键字的时候,我们就可以通过括号来访问表了。
下面是一段简单的事例代码:
metatable = {
__call = function (table,arg1)
print("访问表table")
print("访问参数"..arg1)
return "Lua is Good"
end
}
mytable = setmetatable({"one","two","three","four","five"},metatable)
str = mytable(2)
print(str)
上面的脚本是在元表里面声明了一个__call关键字,然后里面有两个参数,第一个参数肯定就是我们的普通表了,然后第二个参数是我们使用括号访问的时候,里面的参数。然后我们使用括号进行访问,然后传了一参数为:2,这个时候Lua里面就会调用我们刚才声明的__call函数。打印出两个日志,最后将“Lua is good”字符串进行返回。
上面我们是使用括号,传一个参数进行表的访问,当然我们可以使用多个参数。形式和上面的一模一样,只是参数多了。
像下面这样:
metatable = {
__call = function (table,arg1,arg2,arg3)
print("访问参数"..arg1..arg2..arg3)
return "Lua is Good"
end
}
mytable = setmetatable({"one","two","three","four","five"},metatable)
str = mytable(2,3,4)
print(str)
这样的话,输出就是:
这个关键字的作用就是可以将我们表,转化为字符串,平常我们输出的时候,使用:print(),里面需要的就是字符串类型的参数,当我们给里面直接传一个表的时候,日志就会输出表的地址,但是当我们想输出整个表的内容的时候,这个tostring关键字就可以帮我们将table转换为字符串输出。
还是和之前的一样,将__tostring实现为了函数,然后遍历表里面的内容,将其拼接成字符串:
mytable = setmetatable({"Python","Java","C#","C++","Lua"},{
__tostring = function (table)
local str = ""
for k,v in pairs(table) do
str = str..v.."; "
end
return str
end})
print(mytable)
运行结果: