《Programming in Lua 3》读书笔记(十)
这一部分应该挺重要的,Lua中唯一的数据结构便是table,几乎所有的的数据操作都是在table的基础上进行。而本文提到的元表和元方法,便是帮助table实现更强大的功能而设计的。
日期:2014.7.11
Part Ⅱ
Metatables and Metamethods
Lua中不能直接对table进行相加、比较等操作。除非使用元表(Metatables)。元表可以使得我们改变元素在处理未定义操作的应对行为,如定义两个table直接的相加操作。Lua在处理两个table的相加操作时会首先检查两个table是否有元表,且元表中是否有 __add 元方法字段,如果有这个字段则lua会遵循这个字段内定义的操作执行两个table的相加操作。
Lua中各个类似的变量有一个相关联的元表?(到底有没有?),而table与userdata有各自独立的元表。默认的,新创建的table是没有元表的:
e.g. t = {} print(getmetatable(t)) --nil
此时我们可以通过setmetatable方法来设置元表,元表其实也相当于一个table
e.g. t1 = {} setmetatable(t,t1) print(getmetatable(t) == t1) --ture
当然,在lua中我们只可以对table执行setmetatable操作,对其余类型的变量执行这个操作需要使用C 代码。书上string库涉及到了给string类型变量执行设置元表的操作。其余类型的变量默认是没有元表的?
print(getmetatable("ss")) -- table print(getmetatable(10)) --nil
Arithmetic Metamethods
算术运算元方法
这里介绍元表的使用,在这里用一个table表示set,我们需要运算set的并集等操作
e.g. Set = {} local mt = {} --metatable for sets function Set.new(l) --新建一个set,初始化并且设置其元表 local set = {} setmetatable(set,mt) for _ v in ipairs(l) do set[v] = true end return set end
这样每次我们新建一个set其都会有同样的元表:
s1 = Set.new{10,20,11,13} s2 = Set.new{30,1} print(getmetatable(s1)) --table: 0x7fa1eb4093a0 print(getmetatable(s2)) --table: 0x7fa1eb4093a0
给元表添加元方法:__add 字段表示table如果执行相加操作
mt.__add = Set.union --注意此时的 __add 字段还是不能使用的,因为Set.union 还未定义,而且这段代码也需要放在定义Set.union之后,否则会报错。正确的用法是先定义再赋值
--假若mt.__add = Set.union
--定义Set.union function Set.union( a,b ) local res = Set.new{} for k in pairs(a) do res[k] = true end for k in pairs(b) do res[k] = true end return res end
--此时
s3 = s1 + s2 会报错 --attempt to perform arithmetic on global 's1' (a table value)
--需要将mt.__add = Set.union 放置在定义Set.union之后。
同样的,设置 __mul 元方法也是类似的要求
--定义 Set.intersection function Set.intersection( a,b ) local res = Set.new{} for k in pairs(a) do res[k] = b[k] end return res end mt.__mul = Set.intersection
所有的算术运算元方法:
__add(加)、__mul(乘)、__sub(减)、__div(除)、__unm(负)、__mod(取模)、__pow(取幂)、__concat(连接)
Lua在处理两个变量的算术运算时,针对不同类型的变量,如
e.g.
s = Set.new {1,2,3}
s = s + 8
此时运行的话会报错:
--bad argument #1 to 'pairs' (table expected, got number)
其处理两个变量的算术运算遵循的步骤是:如果第一个变量定义了元方法则使用第一个变量的元方法,而不会再考虑第二个元素的元方法;第一个没有而第二个有,则使用第二个的;否则就会报错。
因此为了更好的控制程序运行,我们需要限制两个变量为同类型拥有同一个元表,以__add为例,可以如下操作:
function Set.union( a,b ) if getmetatable(a) ~= mt or getmetatable(b) ~= mt then error("xxx",2) --注意这里的参数是2,表示是提醒用户是传递的参数有问题 local res = Set.new{} for k in pairs(a) do res[k] = true end for k in pairs(b) do res[k] = true end return res end
Relational Metamethods
关系运算元方法
Lua中的关系运算元方法主要有:
__eq(相等)、__lt(小于)、__le(小于或等于)。而对于其他的关系操作符,Lua直接做了转换:a ~= b 相当于 not (a == b) 、a > b 相当于 b < a、a >= b 相当于 b <= a;
关系运算元方法的具体使用类似于上文提到的算术运算元方法;
要注意的是,当两个变量的元方法不同的时候执行相等关系运算会返回false;
Library-Defined Metamethods
库定义的元方法
__tostring 元方法
当调用tostring函数的时候,函数首先会寻找变量是否有__tostring 元方法:
同上文:
s = Set.new{1,2,2} print(s) --table: 0x7f8349403d30 print(getmetatable(s)) --table: 0x7f8349409fd0
此时打印出来的并不是其值,但也不是其元表
--to string function Set.tostring( set ) local l = {} for e in pairs(set) do l[#l + 1] = e end return "{" .. table.concat(l," , ") .. "}" end
mt.__tostring = Set.tostring print(s) --{1,2,2}
在设置了其元方法之后,才会正确的打印出其值
当然我们可以通过一定的方法来保护我们的元表,setmetatabe和getmetatable也是使用到了元方法,我们可以根据这一特性达到我们的目的:
e.g. mt.__metatable = "cannot change" s1 = Set.new{} print(getmetatable(s1)) --cannot change
而当我们想改变其元表的时候
e.g. setmetatable(s1,mt) --error:cannot change a protected metatable
会报错,不能修改其元表,这样就达到了我们要保护元表的目的
Table-Access Metamethods
Lua允许通过元表来控制修改和访问table中不存在的元素的行为
The __index metamehod
当我们试图访问一个table中不存在的元素时,我们得到的值将会是nil。这是一般意义上将的,事实上,我们访问table中不存在的元素的时候,会触发编译器寻找__index 元方法,当没有该方法的时候会返回nil;而当该元方法存在被定义了,将会返回该方法定义的操作。
这一个特性对我们使用继承机制,继承默认变量的时候有很大的帮助,书上也是以这个为例子做了讲解:
--有默认变量的table
prototype = {x = 0,y = 0,width = 100,height = 100}
--元表
mt = {}
--构造函数
function new(o) setmetatable(o,mt) return o end
--定义元方法
mt.__index = function(_,key) return prototype[key] end
--创建一个新的table,使用继承机制,需要技能默认变量的table
w = new{x = 10,y = 20} print(w.width) --100
此时w使用到了prototype里面的值
__index 元方法不一定需要是一个函数,也可以是一个table。当该方法是一个函数的是,Lua会执行函数里定义的操作,当是一个table的时候,Lua直接在table中执行访问操作。
函数 rawget(t,i) 可以使得我们访问table各个元素的时候不去调用 __index 操作.将会对t执行raw访问?啥意思
The __newindex metamethod
该元方法的作用表现在更新table中元素值的时候。当我们试图给table中不存在的键赋值的时候,编译器会寻找 __newindex 的元方法,如果有编译器将会执行该方法定义的操作,否则就会直接赋值。这里也有一个函数 rawset(t,k,v),该函数会绕开元方法,直接在t中对键k设置值v。
书中提到,有效的结合__index 和 __newindex 两个元方法的使用将会带来很强大的设计技巧,如创建只读table,带默认值的table等。
Tables with default values
table带有默认的值,其原理主要是在我们访问一个table中不存在或者没有赋值的键的时候,返回值是一个固定值,这里就涉及到了 __index 元方法
e.g. 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 setDefault(tab,0) print(tab.x,tab.z) --10,0
以上操作就为tab设置了默认值0,如果试图访问tab中没有定义或者不存在的元素,返回值将会是0
为多个不同的table执行设置多个不同默认值的操作
e.g. local mt = {__index = function (t) return t.___ end} function setDefault (t,d) t.___ = d setmetatable(t,mt) end
这里的技巧在于,元表的定义在函数的外部,且将默认值存储在了要设置默认值的table本身的内部。
避免命名冲突的操作:
e.g. local key = {} --以一个table作为key local mt = {__index = function (t) return t[key] end} function setDefault (t,d) t[key] = d setmetatable(t,mt) end
Tracking table accesses
有效的使用__index 和 __newindex 可以帮助我们监控对table的访问和赋值操作。结合使用 proxy (代理) 便可以追踪所有对table的访问操作并且追踪到其访问的值。书上提到的只有当table为空的时候才能捕获到所有对其的访问操作,为啥?
t = {} --original table --keep a private access to the original table local _t = t --create proxy 代理 t = {} --create metatable local mt = { __index = function ( t,k ) print("*access to element " .. tostring(k)) return _t[k] end, __newindex = function ( t,k,v ) print("*update of element " .. tostring(k) .. " to " .. tostring(v)) _t[k] = v --update original table end } setmetatable(t,mt) t[2] = "hello" print(t[2])
打印出来的,追踪了table从赋值到访问的过程
*update of element 2 to hello *access to element 2 hello
Read-only tables
只读table
只读table的原理主要就是在试图给table赋值的时候做限制,这里就涉及到了__newindex 元方法的使用。
e.g. --read only table function readOnly (t) local proxy = {} local mt = { __index = t, __newindex = function (t,k,v) error("attempt to update a read-only table",2) --在试图改变元素的时候抛出错误,且参数为2,表示在报错的地方将会是调用该方法的地方。 end } setmetatable(proxy,mt) return proxy end通过在__newindex元方法里面做恰当的修改,便能将我们的table改写为只读table。