-
元表的概念
通常,
lua
中的每个值都会有一套预定义的操作集合。例如可以将两个数字相加,但是我们没有办法直接让两个table
相加,也没有办法对函数作比较,或者调用一个字符。但是我们可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。例如,我们有两个
table a,b
可以通过元表定义出如何计算a+b
。当lua
试图将两个table
相加时,他会先检查二者之一是否有元表,然后检查该元表中是否有一个叫_add
的字段。如果找到了该字段,就调用该字段对应的值,这个值也就是所谓的元方法。也就是c++中的运算符重载 -
注意事项
-
lua
里每一个值都有一个元表,不同的是table
和userdata
可以有各自独立的元表(两个table
可以分别对应两个元表),而其他类型的值则共享其类型所属的单一元表。
t={}
print(getmetatable(t)) -->nul lua在创建新的table时,不会创建元表
t1={}
t.setmetatable(t,t1) -->将t1设置为t的元表
任何的
table
都可以作为任何值的元表,一组table
可以共用一个元表。这个元表描述了他们共同的行为。一个table
甚至可以作为他自己的元表,用于描述他自己特有的行为。在
lua
里,你只能设置table
的元表。如果想设置其他的类型必须通过C代码来实现。
-
算数类的元方法
我们这里先看一个示例
Set={}
mt={}
function Set.New(t)
local res={}
setmetatable(res,mt)
for k,v in ipairs(t) do
res[k]=v;
end
return res
end
function Set.Add(a,b)
local res=Set.New({})
for k,v in ipairs(a) do res[k]=v end
for k,v in ipairs(b) do res[k]=v end
return res
end
function Set.PrintToString(a)
res="{ "
for k,v in ipairs(a) do res=res..v.." " end
res=res.." }"
return res
end
mt.__add=Set.Add
mt.__concat=Set.PrintToString
a=Set.New({1,2,3,4,5})
b=Set.New{5,6,7}
c=a+b
print(Set.PrintToString(c))
输出结果为 { 5 6 7 4 5 }
这里是一个简单地案例,修改了两个table
相加的元方法。
这里需要注意的是,当两个集合相加时,可以使用任意一个集合的元表。然而,当一个表达式中混合了不同元表的值时,他会按照以下步骤查找元表:如果第一个有元表,并且元表中有__add字段,lua就以此字段作为元方法;如果两个值都没有元方法,Lua就引发一个错误
同理,我们也可以在此基础上对其他的操作进行重载,例如减法,乘法等...
-
关系类的元方法
关系类的元方法有三个,分别为__eq(等于)
、_lt(小于)
、__le(小于等于)
,其他三个没有单独的方法,通过一下转换得出。
a~=b ==> not(a==b)
a>b ==> b=b ==> b<=a
与算术类元方法不同的是,关系类元方法不能应用于混合类型。如果试图这样操作,lua会引发一个错误
等于比较永远不会引发错误。但是如果两个对象拥有了不同的元方法,那么等于操作不会调用任何一个元方法,而是直接返回false。只有当两个比较对象共享一个元方法是,lua才调用这个等于比较的元方法。
-
库定义的元方法
目前介绍的元方法只针对
lua
的核心,也就是一个虚拟机。由于元表也是一种常规的table
,所以任何人、任何函数都可以使用他们。
函数print
总是调用tostring
来格式化其输出。(tostring ==> __tostring
)
函数setmetatable
和getmetable
也会用到用到元表中的一个字段,用于保护元表。假设想要保护器元表,使用户及看不见也不能修改集合的元表,那么就需要用到字段__metatable
。当设置了该字段时,getmetatable
就会返回该段的值,而setmetatble
会引发一个错误。 -
table访问的元方法
lua
还提供了一种可以改变table
行为的方法。有两种可以改变的table
行为:查询table
以及修改table
中不存在的字段。
1. __index元方法
当访问一个table
中不存在的字段时,得到的结果是nil
。但实际上,这些访问会促使解释器去查找一个叫__index
的元方法。如果没有这个元方法,那么访问结果如前所述的为nil
。否则,就由这个元方法来提供最终结果。
来看一个比较经典的示例
Window={}
Window.prototype={x=0,y=0,width=100,height=100}
Window.mt={}
Window.mt.__index=function(table,key)
return Window.prototype[key]
end
function Window.new(o) --类似于构造函数
setmetatable(o, Window.mt)
return o
end
print((Window.new{x=10,y=20}).width)
这里要创建一些窗口的table
,每个table
中必须描述一些窗口参数,所有的这些参数都有默认值。当没有给对应参数赋值是,返回默认值。这里相当于是一个简单地继承。所有生成的窗口继承了prortotype
这个示例中__index
元方法是一个函数。但其实他还可以是一个table
。当他是一个table
时,lua
就会在这个table
中查询这个key
所对应的的value
虽然将函数作为__index
来实现相同功能开销较大,但函数更加灵活。可以通过函数来实现多重继承、缓存及其他一些功能。
如果不想在访问一个table
时涉及到他的__index
元方法,可以使用函数rawget
调用rawget(t,i)
就相当于对table进行了一个原始的访问,不考虑元表
2.__newindex元方法
__newindex
元方法与__index
类似,不同之处在于前者用于table
的更新(set)后者用于table
的查询(get)。当对一个table
中不存在的索引赋值时,解释器就会查找__newindx
元方法。如果有,解释器就会调用它而不是执行赋值。
3.具有默认值的table
常规table中的任何字段默认都是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
setdefalut(tab,0)
print(tab.x,tab.z) -->10 0
在调用setdefault
后,任何对tab中存在字段的访问都将调用他的__index
元方法,而这个元方法会返回0(这个原方法中d的值)
这里setDefault
函数为了所有需要默认值的table
创建了一个新的元表,如果需要很多带默认值的table
其开销会比较大。这里我们可以换一种写法
这里我们把 默认值存放在table
本身中,_ _ _
用这种相对特殊的命名防止冲突
local mt={__index=function(t) return t.___ end}
function setDefault(t,d)
t.___=d
setmetatable(t,mt)
end
防止冲突还有一种写法,确保这个特殊key
的唯一性
local key={} --唯一key
local mt={__index=function(t) return t.[key] end}
function setDefault(t,d)
t.[key]=d
setmetatable(t,mt)
end
4. 跟踪table的访问
_index
和_newindex
都是在table中没有需要访问的index
才发挥作用的。因此只有讲一个table
保持空,才可能捕捉到所有对他的访问。
t={} --原来的table
local _t=t; --保持一个对原来的table的引用
t={} --创建代理
local mt={
__index=function(t,k)
print("get element"..k)
return _t[k] --从本来的table中获取get数值
end,
__newindex=function(t,k,v)
print("set element"..k)
_t[k]=v --像本来的table中set数值
end
}
setmetatable(t, mt)
t[1]=1
t[2]=2
print(t[1])
输出结果:
set element1
set element2
get element1
1
但这种写法有一个弊端:无法遍历原本的table。pairs
只能够访问到代理的table
但是这里又有一个问题。如果我们需要同时监视多个table,其实无需创建多个不同的元表。我们只要以某种形式让每个代理和元表关联起来,并且所有代理都共享一个公共的元表,这里上代码
local index={} --当做一个不容易重复的key值
local mt={
__index=function(t,k)
print("get data"..k)
return t[index][k]
end,
__newindex=function(t,k,v)
print("set data"..k)
t[index][k]=v
end
}
function track(t)
local proxy={} --此处每次都会创建新的table,此table会作为代理返回
print("proxy adress is "..tostring(proxy))
print("index adress is "..tostring(index))
proxy[index]=t --真正的数据,放在table内某个key值能难重复的地方
setmetatable(proxy, mt)
return proxy
end
t={}
t=track(t)
t[1]=45
print(t[1])
t1={}
t1=track(t1)
print(t1[1])
所有的代理都存放在元表内,而真正的表又在代理内。外部无法直接获取到对应的值
5. 只读的table
我们可以通过代理概念,很轻松的创造出实现只读的元表。具体只需要在__newindex是取消更新操作并引发一个错误提示。具体代码不在演示。
最后所有重载的一览
函数名 | 意义&注意事项 |
---|---|
_add | + 操作,如果任何不是数字的值(包括不能转换为数字的字符串)做加法, Lua 就会尝试调用元方法。 首先、Lua 检查第一个操作数(即使它是合法的), 如果这个操作数没有为 "__add" 事件定义元方法, Lua 就会接着检查第二个操作数。 一旦 Lua 找到了元方法, 它将把两个操作数作为参数传入元方法, 元方法的结果(调整为单个值)作为这个操作的结果。 如果找不到元方法,将抛出一个错误。 |
__sub | - 操作。 行为和 "add" 操作类似。 |
__mul | * 操作。 行为和 "add" 操作类似。 |
__div | / 操作。 行为和 "add" 操作类似 |
__mod | % 操作。 行为和 "add" 操作类似 |
__pow | ^ (次方)操作。 行为和 "add" 操作类似 |
__unm | - (取负)操作。 行为和 "add" 操作类似。 |
__concat | .. (连接)操作。 行为和 "add" 操作类似, 不同的是 Lua 在任何操作数即不是一个字符串 也不是数字(数字总能转换为对应的字符串)的情况下尝试元方法。 |
__idiv | // (向下取整除法)操作。 行为和 "add" 操作类似。 |
__band | & (按位与)操作。 行为和 "add" 操作类似, 不同的是 Lua 会在任何一个操作数无法转换为整数时 (参见 §3.4.3)尝试取元方法。 |
__bor | |(按位或)操作。 行为和 "band" 操作类似。 |
__bxor | ~ (按位异或)操作。 行为和 "band" 操作类似。 |
__bnot | ~ (按位非)操作。 行为和 "band" 操作类似。 |
lua手册