《Programming in Lua 3》读书笔记(十)

来源:互联网 发布:能帮人找东西的软件 编辑:程序博客网 时间:2024/06/06 09:43
这一部分应该挺重要的,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"))               -- tableprint(getmetatable(10))                  --nil


Arithmetic Metamethods
算术运算元方法

这里介绍元表的使用,在这里用一个table表示set,我们需要运算set的并集等操作
e.g.Set = {} local mt = {}          --metatable for setsfunction Set.new(l)     --新建一个set,初始化并且设置其元表     local set = {}          setmetatable(set,mt)     for _ v in ipairs(l) do set[v] = true end     return setend

这样每次我们新建一个set其都会有同样的元表:
s1 = Set.new{10,20,11,13}s2 = Set.new{30,1}print(getmetatable(s1))          --table: 0x7fa1eb4093a0print(getmetatable(s2))          --table: 0x7fa1eb4093a0


给元表添加元方法:__add 字段表示table如果执行相加操作
mt.__add = Set.union     --注意此时的 __add 字段还是不能使用的,因为Set.union 还未定义,而且这段代码也需要放在定义Set.union之后,否则会报错。正确的用法是先定义再赋值
--假若mt.__add = Set.union
--定义Set.unionfunction 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 resend
--此时
s3 = s1 + s2 会报错     --attempt to perform arithmetic on global 's1' (a table value)
--需要将mt.__add = Set.union 放置在定义Set.union之后。

同样的,设置 __mul 元方法也是类似的要求
--定义 Set.intersectionfunction Set.intersection( a,b )     local res = Set.new{}     for k in pairs(a) do          res[k] = b[k]     end     return resendmt.__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 resend


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: 0x7f8349403d30print(getmetatable(s))           --table: 0x7f8349409fd0
此时打印出来的并不是其值,但也不是其元表

--to stringfunction Set.tostring( set )     local l = {}     for e in pairs(set) do          l[#l + 1] = e     end     return "{" .. table.concat(l," , ") .. "}"end

mt.__tostring = Set.tostringprint(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 oend
--定义元方法
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)endtab = {x = 10,y = 20}print(tab.x,tab.z)               --10,nilsetDefault(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作为keylocal 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 tablelocal _t = t--create proxy 代理t = {}--create metatablelocal 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 2hello


Read-only tables
只读table

只读table的原理主要就是在试图给table赋值的时候做限制,这里就涉及到了__newindex 元方法的使用。
e.g.--read only tablefunction 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 proxyend
通过在__newindex元方法里面做恰当的修改,便能将我们的table改写为只读table。


0 0
原创粉丝点击