1、 Lua 中提供的元表是用于帮助 Lua 数据变量完成某些非预定义功能的个性化行为,如两个 table 的相加。假设 a 和 b 都是 table,通过元表可以定义如何计算表达式 a+b。当 Lua 试图将两个 table 相加时,它会先检查两者之一是否有元表,然后检查该元表中是否存在_add 字段,如果有,就调用该字段对应的值。这个值就是所谓的“元方法”,这个函数用于计算 table 的和。Lua 中每个值都有一个元表。table 和 userdata 可以有各自独立的元表,而其它数据类型的值则共享其类型所属的单一元表。缺省情况下,table 在创建时没有元表,如:t = print(get
2、metatable(t) -输出为 nil这里我们可以使用 setmetatable 函数来设置或修改任何 table 的元表。t1 = setmetatable(t,t1)assert(getmetatable(t) = t1)任何 table 都可以作为任何值的元表,而一组相关的 table 也可以共享一个通用的元表,此元表将描述了它们共同的行为。一个 table 甚至可以作为它自己的元表,用于描述其特有的行为。在 Lua 代码中,只能设置 table 的元表,若要设置其它类型值的元表,则必须通过 C 代码来完成。1. 算术类的元方法:在下面的示例代码中,将用 table 来表示集合,并且
3、有一些函数用来计算集合的并集和交集等。1 Set = 2 local metatable = -元表3 4 -根据参数列表中的值创建一个新的集合5 function Set.new(l)6 local set = 7 -将所有由该方法创建的集合的元表都指定到 metatable8 setmetatable(set,metatable)9 for _, v in ipairs(l) do10 setv = true11 end12 return set13 end14 http:/ 15 -取两个集合并集的函数16 function Set.union(a,b)17 local res = Se
4、t.new18 for k in pairs(a) do19 resk = true20 end21 for k in pairs(b) do22 resk = true23 end24 return res25 end26 27 -取两个集合交集的函数28 function Set.intersection(a,b)29 local res = Set.new30 for k in pairs(a) do31 resk = bk32 end33 return res34 end35 http:/ 36 function Set.tostring(set)37 local l = 38 for
5、 e in pairs(set) do39 l#l + 1 = e40 end41 return “ . table.concat(l,“, “) . “;42 end43 44 function Set.print(s)45 print(Set.tostring(s)46 end47 48 -最后将元方法加入到元表中,这样当两个由 Set.new 方法创建出来的集合进行49 -加运算时,将被重定向到 Set.union 方法,乘法运算将被重定向到 Set.intersection50 metatable._add = Set.union51 metatable._mul = Set.inte
6、rsection52 53 -下面为测试代码54 s1 = Set.new10,20,30,5055 s2 = Set.new30,156 s3 = s1 + s257 Set.print(s3)58 Set.print(s3 * s1)59 60 -输出结果为:61 -1, 30, 10, 50, 2062 -30, 10, 50, 20在元表中,每种算术操作符都有对应的字段名,除了上述的_add(加法)和_mul(乘法)外,还有_sub(减法)、_div(除法)、_unm(相反数)、_mod(取模) 和_pow(乘幂)。此外,还可以定义_concat 字段,用于描述连接操作符的行为。对于上
7、面的示例代码,我们在算术运算符的两侧均使用了 table 类型的操作数。那么如果为 s1 = s1 + 8,Lua 是否还能正常工作呢?答案是肯定的,因为 Lua 定位元表的步骤为,如果第一个值有元表,且存在_add 字段,那么 Lua 将以这个字段为元方法,否则会再去查看第二个值否是有元表且包含_add字段,如果有则以此字段为元方法。最后,如果两个值均不存在元方法,Lua 就引发一个错误。然而对于上例中的 Set.union 函数,如果执行 s1 = s1 + 8 将会引发一个错误,因为 8 不是 table 对象,不能基于它执行 pairs 方法调用。为了得到更准确的错误信息,我们需要给
8、Set.union 函数做如下的修改,如:1 function Set.union(a,b)2 if getmetatable(a) = metatable or getmetatable(b) = metatable then3 error(“attempt to add a set with a non-set value“)4 end5 -后面的代码与上例相同。6 . .7 end2. 关系类的元方法:元表还可以指定关系操作符的含义,元方法分别为_eq(等于)、_lt(小于)和_le(小于等于),至于另外 3 个关系操作符,Lua 没有提供相关的元方法,可以通过前面 3 个关系运算符的取
9、反获得。见如下示例:1 Set = 2 local metatable = 3 4 function Set.new(l)5 local set = 6 setmetatable(set,metatable)7 for _, v in ipairs(l) do8 setv = true9 end10 return set11 end12 13 metatable._le = function(a,b) 14 for k in pairs(a) do15 if not bk then return false end16 end17 return true18 end19 metatable._
10、lt = function(a,b) return a = s1) -true28 print(s1 s1) -false与算术类的元方法不同,关系类的元方法不能应用于混合的类型。3. 库定义的元方法:除了上述基于操作符的元方法外,Lua 还提供了一些针对框架的元方法,如 print 函数总是调用tostring 来格式化其输出。如果当前对象存在_tostring 元方法时, tostring 将用该元方法的返回值作为自己的返回值,如:1 Set = 2 local metatable = 3 4 function Set.new(l)5 local set = 6 setmetatable(
11、set,metatable)7 for _, v in ipairs(l) do8 setv = true9 end10 return set11 end12 13 function Set.tostring(set)14 local l = 15 for e in pairs(set) do16 l#l + 1 = e17 end18 return “ . table.concat(l,“, “) . “;19 end20 21 metatable._tostring = Set.tostring22 23 24 -下面是测试代码:25 s1 = Set.new4,5,1026 print(
12、s1) -5,10,4函数 setmetatable 和 getmetatable 也会用到元表中的一个字段(_metatable),用于保护元表,如:1 mt._metatable = “not your business“2 s1 = Set.new3 print(getmetatable(s1) -此时将打印“not your business“4 setmetatable(s1,) -此时将输出错误信息:“cannot change protected metatable“从上述代码的输出结果即可看出,一旦设置了_metatable 字段,getmetatable 就会返回这个字段的值
13、,而 setmetatable 将引发一个错误。4. table 访问的元方法:算术类和关系类运算符的元方法都为各种错误情况定义了行为,它们不会改变语言的常规行为。但是Lua 还提供了一种可以改变 table 行为的方法。有两种可以改变的 table 行为:查询 table 及修改 table中不存在的字段。1). _index 元方法:当访问 table 中不存在的字段时,得到的结果为 nil。如果我们为该 table 定义了元方法_index,那个访问的结果将由该方法决定。见如下示例代码:1 Window = 2 Window.prototype = x = 0, y = 0, width
14、 = 100, height = 1003 Window.mt = -Window 的元表4 5 function Window.new(o)6 setmetatable(o,Window.mt)7 return o8 end9 10 -将 Window 的元方法_index 指向一个匿名函数11 -匿名函数的参数 table 和 key 取自于 table.key。12 Window.mt._index = function(table,key) return Window.prototypekey end13 14 -下面是测试代码:15 w = Window.newx = 10, y =
15、 2016 print(w.width) -输出 10017 print(w.width1) -由于 Window.prototype 变量中也不存在该字段,因此返回 nil。最后,Lua 为 _index 元方法提供了一种更为简洁的表示方式,如:Window.mt._index = Window.prototype。该方法等价于上例中的匿名函数表示方法。相比而言,这种简洁的方法执行效率更高,但是函数的方法扩展性更强。如果想在访问 table 时禁用_index 元方法,可以通过函数 rawget(table,key)完成。通过该方法并不会加速 table 的访问效率。2). _newinde
16、x 元方法:和_index 不同的是,该元方法用于不存在键的赋值,而前者则用于访问。当对一个 table 中不存在的索引赋值时,解释器就会查找_newindex 元方法。如果有就调用它,而不是直接赋值。如果这个元方法指向一个 table,Lua 将对此 table 赋值,而不是对原有的 table 赋值。此外,和_index 一样,Lua 也同样提供了避开元方法而直接操作当前 table 的函数 rawset(table,key,value),其功能类似于rawget(table,key)。3). 具有默认值的 table:缺省情况下,table 的字段默认值为 nil。但是我们可以通过元表修
17、改这个默认值,如:1 function setDefault(table,default)2 local mt = _index = function() return default end 3 setmetatable(table,mt)4 end5 tab = x = 10, y = 206 print(tab.x,tab.z) -10 nil7 setDefault(tab,0)8 print(tab.x,tab.z) -10 04). 跟踪 table 的访问:_index 和_newindex 都是在 table 中没有所需访问的 index 时才发挥作用的。因此,为了监控某个 t
18、able 的访问状况,我们可以为其提供一个空 table 作为代理,之后再将_index 和_newindex 元方法重定向到原来的 table 上,见如下代码:1 t = -原来的 table2 local _t = t -保持对原有 table 的私有访问。3 t = -创建代理4 -创建元表5 local mt = 6 _index = function(table,key)7 print(“access to element “ . tostring(key)8 return _tkey -通过访问原来的表返回字段值9 end,10 11 _newindex = function(ta
19、ble,key,value)12 print(“update of element “ . tostring(key) . “ to “ . tostring(value)13 _tkey = value -更新原来的 table14 end15 16 setmetatable(t,mt)17 18 t2 = “hello“19 print(t2)20 21 -输出结果为22 -update of element 2 to hello23 -access to element 224 -hello5). 只读的 table:通过代理的概念,可以很容易的实现只读 table。只需跟踪所有对 ta
20、ble 的更新操作,并引发一个错误即可,见如下示例代码:1 function readOnly(t)2 local proxy = 3 local mt = 4 _index = t,5 _newindex = function(t,k,v)6 error(“attempt to update a read-only table“)7 end8 9 setmetatable(proxy,mt)10 return proxy11 end12 13 days = readOnly“Sunday“,“Monday“,“Tuesday“,“Wednesday“,“Thursday“,“Friday“,“Saturday“14 print(days1)15 days2 = “Noday“16 17 -输出结果为:18 -19 Sunday20 lua: d:/test.lua:6: attempt to update a read-only table21 stack traceback:22 C: in function error23 d:/test.lua:6: in function 24 d:/test.lua:15: in main chunk25 C: ?26 -