元表
可以修改一个值在面对一个未知操作时的行为
元表是面向对象领域中的受限制类。
不支持继承
Lua语言中的每一个值都可以有元表。每一个表和用户数据类型都具有各自独立的元表,而其他类型的值则共享对应类型所属的同一个元表。
t = {}
print(getmetatable(t)) --> nil
可以使用函数setmetatable来设置或修改任意表的元表
t1 = {}
setmetatable(t, t1) --将t1设置为t的元表
print(getmetatable(t) == t1) --> true
在Lua语言中,只能为表设置元表;如果要为其他类型的值设置元表,则必须通过C代码或调试库完成
算术运算符相关的元方法
例:一个用于集合的简单模块
local Set = {}
--使用指定的列表创建一个新的集合
function Set.new (l)
local set = {}
for _, v in ipairs(l) do
set[v] = true
end
return set
end
function Set.union(a, b)
local res = Set.new{}
for k in pairs(a) do
res[k] = true
end
for k in pairs(b) do
res[k] = true
end
return res
end
function Set.intersection(a, b)
local res = Set.new{}
for k in pairs(a) do
res[k] = b[k]
end
return res
end
--将集合表示为字符串
function Set.tostring(set)
local l = {} --保存集合中所有元素的列表
for e in pairs(set) do
l[#l + 1] = tostring(e)
end
return "i" .. table.concat(l, ", ") .. "}"
end
return Set
现在,假设想使用加法操作符来计算两个集合的并集,那么可以让所有表示集合的表共享一个元表。这个元表中定义了这些表应该如何执行加法操作。
首先,我们创建一个普通的表,这个表被用作集合的元表:local mt = {}
然后,修改用于创建集合的函数Set.new。即将mt设置为函数Set.new所创建的表的元表
function Set.new(l) --第二个版本
local set = {}
setmetatable(set, mt)
for _, v in ipairs(l) do
set[v] = true
end
return set
end
在此之后,所有由Set.new 创建的集合都具有了一个相同的元表:
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
print(getmetatable(s1)) --> table: 0x00672B60
print(getmetatable(s2)) --> table: 0x00672B60
最后,向元表中加入元方法__add
,也就是用于描述如何完成加法的字段:
mt.__add = Set.union
此后,只要Lua语言试图将两个集合相加,它就会调用函数Set.union
,并将两个操作数作为参数传入。
通过元方法,我们就可以使用加法运算符来计算集合的并集了:
s3 = s1 + s2
print(Set.tostring(s3)) --> {1, 10, 20, 30, 50}
乘法运算符来计算集合的交集
mt.__mul = Set.intersection
print(Set.tostring((s1 + s2)*s1)) -->{10,20,30,50}
算术运算符元方法
元方法 | 描述 |
---|---|
__add | 加法 |
__sub | 减法 |
__mul | 乘法 |
__div | 除法 |
__idiv | floor除法 |
__unm | 负数 |
__mod | 取模 |
__pow | 幂运算 |
位操作元方法
元方法 | 描述 |
---|---|
__band | 按位与 |
__bor | 按位或 |
__bxor | 按位异或 |
__bnot | 按位取反 |
__shl | 向左移位 |
__shr | 向右移位 |
连接运算符__concat
当我们把两个集合相加时,使用哪个元表是确定的。然而,当一个表达式中混合了两种具有不同元表的值时,例如:
s = Set.new{1, 2, 3}
s = s + 8
Lua语言会按照如下步骤来查找元方法:如果第一个值有元表且元表中存在所需的元方法,那么Lua语言就使用这个元方法,与第二个值无关;如果第二个值有元表且元表中存在所需的元方法,Lua语言就使用这个元方法;否则,Lua语言就抛出异常。因此,上例会调用Set.union,而表达式10+S
和"hello"+s
同理(由于数值和字符串都没有元方法__add
)。
Lua语言不关心这些混合类型,但我们在实现中需要关心混合类型。如果我们执行了s = s + 8
,那么在Set.union
内部就会发生错误:bad argument #1 to 'pairs' ( table expected,got number)
关系运算符的元方法
元方法 | 描述 |
---|---|
__eq | 等于 |
__lt | 小于 |
__le | 小于等于 |
Lua语言会将
a~=6
转换为not(a == b)
,a > b转换为b < a
,a >= b
转换为b <= a
库定义的元方法
函数tostring元方法
__tostring
对应tostring
那么以上面例
mt.__tostring = Set.tostring
--调用函数print时,print就会调用函数tostring,tostring又会调用Set.tostring
s1 = Set.new{10, 5, 8}
print(s1) -->{5, 8, 10}
函数setmetatable和getmetatable的元方法
__metatable
:用于保护元方法,使用户既不能看到也不能修改集合的元表。
getmetatable会返回这个字段的值,而setmetatable则会引发一个错误
mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1)) --> not your business
setmetatable(s1, {}) --> stdin:1: cannot change protected metatable
pairs元方法
__pairs
:修改表被遍历的方式和为非表的对象增加遍历行为。
当一个对象拥有
__pairs
元方法时,pairs
会调用这个元方法来完成遍历。
__index元方法
用来在表中访问字段,如果没有返回nil
--创建具有默认值的原型
prototype = {x = 0, y= 0, width = 100,height = 100}
local mt = {} --创建一个元表
--声明构造函数
function new (o)
setmetatable(o, mt)
return o
end
--定义元方法__index
mt.__index = function(_, key)
return prototype[key]
end
w = new{x = 10, y = 20}
print(w.width) --> 100
Lua语言会发现w中没有对应的字段
width
,但却有一个带有__index
元方法的元表。因此,Lua语言会以w和"width"(不存在的键)为参数来调用这个元方法。元方法随后会用这个键来检索原型并返回结果。
如果不想调用__index
元方法,可以使用函数rawget(t, i)
,即不考虑元表的情况下对表进行简单访问
__newindex方法
元方法__newindex
与__index
类似,不同之处在于前者用于表的更新而后者用于表的查询。
当对一个表中不存在的索引赋值时,解释器就会查找_newindex元方法:如果这个元方法存在,那么解释器就调用它而不执行赋值。像元方法__index
一样,如果这个元方法是一个表,解释器就在此表中执行赋值,而不是在原始的表中进行赋值。
可使用rawset(t, k, v)
等价于t[k] = v
来绕过元方法
修改默认值
通过元表,可以修改字段的默认值(一般默认值为nil)
function setDefault(t, d)
local mt = {__index = function() return d end}
setmetatable(t, mt)
end
tab = {x=10, y=20}
print(tab.x, tab.z) --> 10 nil
setDefault(tab, 0)
print(tab.x, tab.z) --> 10 0
在调用
setDefault
后,任何对表tab中不存在字段的访问都将调用它的__index
元方法,而这个元方法会返回零(这个元方法中的值是d)。