Lua使用元表来定义对table
或者用户自定义数据的操作。在很多情况下,可以简化或者提高table
或用户自定义数据的操作方式,比如对于+
法操作,table
是不具备该功能,两个table直接相加将发生错误
t = { 1, 2, 3 }
t1 = { 10, 20, 30, 40 }
t2 = t + t1
-->> attempt to perform arithmetic on a table value (global 't')
对于加法操作,如上述中的t + t1
,在这行该表达式时,由于两者都是table,因此会先检查是否具有元表,如果有元表,则从元表中检查是否具有__add
方法,如果有则调用该方法执行t+t1
。__add
及其指向的函数,称为元方法,实现这种形式,类似于C#中的算数表达式重载。
Lua通过
setmetatable
进行元表设置,通过getmetatable
获取元表信息。在对table
进行元编程时,一般需要通过setmetatable
进行元方法的设置。当对不同table实例设置同一个元表时,这些实例的元表将相同。
a = {}
t = { 1, 2, 3 }
t1 = { 10, 20, 30, 40 }
-- 刚声明table时,不具备元表。需要设置,a即为元表
print(getmetatable(t))
print(getmetatable(t1))
-->> nil
-->> nil
setmetatable(t, a)
print(getmetatable(t))
-->> table: 0000000000eca040
print(getmetatable(t1))
-->> nil
setmetatable(t1, a)
print(getmetatable(t1))
-->> table: 0000000000eca040
- 对数组实现
+
元函数
-- 声明元表
local mt = {}
-- 对元表添加_add方法,用于描述+法操作
mt.__add = function(a, b)
local res = {}
-- 由于是数组,因此只需要记录值即可,丢弃索引值
for _, v in pairs(a) do
res[v] = true
end
for _, v in pairs(b) do
res[v] = true
end
return res
end
-- 打印table
function printTable(t)
-- 读取所有的key即可,这里是特殊操作,上面只将数组的值记录到了索引位置
local res = {}
for k in pairs(t) do
res[#res + 1] = k
end
print("{" .. table.concat(res, ",") .. "}")
end
local t1 = { 1, 2, 3 }
local t2 = { 10, 20, 30 }
setmetatable(t1, mt)
local t3 = t1 + t2
printTable(t3)
在这个示例中,实现了元表的__add
方法,其接收两个table
参数,这个示例只能处理数组,不是通用table
相加操作;因此在计算两个table相加时,为了简单,只记存储了value
值,只要在新的table中key存在的就是两个table相加的结果。提供了测试打印table的函数,在这个里面直接取key存储新table,按数组序列存储,并将其组织成字符串,以,
分割。
最后在计算前,指定了t1
同元表mt
相同,Lua在计算+
时,先判断第一个参数t1
,在其元表中发现有__add
方法,则直接调用该方法执行相加操作;如果t1
元表中不具备__add
方法,则检查第二个变量t2
,如果两者都不具备__add
方法,则程序异常。
- Table元表支持的元方法
元方法 | 描述 |
---|---|
__add | 加,运算符+ |
__mul | 乘,运算符* |
__sub | 减,运算符- |
__div | 除,运算符/ |
__unm | 取反,相反数 |
__mod | 模,运算符% |
__pow | 乘幂,运算符^ |
__concat | 连接操作,.. |
该表格描述了Table支持的运算符元方法,可以根据业务需求进行定制。上面针对__add
方法对数组进行了实现,不过上述实现方法,较为复杂,每次需要重新定义,并设置变量元表同__add
的元表一致。这里以运算符*
和运算符-
为示例进行重新定义。
Set = {}
-- table的metatable
local mt = {}
-- 实现创建新集合的方法
function Set.new(lst)
local set = {}
setmetatable(set, mt)
for _, v in ipairs(lst) do
set[v] = true
end
return set
end
-- 并集,运算符+
function Set.union(a, b)
local res = Set.new {}
for key in pairs(a) do
res[key] = true
end
for key in pairs(b) do
res[key] = true
end
return res
end
-- 交集,即运算符*,同时在a,b中都存在的元素
function Set.intersection(a, b)
local res = Set.new {}
for key in pairs(a) do
res[key] = b[key]
end
return res
end
-- 补集,即运算符-,b在a中的相对补集,a-b,属于a但不属于b的元素
function Set.complement(a, b)
local res = Set.new {}
for key in pairs(a) do
if not b[key] then
res[key] = true
end
end
return res
end
-- 连接,实现连接符
function Set.toString(a)
local lst = {}
for key in pairs(a) do
lst[#lst + 1] = key
end
return table.concat(lst, ",")
end
mt.__add = Set.union
mt.__mul = Set.intersection
mt.__sub = Set.complement
local a = Set.new { 10, 20, 30, 40 }
local b = Set.new { 30, 40, 50, 60 }
local d = a * b
print("{" .. Set.toString(d) .. "}")
-->> {40,30}
local e = a - b
print("{" .. Set.toString(e) .. "}")
-->> {20,10}
在本例中,创建了一个新的对象,将创建数组,指定元表等方法全部集成,同时实现了交集和补集函数。在本例中,主要是利用了数组数值作为索引值,同时索引位置设置为nil
时,将删除一个元素的特性;在实现table转字符串时,又重新创建了数组,从索引位置1
开始存储数据。最后将交集、补集方法指定给了mt
元表的__add
、__sub
等方法,之后就可以使用运算符进行操作table。
- 关系运算符元方法
元方法 | 描述 |
---|---|
__eq | 等于,== |
__lt | 小于,< |
__le | 小于等于,<= |
Lua对于关系运算的元方法只定义了这三种,其他的不等于~=
、大于>
、大于等于>=
使用上述三种进行转换。下述实现Table的关系运算符,虽然不实现>
、>=
但根据Lua的设计,会使用<
、<=
、==
等实现,仍然可用。在本示例中,仍然使用前一节的Set
扩展,在该对象上继续实现关系运算符。
-- 小于等于,集合的关系运算符,<=,对于集合,一般a<=b,即a是b的子集
function Set.lessEqual(a, b)
for i in pairs(a) do
if not b[i] then
return false
end
end
return true
end
-- 小于,集合关系运算符<。
function Set.less(a, b)
-- 利用lessEqual实现,首先a是b的子集,同时,b不能是a的子集,即排除了b==a的情况,剩余a> true
print(b < a)
-->> true
print(a <= a)
-->> true
print(a >= a) -- 可以使用>、>=等运算符
-->> true
-- 因为在Set的算数运算符基础上实现关系运算符,因此可以使用关系运算符和算数运算符混合使用
print(b == a * b) -- a * b取集合a、b交集,由于b是a的子集,则a * b即为 {3,4},即获取到b
-->> true
- 元表的其他元方法
元方法 | 描述 |
---|---|
__tostring | 转字符串 |
__call | 函数调用 |
__index | 调用索引值 |
__newindex | 赋值 |
- __tostring
对于table在转为字符串的情况比较少,一般用于跟踪。仍然在前面的Set
对象上进行测试,之前Set方法已经包含了一个toString
方法,对此方法进行部分改造。
-- 连接,实现连接符
function Set.toString(a)
local lst = {}
for key in pairs(a) do
lst[#lst + 1] = key
end
return "{" .. table.concat(lst, ",") .. "}" -- 实现{}的拼接
end
-- 将该方法添加到元方法__tostring
mt.__tostring = Set.toString
-- 使用
local a = Set.new {1, 2, 3}
print(a)
-->> {1,2,3}
- __call
__call
元方法,可以是的一个table像函数一样调用一个新的值。实现一个table和另外一个table的元素数值相加,使用__call
实现。仍在Set
对象上实现。
-- __call调用,第一个参数为当前table
function Set.summation(t, ...)
local sum = 0
for k in pairs(t) do
sum = sum + k
end
for _, v in pairs { ... } do
sum = sum + v
end
return sum
end
-- 将mt的元方法指向summation方法
mt.__call = Set.summation
-- 可以调用__call方法
local a = Set.new { 1, 2, 3 }
local sum = a(4, 5) -- 此处table可以像一个函数一样调用,参数为新的table或者可变参数
print(sum)
-->> 15
注意,本例中使用Set对象,Set在初始化时,将数组的值存储在新的数组的索引处。第二个参数为可变参数,如果要接收为table需要调整代码。
- __index
当访问一个table
中的字段时,Lua会先从table
中查找该字段,如果存在,则返回该字段的值;如果没有,则检查该table
是否具有元表,如果没有元表,则返回nil
;如果有元表,则会从元表中查找__index
元方法,如果没有该元方法,返回nil
;如果有__index
元方法,则从该方法中查找指定字段。__index
方法可以返回一个函数、也可以返回一个table
。因此查找该方法,也需要分两种情况。
local a = { name = "Lua", version = "5.3" }
local mt = setmetatable({}, { __index = a }) -- __index使用元表实现
print(mt.name)
-->> Lua
-- 采用方法实现__idnex,效果和table差不多,只不过是方法时,直接返回方法的返回值
local mt2 = setmetatable({}, {
__index = function(mt2, key)
return a[key]
end
})
print(mt2.name, mt2.score)
-->> Lua nil
在该例中,mt
并不具有name
属性,但是在声明时将其元方法__index
指向了表a
,因此当访问name字段时,mt
中没有,但是mt
具有元表,并且实现了__index
元方法,因此会继续调用__index
,此时是一个table,将会和最初查找mt
的流程一致,先从table中找,后找元表__index
元方法。
Lua的这种设计对于面向对象的继承,有着重要的作用。一个动物具有嘴、手、脚等肢体,这些肢体是构成动物的基本元素,但是每个具体动物的嘴、手、脚的数量可能不同。设计一个对象,实现基础的嘴、手、脚属性,在创建一个具体的动物时,指定属性的具体数值。
local animal = {}
animal.prototype = {
leg = 0,
mouth = 0,
hand = 0
}
-- 创建元表
local mt = {}
function animal.new(prototype)
-- 设置元表
setmetatable(prototype, mt)
-- 实现元表的__index
mt.__index = animal.prototype
return prototype
end
local person = animal.new({leg = 2, mouth = 1, hand = 2})
print("腿:" .. person.leg .. ",嘴:" .. person.mouth .. ",手:" .. person.hand)
-->> 腿:2,嘴:1,手:2
- __newindex
__newindex
和__index
是类似的元方法,__newindex
是用于对table的更新,__index
是用来查询。当对一个table进行赋值时,会先查找是否具有__newindex
元方法,如果有,Lua则会调用该元方法,如果没有,则执行赋值。如果__newindex
是一个table,则调用赋值时,会直接将值插入到__newindex
指向的table。
local animal = {}
-- 设置__newindex
local mt = setmetatable({ mouth = 1 }, { __newindex = animal })
print(mt.mouth)
-- 设置手的数量,因为具有__newindex,因此该赋值会将数据插入到animal中
mt.hand = 2
print(mt.hand, animal.hand)
-- 对原有的mouth设置
mt.mouth = 2
print(mt.mouth, animal.mouth)