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







__index

这是最常见的元方法。我们访问一个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