metatable,Lua中的元表,是Lua中重要的内容。
参考自:Lua中的元表与元方法(果冻想)
在Lua代码中,只能设置table的元表。若要设置其它类型的值的元表,则必须通过C代码来完成。还存在一个特例,对于字符串,标准的字符串程序库为所有的字符串都设置了一个元表,而其它类型在默认情况下都没有元表。
下面用一个简单的例子初探元表:
__add元方法:
Set = {}
local mt = {}
function Set.new(table)
local set = {}
setmetatable(set, mt)
for _, v in pairs(table) do
set[v] = true
end
return set
end
function Set.union(tableA, tableB)
if getmetatable(tableA) ~= mt or getmetatable(tableB) ~= mt then
print("Set.union metatable error")
return
end
local set = Set.new{} --类似Set.new({})。很关键,新表的生成调用new函数,保证绑定了元表
for k in pairs(tableA) do
set[k] = true
end
for k in pairs(tableB) do
set[k] = true
end
return set
end
function Set.print(table)
for k, v in pairs(table) do
print(k, " ", v)
end
end
mt.__add = Set.union
local setA = Set.new({1, 2, 3})
local setB = Set.new({4, 5})
print(getmetatable(setA))
print(getmetatable(setB))
local setC = setA + setB
local setD = 2 + setC
Set.print(setC)
table: 0x213cad0
table: 0x213cad0
Set.union metatable error
1 true
2 true
3 true
4 true
5 true
Lua是按照以下步骤寻找对元方法:
1、对于二元操作符,如果第一个操作数有元表,并且元表中有所需要的字段定义,比如我们这里的__add元方法定义,那么Lua就以这个字段为元方法,而与第二个值无关;
2、对于二元操作符,如果第一个操作数有元表,但是元表中没有所需要的字段定义,比如我们这里的__add元方法定义,那么Lua就去查找第二个操作数的元表;
3、如果两个操作数都没有元表,或者都没有对应的元方法定义,Lua就引发一个错误。
上面的例子中,2 + setC 虽然 setC 具有元方法,但是其与数字 2 相加的并没有定义相关的方法,所以在Set.union进行了判断,保证两个入参对应同样的元表。
__tostring元方法:
上面我们写了自己 print 函数,如果我们使用 lua 自带的 print 函数呢?不能格式化输出!怎么办?我们需要自己重新定义__tostring元方法,让print可以格式化打印出table类型的数据。
function Set.tostring(set)
local tab = {}
for k in pairs(set) do
tab[#tab + 1] = k
end
return "{" .. table.concat(tab, ", ") .. "}"
end
mt.__tostring = Set.tostring
local setA = Set.new({1, 2, 3})
local setB = Set.new({4, 5})
local setC = setA + setB
print(setC)
这样打印出:
{1, 2, 3, 4, 5}
元表的设置与查看通过 setmetatable 和 getmetatable 两个函数可以方便得到,但如果我们不希望这么“方便”呢?我们希望在元表被设置后不被修改,元表中的一个字段,用于保护元表,该字段是__metatable。当我们想要保护集合的元表,让用户既不能看也不能修改元表,那么就需要使用__metatable字段了,在设置了该字段时,getmetatable就会返回这个字段的值,而setmetatable则会引发一个错误。如下:
Set = {}
local mt = {}
function Set.new(table)
local set = {}
setmetatable(set, mt)
for _, v in pairs(table) do
set[v] = true
end
mt.__metatable = "You cannot get metatable"
return set
end
local setA = Set.new({1, 2, 3})
local setB = Set.new({4, 5})
print(getmetatable(setA))
print(setmetatable(setB, {}))
你会得到:
You cannot get metatable
lua: a.lua:48: cannot change a protected metatable
这是最常见的元方法。我们访问一个table中不存在的字段时,得到的结果是nil,但是这种状况很容易被改变。Lua按照以下的步骤决定是返回nil还是其它值:
1、当访问一个table的字段时,如果table有这个字段,则直接返回对应的值;
2、当table没有这个字段,则会促使解释器去查找一个叫__index的元方法,接下来就就会调用对应的元方法,返回元方法返回的值;
3、如果没有这个元方法,那么就返回nil结果。
下面用例子说明:
Person = {}
Person.default = {sex = "male", year = 0, height = 0}
Person.mt = {}
function Person.new(person)
setmetatable(person, Person.mt)
return person
end
-- 定义__index元方法
Person.mt.__index = function (table, key)
return Person.default[key]
end
local person = Person.new({sex = "female", year = 18})
print(person.sex)
print(person.year)
print(person.height)
输出为:
female
18
0
__index元方法不必一定是一个函数,它还可以是一个table。当它是一个函数时,Lua以table和不存在key作为参数来调用该函数,当它是一个table时,Lua就以相同的方式来重新访问这个table,所以上面的代码也可以是这样的:
Person.mt.__index = Person.default
可以达到同样的目的。
有时候我们并不想去查询元表,怎么办?
使用
rawget(table, key)
可以完成一次原生的访问。如:
print(rawget(person, "sex"))
print(rawget(person, "year"))
print(rawget(person, "height"))
的输出结果为:
female
18
nil