LUA学习(七) 元表和元方法

lua中可以对每个表进行操作,同时也提供了元表(metatable)概念,用来关联两个不同的表。如果用面向对象的思路来看,一个表的元表就相当于一个类的父类,有点继承的意思。

有两个很重要的函数来处理元表:

setmetatable(table, metatable)  -->将metatable设置为table的元表
getmetatable(table) 	-->获取table的元表
示例代码如下:

local t = {}
local mt = {7,8,9}
setmetatable(t,mt) 

--或者写在一行
local t = setmetatable({}, mt)


Lua中的每个值都有一套预定义的操作集合,比如数字是可以相加的,字符串是可以连接的,但是对于两个table类型,则不能直接进行“+”操作。这需要我们进行一些操作。在Lua中有一个元表,也就是上面说的metatable,我们可以通过元表来修改一个值得行为,使其在面对一个非预定义的操作时执行一个指定的操作,这个操作就可以称为元方法(metamethod)。比如:
local t = {}
local mt = {7,8,9}
setmetatable(t,{__index = mt})
print(t[3])	--> 9
在上面这个例子中,要打印t[3]的值,首先查看t[3]是否存在,如果存在,就打印。如果不存在,就查看t是否有元表,如果元表内有__index键值,就使用该键值指明的元方法(该键值我们稍后介绍),然后使用该table的第三个元素作为结果打印。

任何table都可以作为任何值得元表,而一组相关的table有可以共享一个通用的元表,此元表描述了它们共同的行为。一个table甚至可以作为它自己的元表,用于描述其特有的行为。总之,任何搭配形式都是合法的。

在Lua代码中,只能设置table的元表。若要设置其它类型的值得元表,则必须通过C代码来完成。还存在一个特例,对于字符串,标准的字符串程序库为所有的字符串都设置了一个元表,而其它类型在默认情况下都没有元表。如:

print(getmetatable("Hello World"))
print(getmetatable(10))

打印结果为:
table: 0x8548148
nil

对于一个table,它支持的元方法,即我们可以重新定义的元方法主要由以下:

__add(a, b) --加法
__sub(a, b) --减法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘幂
__unm(a) --相反数
__concat(a, b) --连接
__len(a) --长度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查询
__newindex(a, b, c) --索引更新(PS:不懂的话,后面会有讲)
__call(a, ...) --执行方法调用
__tostring(a) --字符串输出
__metatable --保护元表

下面介绍几个常用的元方法。

__index 元方法

当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index 键。如果__index包含一个表格,Lua会在表格中查找相应的键。如:

local t = {}
local mt = {7,8,9}
setmetatable(t,{__index = mt})
print(t[3]) --> 9

如果__index包含一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数。如:

mytable = setmetatable({key1 = "value1"}, {
	__index = function(mytable, key)
	print(key)	-->key2
	if key == "key2" then
		return "metatablevalue"
	else
		return nil
	end
end
})

print(mytable.key1,mytable.key2) -->value1, metatablevalue
小结:

Lua查找一个表元素时的规则,其实就是如下3个步骤:

1.在表中查找,如果找到,返回该元素,找不到则继续;

2.判断该表是否有元表,如果没有元表,返回nil,有元表则继续;

3.判断元表有没有__index方法,如果__index方法为nil,则返回nil;如果__index方法是一个表,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值。

__newindex 元方法

__newindex 元方法用来对表更新,__index则用来对表访问 。

当你给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。如:

mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })

print(mytable.key1)

mytable.newkey = "新值2"
print(mytable.newkey,mymetatable.newkey)

mytable.key1 = "新值1"
print(mytable.key1,mymetatable.key1)

--执行结果打印为:
value1
nil	新值2
新值1	nil
以上实例中表设置了元方法 __newindex,在对新索引键(newkey)赋值时(mytable.newkey = "新值2"),会调用元方法(该元方法中对元表进行了增加元素的操作,使mymetatable替换mytable),而不进行赋值。而如果对已存在的索引键

(key1),则会进行赋值,而不调用元方法 __newindex。

mytable = setmetatable({key1 = "value1", key2 = 0}, { __newindex = function(t, key) t.key2 = 5 end })
print(mytable.key1)
mytable.newkey = 8
print(mytable.newkey,mytable.key2)

--执行结果打印为:
value1
nil	5
同样,如果元方法是函数的话,就以table和新索引键为参数执行函数。


为表添加操作符

我们以重新定义两个表相加来演示如何为表添加操作符

-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中元素数量 table_num
function table_num(t)
    local mn = 0
    for k, v in pairs(t) do
        if mn < k then
            mn = k
        end
    end
    return mn
end

-- 两表相加操作
mytable = setmetatable({ 9, 8, 7 }, {
  __add = function(mytable, newtable)
    for i = 1, table_num(newtable) do   --以newtable的元素数量为循环终止值
	table.insert(mytable, table_num(mytable)+1,newtable[i])  -- 将newtable的元素插入mytable末尾
    end
    return mytable
  end
})

secondtable = {6, 5, 4}

mytable = mytable + secondtable
	for k,v in ipairs(mytable) do
print(k,v)
end
打印结果如下:

1	9
2	8
3	7
4	6
5	5
6	4

__add键值定义在元表中,table在遇到加操作符后,会在元表中查找__add键值,然后将加号两侧的表作为参数传递给__add定义的函数,执行该函数。

注意:在重写运算操作符对应的元方法时,lua的解释器首先会依次查找两个表的元表是否定义了该操作符对应的键值函数,如果第一个表有,就用第一个表得,不然,就用第二个表的,如果都没有,就会报错。


__call 元方法

__call 元方法在 Lua中 调用一个值时调用。

-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中元素数量 table_num
function table_num(t)
    local mn = 0
    for k, v in pairs(t) do
        if mn < k then
            mn = k
        end
    end
    return mn
end

-- 定义元方法__call
mytable = setmetatable({10}, {
  __call = function(mytable, newtable)
	sum = 0
	for i = 1, table_num(mytable) do
		sum = sum + mytable[i]
	end
    for i = 1, table_num(newtable) do
		sum = sum + newtable[i]
	end
	return sum
  end
})
newtable = {10,20,30}
print(mytable(newtable)) -->70


-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中元素数量 table_num
function table_num(t)
    local mn = 0
    for k, v in pairs(t) do
        if mn < k then
            mn = k
        end
    end
    return mn
end

-- 定义元方法__call
mytable = setmetatable({10}, {
  __call = function(mytable, t)
	sum = 0
	for i = 1, table_num(mytable) do
		sum = sum + mytable[i]
	end
	
	sum = sum + t

	return sum
  end
})
newtable = {10,20,30}
print(mytable(30)) -->40

当用mytable调用newtable时,会在mytable的元表中查找__call键值,然后以该表和该表的调用值作为参数执行__call键值定义的函数。该表的调用值由开发者约定。


__tostring 元方法

__tostring 元方法用于修改表的输出行为。

mytable = setmetatable({ 10, 20, 30 }, {
  __tostring = function(mytable)
    sum = 0
    for k, v in pairs(mytable) do
		sum = sum + v
	end
    return "表所有元素的和为 " .. sum
  end
})
print(mytable)
该段代码的执行结果为:

表所有元素的和为 60
__tostring这个键值有一点点的迷惑性,并不是转化为字符串输出,而是修改被print时的打印结果。当被print时,如果一个表的元表内有__tostring键值定义的函数,就可以以该表为参数执行该函数,否则,就正常执行print。


__metatable元方法

元表和元方法的使用可以带来很多的便利,但是如果可以随意的获取一个表的元表,那就有点危险了。

lua中提供了__metatable键值,如果一个表的元表设置了这个键值,那么既不能通过getmetatable方法获取该表的元表,也不能通过setmetatable方法为该表设置新的键值。如:

local t = {}
local mt = {}
setmetatable(t, mt)
mt.__metatable = "you cannot get the metatable"

print(getmetatable(t))
setmetatable(t, {})
执行结果如下:

you cannot get the metatable
lua: script.lua:7: cannot change a protected metatable
stack traceback:
	[C]: in function 'setmetatable'
	script.lua:7: in main chunk
	[C]: in ?


丢掉那该死的元表

有的时候,我们就不想从__index对应的元方法中查询值,我们也不想更新table时,也不想执行__newindex对应的方法,或者__newindex对应的table。那怎么办?在Lua中,当我们查询table中的值,或者更新table中的值时,不想理那该死的元表,我们可以使用rawget函数,调用rawget(tb, i)就是对table tb进行了一次“原始的(raw)”访问,也就是一次不考虑元表的简单访问;你可能会想,一次原始的访问,没有访问__index对应的元方法,可能有性能的提升,其实一次原始访问并不会加速代码执行的速度。对于__newindex元方法,可以调用rawset(t, k, v)函数,它可以不涉及任何元方法而直接设置table t中与key k相关联的value v。测试代码如下:

local t = {}
local mt = {y = 1, z = 2, x = 3}
setmetatable(t, {__index = mt})
print(t.y)		--> 使用元表的值 1
print(rawget(t, t.y))  --> 不使用元表的值 nil


rawset()的使用主要是为了避免陷入死循环,如:

mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = function(mytable, key, value) mytable.key = value * value end})
mytable.newkey = 4
print(mytable.newkey,mymetatable.newkey)
在__newindex定义的函数中,会根据新的key对表做操作,但是这个表没有这个键值,这样就会又调用__newindex定义的函数,然后又重复这个过程,这样就陷入了死循环,最终的结果就是堆栈溢出,打印:C stack overflow。

如果用rawset()的话,就不会出现这个问题了。如:

mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = function(mytable, key, value) rawset(mytable, key, value * value) end})
mytable.newkey = 4
print(mytable.newkey,mymetatable.newkey) --16 nil
这样,就不会陷入死循环,而且会对表进行直接操作,不会调用元方法。



你可能感兴趣的:(Lua)