补充一下metatable (元表)的知识。
lua中的table拥有一些列可预见的操作。
比如申明一个普通的table:a = {x=1,y=2}
,如果我们访问a.x,那么打印1,如果我们访问a.y,那么打印2,如果我们访问a.z,那么打印nil。但是如果我们想要a.z有值呢?当然不是简单的a.z = xx
赋值这么简单的方法。这时候就需要我们的元表了。
lua提供了内建函数getmetatable
和setmetatable
lua的元表可以是自身,也可以是别的table:
> a = {x=1,y=2}
> setmetatable(a,a) -- 设置a的元表为自己
table: 003ea930 -- 返回table a
> getmetatable(a) -- 返回a的元表
table: 003ea930
> b = {z=3}
> setmetatable(a,b) -- 设置a的元表为b
table: 003ea930
> getmetatable(a)
table: 003eb328
> a.z -- 猜猜看我们能打印出3吗?
好,既然上面不能打印3那就疑问了,我们设置这个干啥呢?这个需要配合元方法使用!上面这个例子后面还会说,我们先来看最简单的元方法-算术元方法。
举个例子,通常我们想要两个table实现+
运算是不行的:
> a = {1}
> b = {2}
> a + b
stdin:1: attempt to perform arithmetic on a table value (global 'a')
stack traceback:
stdin:1: in main chunk
[C]: in ?
好,下面我们来定义一个集合,保存为set.lua
文件:
Set = {}
Set.mt = {}
function Set.new(t)
local set = {}
setmetatable(set,Set.mt)
for _,v in pairs(t) do
table.insert(set,v)
end
return set
end
function Set.add(a,b)
local ret = Set.new{} -- 等价于 Set.new({}),以前提过
for _,v in pairs(a) do
table.insert(ret,v)
end
for _,v in pairs(b) do
table.insert(ret,v)
end
return ret
end
Set.mt.__add = Set.add -- 注意这里的 __add
我们引入这个模块,然后执行我们的加法:
> require "set"
true
> s1 = Set.new{1,2}
> s2 = Set.new{4,5}
> s3 = s1 + s2
> for k,v in pairs(s3) do print(k,v) end
1 1
2 2
4 4
5 5
到这里,我们发现我们可以加法了呢。。。为啥呢???
仔细看我们上面的元表Set.mt
,我们对它增加了一个方法Set.mt.__add
,如果我们尝试把它赋值为nil:
> Set.mt.__add = nil
> s1 + s2 -- 报错了。
stdin:1: attempt to perform arithmetic on a table value (global 's1')
stack traceback:
stdin:1: in main chunk
[C]: in ?
> Set.mt.__add = Set.add -- 这里再改回来!
所以呢,我们两个Set能相加,多亏了我们的__add
方法。我们称之为元方法,类似的方法还有很多:
另外,lua对于自定义类型(table)的操作运算遵循:
- 如果第一个值有元表,并且有对应的操作符重载,lua选择它作为这次运算的方法。不依赖第二个值
- 否则,如果第二个值有元表,并且有对应的操作符重载,lua选择它作为这次运算的方法。
- 否则,lua raises an error(报错)
按照上面的规则,1+s1
和s1+1
是等价的,想一想为什么?
如果我们直接去运算 s1+1
会报错,我们需要加条件判断:
function Set.add(a,b)
-- 增加对a和b的判断
if getmetatable(a) ~= Set.mt or getmetatable(b) ~= Set.mt then
error("attempt to `add' a set with a non-set value")
end
local ret = Set.new{} -- 等价于 Set.new({})
for _,v in pairs(a) do
table.insert(ret,v)
end
for _,v in pairs(b) do
table.insert(ret,v)
end
return ret
end
再试一下:
> s1 + 1 -- 现在就是报我们自己的error了
.\set.lua:17: attempt to `add' a set with a non-set value
stack traceback:
[C]: in function 'error'
.\set.lua:17: in metamethod '__add'
stdin:1: in main chunk
[C]: in ?
还有关系元方法:
__eq 重载 == -- 没有~=,可以用 not ==
__lt 重载 < -- 没有>,可以换个方向
__le 重载 <= -- 没有>=,可以换个方向
改造一下我们的脚本:
function Set.check(a,b)
-- 增加对a和b的判断
if getmetatable(a) ~= Set.mt or getmetatable(b) ~= Set.mt then
error("attempt to operate a set with a non-set value")
end
end
function Set.le(a,b)
Set.check(a,b)
return #a <= #b
end
function Set.lt(a,b)
return a <= b and not (b <= a)
end
function Set.eq(a,b)
return a <= b and b <= a
end
Set.mt.__le = Set.le
Set.mt.__lt = Set.lt
Set.mt.__eq = Set.eq
退出重新引入一下:
> Set.new{1,2,3} == Set.new{4,5,6} -- 长度一致
true
> {1,2,3} == {4,5,6} -- 没有重载就会去判断对象是否同一个
false
> {1,2,3} == {1,2,3} -- 这里跟python不同
false
关系元方法跟算术元方法不一样,它不支持混合类型。如果你比较两个不同类型的数据(string和number)的大小,或者拥有不同关系元方法的对象的大小,lua将报错,但是==
是另外。==的两边如果是不同类型的数据,那么直接返回false,或者都是table,但是他们的关系元方法不一样,那么也是返回false,仅当都是对象(table),且关系元方法一致,那么lua将会调用这个元方法。
还有一些其他的元方法:
__tostring 重载内建函数 tostring()
__metatable 重载内建函数 getmetatable()
看例子:
对我们的脚本增加下面代码:
function Set.checkset(a)
if getmetatable(a) ~= Set.mt then
error("attempt to operate with a non-set value")
end
end
function Set.tostring(a)
Set.checkset(a)
local ret = "{"
local sep = ""
for k,v in pairs(a) do
ret=ret..sep..v
sep = ","
end
return ret.."}"
end
重新引入:
> s1 = Set.new{1,2,3}
> s1
{1,2,3} -- 这就是我们想要的打印.
-- 如果不想别人修改你的metatable,那么我们可以定义一个__metatable
> Set.mt.__metatable = "not your business"
> getmetatable(s1)
not your business
> setmetatable(s1,s1)
stdin:1: cannot change a protected metatable
stack traceback:
[C]: in function 'setmetatable'
stdin:1: in main chunk
[C]: in ?
-- 无法修改保护的metatable,OK目的达成
以上基础元方法已经介绍完毕,最后看两个特殊的元方法__index
和__newindex
,它们就可以完成我们一开始的a.z
任务_
所有的table,它们在访问成员的时候其实通过__index
方法去找的,如果找到返回这个值,如果没找到返回nil。
先来看看我们的最新脚本文件set.lua:
Set = {}
Set.mt = {}
Set.mt.__metatable = "sorry abourt this"
function Set.new(t)
local set = {}
setmetatable(set,Set.mt)
for _,v in pairs(t) do
table.insert(set,v)
end
return set
end
function Set.checkset(a)
if getmetatable(a) ~= Set.mt.__metatable then
error("attempt to operate with a non-set value")
end
end
function Set.check(a,b)
-- 增加对a和b的判断
if getmetatable(a) ~= Set.mt.__metatable
or getmetatable(b) ~= Set.mt.__metatable then
error("attempt to operate a set with a non-set value")
end
end
function Set.add(a,b)
Set.check(a,b)
local ret = Set.new{} -- 等价于 Set.new({})
for _,v in pairs(a) do
table.insert(ret,v)
end
for _,v in pairs(b) do
table.insert(ret,v)
end
return ret
end
function Set.le(a,b)
Set.check(a,b)
return #a <= #b
end
function Set.lt(a,b)
return a <= b and not (b <= a)
end
function Set.eq(a,b)
return a <= b and b <= a
end
function Set.tostring(a)
Set.checkset(a)
local ret = "{"
local sep = ""
for k,v in pairs(a) do
ret=ret..sep..v
sep = ","
end
return ret.."}"
end
Set.mt.__add = Set.add
Set.mt.__le = Set.le
Set.mt.__lt = Set.lt
Set.mt.__eq = Set.eq
Set.mt.__tostring = Set.tostring
重新引入文件,然后看这个例子:
> s1 = Set.new{1,2,3} -- 申明一个新的Set
> s1 -- 看打印
{1,2,3}
-- 我们申明__index函数,打印参数table和key,并返回nil
> Set.mt.__index = function(tab,key) print(tab,key) return nil end
> s1.a -- 跟原来一样
a
> s1.b -- 区别来了。。。
{1,2,3,a} b
nil
通过上面的例子我们看到,当调用s1里面含有的key的时候跟原来一样,但是s1.b
的时候,由于我们的s1里面并不含有,所以就调用了我们给__index
赋值的函数,打印输出,当然结果还是nil.
回到最开始的例子(a.z):
> a = {x=1,y=2}
> b = {z=3}
> setmetatable(a,b)
table: 003eee58
> b.__index = function(tab,key) return b[key] end
> a.z
3 -- OK 我们想要的结果来了~
其实我们的__index
可以是function
也可以是table,如果是function,那么会把调用的table和key作为参数传过去;如果是table,那么lua只是重新查询,如果查到返回,没有则继续调用这个table的__index
元方法。
利用这一点我们可以完成类的继承这样的功能。具体后面再细说。
如果我们只是想访问这个table的元素,避开它的__index
调用,我们可以调用内建函数rawget(table,key)
:
> rawget(a,"x") -- 避开__index
1
> rawget(a,"z") -- 避开__index
nil
rawget
函数并不能给我们的程序提速,但是我们有时候需要用它。
__newindex
元方法是更新table,当我们给table赋值一个本不存在的key的时候,lua解释器先去找这个table的__newindex
,如果不为nil,则调用它;如果为nil,就对table做赋值操作:
> a = {}
> setmetatable(a,a)
table: 0033aa88
> a.__newindex = function(tab,k,v) return print(tab,k,v) end
> a.x = 1
table: 0033aa88 x 1
>a.x
nil
__newindex
的值可以是table,这样赋值操作将会在那个table上发生,原table不会做任何事。
> a = {}
> b = {}
> setmetatable(a,b)
table: 00c6f0a0
> b.__newindex = b -- 想想这里能不能赋值 a ?
> a.x = 1
> a.x
nil
> b.x
1
> rawset(a,'y',2) -- 这个跟rawget有点类似,避开__newindex
table: 0068a9c0
> a.y
2
> b.y
nil
> b.__index = b
> a.x
1
table的默认值是nil,我们可以用__index
修改这个默认值,想想可以怎么做?
利用__index
和__newindex
我们可以监视某个空table的操作:
-- create private index
local index = {}
-- create metatable
local mt = {
__index = function (t,k)
print("*access to element " .. tostring(k))
return t[index][k] -- access the original table
end,
__newindex = function (t,k,v)
print("*update of element " .. tostring(k) ..
" to " .. tostring(v))
t[index][k] = v -- update original table
end
}
function track (t)--监视函数,要监视某个table,只需t=track(t)
local proxy = {}
proxy[index] = t
setmetatable(proxy, mt)
return proxy
end
把table变为只读:
function readOnly (t)
local proxy = {}
local mt = { -- create metatable
__index = t,
__newindex = function (t,k,v)
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end
使用方法:
days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday"}
print(days[1]) --> Sunday
days[2] = "Noday"
stdin:1: attempt to update a read-only table
最后讲一下这个 __call 元方法
> a = {}
> mt = {}
> mt.__call = function(...) print("call mt.__call",...) end --定义我们的元方法
> a(1,2,3) --因为我们的a是一个table,所以直接这样调用是不能的。
stdin:1: attempt to call a table value (global 'a')
stack traceback:
stdin:1: in main chunk
[C]: in ?
> setmetatable(a,mt) --这里设置一下元表
table: 00bdcce0
> a(4,5,6)
call mt.__call table: 00bdcce0 4 5 6
> mt
table: 00bdcc18
> a
table: 00bdcce0
>
__call在调用的时候,默认会把对象作为第一个参数也传入进去,上面的例子就是把a也传入进去。所以我们定义__call的时候可以写成下面这样:
mt.__call = function(ins,...) print("ins=",ins,...) end
网友评论