Lua_第 12 章 Metatables and Metamethods

第 12 章 Metatables and Metamethods

Lua中的 table 由于定义的行为,我们可以对 key-value 对执行加操作,访问 key 对 应的 value,遍历所有的 key-value。但是我们不可以对两个 table
执行加操作,也不可以比较两个表的大小。

Metatables 允许我们改变 table 的行为,例如,使用 Metatables 我们可以定义 Lua 如 何计算两个 table 的相加操作 a+b。当 Lua试图对两个表进行相加时,他会检查两个表是 否有一个表有 Metatable,并且检查 Metatable 是否有_add 域。如果找到则调用这个_add 函数(所谓的  Metamethod)去计算结果。Lua
中的每一个表都有其Metatable。(后面我们将看到 userdata 也有 Metatable),Lua默认创建一个不带metatable的新表。

t = {}
print(getmetatable(t))      --> nil

可以使用 setmetatable 函数设置或者改变一个表的 metatable

t1 = {}
setmetatable(t,t1)
assert(getmetatable(t) ==t1)

任何一个表都可以是其他一个表的 metatable,一组相关的表可以共享一个metatable(描述他们共同的行为)。一个表也可以是自身的   metatable(描述其私有行为)。

12.1算术运算的 Metamethods

这一部分我们通过一个简单的例子介绍如何使用 metamethods。假定我们使用 table 来描述结合,使用函数来描述集合的并操作,交集操作,like 操作。我们在一个表内定 义这些函数,然后使用构造函数创建一个集合:

Set = {}

function Set.new (t)
     local set = {}
     for _, l in ipairs(t) do set[l] = true end
     return set
end

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

function Set.intersection (a,b)
     local res = Set.new{}
     for k in pairs(a) do
         res[k] =b[k]
     end
        return res
end

为了帮助理解程序运行结果,我们也定义了打印函数输出结果:

function Set.tostring (set)
   local s = "{"
   local sep = ""
   for e in pairs(set) do
       s = s .. sep .. e
       sep = ", "
   end
   return s .. "}"
end

function Set.print (s)
    print(Set.tostring(s))
end

现在我们想 加号运算符(+)执行两个 集合的并操 作,我们将 所有集合共 享一个metatable,并且为这个 metatable 添加如何处理相加操作。

第一步,我们定义一个普通的表,用来作为 metatable。为避免污染命名空间,我们将其放在 set 内部。

Set.mt = {}       --metatable for sets

第二步,修改 set.new 函数,增加一行,创建表的时候同时指定对应的 metatable。

function Set.new (t)     -- 2nd version
   local set = {}
   setmetatable(set, Set.mt)
   for _ , l in ipairs(t) do set[l] = true end
   return set
end

这样一来,set.new 创建的所有的集合都有相同的 metatable 了:

<pre name="code" class="csharp">s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
print(getmetatable(s1)) --> table: 00672B60
print(getmetatable(s2)) --> table: 00672B60 

第三步,给 metatable 增加 _add 函数。

Set.mt._add = Set.union

当 Lua 试图对两个集合相加时,将调用这个函数,以两个相加的表作为参数。 通过 metamethod,我们可以对两个集合进行相加:

s3 = s1 + s2
Set.print(s3)     --> {1, 10, 20,30, 50}

同样的我们可以使用相乘运算符来定义集合的交集操作

Set.mt._mul = Set.intersection
Set.print((s1 + s2)*s1)     --> {10, 20, 30,50}

对于每一个算术运算符,metatable 都有对应的域名与其对应,除了 _add,   _mul 外,还有_sub(减),_div(除),    _unm(负),__pow(幂),我们也可以定义_concat 定义连接行为。当我们对两个表进行加没有问题,但如果两个操作数有不同的
metatable 例如:

s = Set.new{1,2,3}
s = s + 8

Lua 选择 metamethod 的原则:如果第一个参数存在带有 _add 域的 metatable,Lua使用它作为metamethod,和第二个参数无关;否则第二个参数存在带有
_add域的 metatable,Lua使用它作为 metamethod 否则报错。Lua不关心这种混合类型的,如果我们运行上面的 s=s+8 的例子在 Set.union 发生错误:

bad argument#1 to `pairs' (table expected, got number)

如果我们想得到更加清楚地错误信息,我们需要自己显式的检查操作数的类型:

function Set.union (a,b)
    if getmetatable(a) ~= Set.mt or
          getmetatable(b) ~=Set.mt then
        error("attempt to `add' a set with a non-set value", 2)
   end
... --same as before

12.2关系运算的 Metamethods

Metatables 也允许我们使用metamethods: _eq(等于),_lt(小于),和 _le(小于 等于)给关系运算符赋予特殊的含义。对剩下的三个关系运算符没有专门的 metamethod, 因为 Lua 将 a ~= b 转换为not
(a == b);a > b 转换为 b < a;a >= b 转换为 b <= a。

(直到 Lua 4.0 为止,所有的比较运算符被转换成一个,a <= b 转为 not (b < a)。然 而这种转换并不一致正确。当我们遇到偏序(partial order)情况,也就是说,并不是所 有的元素都可以正确的被排序情况。例如,在大多数机器上浮点数不能被排序,因为他的值不是一个数字(Not a Number 即 NaN)。根据 IEEE 754 的标准,NaN 表示一个未定
义的值,比如 0/0 的结果。该标准指出任何涉及到 NaN 比较的结果都应为 false。也就是 说,NaN <= x 总是 false,x < NaN 也总是 false。这样一来,在这种情况下 a <= b 转换 为 not(b < a)就不再正确了。)

在我们关于基和操作的例子中,有类似的问题存在。<=代表集合的包含:a <= b 表 示集合 a 是集合 b 的子集。这种意义下,可能 a<= b 和 b < a 都是 false;因此,我们需 要将 _le 和 _lt 的实现分开:

Set.mt._le =function (a,b)    --set containment
     for k in pairs(a) do
        if not b[k] then return falseend end
     end
     return true
end

Set.mt._lt = function (a,b)
    return a <= b and not (b <= a)
end

最后,我们通过集合的包含来定义集合相等:

Set.mt._eq = function (a,b)
    return a <= b and b <= a
end

有了上面的定义之后,现在我们就可以来比较集合了:

s1 =Set.new{2, 4}
s2 =Set.new{4, 10, 2}
print(s1 <=s2)          --> true
print(s1 <s2)           --> true
print(s1 >=s1)          --> true
print(s1 >s1)           --> false
print(s1 == s2 * s1)     --> true

与算术运算的 metamethods 不同,关系元算的 metamethods 不支持混合类型运算。 对于混合类型比较运算的处理方法和 Lua的公共行为类似。如果你试图比较一个字符串 和一个数字,Lua 将抛出错误.相似的,如果你试图比较两个带有不同 metamethods 的对 象,Lua 也将抛出错误。

但相等比较从来不会抛出错误,如果两个对象有不同的 metamethod,比较的结果为 false,甚至可能不会调用 metamethod。这也是模仿了 Lua的公共的行为,因为 Lua 总是 认为字符串和数字是不等的,而不去判断它们的值。仅当两个有共同的metamethod的对 象进行相等比较的时候,Lua 才会调用对应的 metamethod。

12.3库定义的 Metamethods

在一些库中,在自己的 metatables 中定义自己的域是很普遍的情况。到目前为止,我们看到的所有 metamethods 都是 Lua 核心部分的。有虚拟机负责处理运算符涉及到的 metatables 和为运算符定义操作的 metamethods。但是,metatable 是一个普通的表,任何 人都可以使用。tostring 是一个典型的例子。如前面我们所见,tostring 以简单的格式表示出
table:

print({})     --> table: 0x8062ac0

(注意:print 函数总是调用 tostring 来格式化它的输出)。然而当格式化一个对象的时候,tostring 会首先检查对象是否存在一个带有 tostring 域的 metatable。如果存在则 以对象作为参数调用对应的函数来完成格式化,返回的结果即为tostring 的结果。

在我们集合的例子中我们己经定义了一个函数来将集合转换成字符串打印出来。因此,我们只需要将集合的 metatable 的  tostring 域调用我们定义的打印函数:

Set.mt._tostring = Set.tostring

这样,不管什么时候我们调用 print 打印一个集合,print 都会自动调用 tostring,而 tostring  则会调用Set.tostring:

s1 =Set.new{10, 4, 5}
print(s1)     --> {4, 5, 10}

setmetatable/getmetatable 函数也会使 用 metafield ,在这种情 况下,可以 保护metatables。假定你想保护你的集合使其使用者既看不到也不能修改 metatables。如果你 对 metatable 设置了 _metatable
的值,getmetatable 将返回这个域的值,而调用 setmetatable 将会出错:

Set.mt._metatable = "not yourbusiness"

s1 =Set.new{}
print(getmetatable(s1))     --> not your business
setmetatable(s1, {})
stdin:1: cannotchange protected metatable

12.4表相关的 Metamethods

关于算术运算和关系元算的 metamethods 都定义了错误状态的行为,他们并不改变 语言本身的行为。针对在两种正常状态:表的不存在的域的查询和修改,Lua 也提供了 改变 tables 的行为的方法。

12.4.1The __index Metamethod

前面说过,当我们访问一个表的不存在的域,返回结果为nil,这是正确的,但并不 一致正确。实际上,这种访问触发 lua 解释器去查找 _index metamethod:如果不存在, 返回结果为 nil;如果存在则由_index metamethod
 返回结果。

这个例子的原型是一种继承。假设我们想创建一些表来描述窗口。每一个表必须描 述窗口的一些参数,比如:位置,大小,颜色风格等等。所有的这些参数都有默认的值,当我们想要创建窗口的时候只需要给出非默认值的参数即可创建我们需要的窗口。第一 种方法是,实现一个表的构造器,对这个表内的每一个缺少域都填上默认值。第二种方 法是,创建一个新的窗口去继承一个原型窗口的缺少域。首先,我们实现一个原型和一个构造函数,他们共享一个
metatable:

-- create anamespace
Window = {}
-- create theprototype with defaultvalues
Window.prototype ={x=0, y=0, width=100, height=100, }
-- create ametatable
Window.mt = {}
-- declare theconstructor function
function Window.new (o)
   setmetatable(o,Window.mt)
   return o
end

现在我们定义_index metamethod:

Window.mt._index = function (table, key)
      return Window.prototype[key]
end

这样一来,我们创建一个新的窗口,然后访问他缺少的域结果如下:

w = Window.new{x=10, y=20}
print(w.width)   --> 100

当Lua 发现 w 不存在域width 时,但是有一个 metatable 带有 _index 域,Lua 使用 w(the table)和 width(缺少的值)来调用 _index metamethod,metamethod 则通过访问 原型表(prototype)获取缺少的域的结果。

_index metamethod 在继承中的使用非常常见,所以 Lua 提供了一个更简洁的使用方式。_index metamethod 不需要非是一个函数,他也可以是一个表。但它是一个函数 的时候,Lua 将 table 和缺少的域作为参数调用这个函数;当他是一个表的时候,Lua 将 在这个表中看是否有缺少的域。所以,上面的那个例子可以使用第二种方式简单的改写 为:

Window.mt._index = Window.prototype

现在,当 Lua 查找 metatable 的__index 域时,他发现 window.prototype 的值,它是 一个表,所以 Lua 将访问这个表来获取缺少的值,也就是说它相当于执行:

Window.prototype["width"]

将一个表作为 _index metamethod 使用,提供了一种廉价而简单的实现单继承的方法。一个函数的代价虽然稍微高点,但提供了更多的灵活性:我们可以实现多继承,隐藏,和其他一些变异的机制。

当我们想不通过调用  indexmetamethod 来访问一个表,我们可以使用 rawget函数。Rawget(t,i)的调用以 raw  access 方式访问表。这种访问方式不会使你的代码变快Cthe overhead of a function call kills any gain you could have),但有些时候我们需要他,在后面我们将会看到。

   12.4.2The __newindex Metamethod   

_newindex metamethod 用来对表更新, _index 则用来对表访问。当你给表的一个缺少的域赋值,解释器就会查找_newindex metamethod:如果存在则调用这个函数而不 进行赋值操作。像 _index 一样,如果 metamethod 是一个表,解释器对指定的那个表, 而不是原始的表进行赋值操作。另外,有一个 raw 函数可以绕过 metamethod:调用
rawset(t,k,v)不掉用任何 metamethod 对表 t 的 k 域赋值为 v。 index 和 newindex metamethods的混合使用提供了强大的结构:从只读表到面向对象编程的带有继承默认 值的表。

 12.4.3有默认值的表

在一个普通的表中任何域的默认值都是 nil。很容易通过 metatables 来改变默认值:

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

现在,不管什么时候我们访问表的缺少的域,他的_index metamethod 被调用并返 回 0。setDefault函数为每一个需要默认值的表创建了一个新的 metatable。在有很多的表 需要默认值的情况下,这可能使得花费的代价变大。然而 metatable 有一个默认值 d 和它 本身关联,所以函数不能为所有表使用单一的一个 metatable。为了避免带有不同默认值
的所有的表使用单一的 metatable,我们将每个表的默认值,使用一个唯一的域存储在表 本身里面。如果我们不担心命名的混乱,我可使用像" "作为我们的唯一的域:

local mt = { index =function (t) return t._<span style="font-family: Arial, Helvetica, sans-serif;">end}</span>

function setDefault (t, d)
    t._ = d
    setmetatable(t, mt)

end

如果我们担心命名混乱,也很容易保证这个特殊的键值唯一性。我们要做的只是创 建一个新表用作键值:

local key = {}    -- unique key
local mt = {_index =function (t) return t[key] end}
function setDefault (t, d)
    t[key] = d
    setmetatable(t, mt)
end

另外一种解决表和默认值关联的方法是使用一个分开的表来处理,在这个特殊的表 中索引是表,对应的值为默认值。然而这种方法的正确实现我们需要一种特殊的表:weak table,到目前为止我们还没有介绍这部分内容。为了带有不同默认值的表可以重用相同的原表,还有一种解决方法是使用
memoize metatables,以后说。

12.4.4 监控表

_index和 _newindex 都是只有当表中访问的域不存在时候才起作用。捕获对一个表的所有访问情况的唯一方法就是保持表为空。因此,如果我们想监控一个表的所有访 问情况,我们应该为真实的表创建一个代理。这个代理是一个空表,并且带有 _index和 _newindexmetamethods,由这两个方法负责跟踪表的所有访问情况并将其指向原始的表。假定,t  是我们想要跟踪的原始表,我们可以:

t = {}     --original table (createdsomewhere)

 -- keep aprivate access tooriginal table
local _t = t

 -- create proxy
t = {}

 -- create metatable
local mt = {
  _index = function (t,k)
  print("*access to element " .. tostring(k))
  return _t[k]  -- access the originaltable
  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 的访问情况:

<pre name="code" class="csharp">> t[2] = 'hello'
*update of  element 2 to hello
> print(t[2])
*access to element 2
hello

(注意:不幸的是,这个设计不允许我们遍历表。Pairs  函数将对 proxy  进行操作,而不是原始的表。)如果我们想监控多张表,我们不需要为每一张表都建立一个不同的 metatable。我们只要将每一个 proxy 和他原始的表关联,所有的 proxy
共享一个公用的 metatable 即可。将表和对应的 proxy 关联的一个简单的方法是将原始的表作为 proxy 的 域,只要我们保证这个域不用作其他用途。一个简单的保证它不被作他用的方法是创建 一个私有的没有他人可以访问的 key。将上面的思想汇总,最终的结果如下:

-- create privateindex
local index = {}

-- create metatable
local mt = {
  _index = function (t,k)
  print("*access to element" .. tostring(k))
  return t[index][k]   -- access theoriginal 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) local proxy = {} proxy[index] = t
setmetatable(proxy, mt)
return proxy
end

现在,不管什么时候我们想监控表 t,我们要做得只是 t=track(t)。

 12.4.5只读表

采用代理的思想很容易实现一个只读表。我们需要做得只是当我们监控到企图修改 表时候抛出错误。通过 _index metamethod,我们可以不使用函数而是用原始表本身来使 用表,因为我们不需要监控查寻。这是比较简单并且高效的重定向所有查询到原始表的方法。但是,这种用法要求每一个只读代理有一个单独的新的 metatable,使用 _index 指向原始表:

function readOnly (t)
  local proxy = {}
  local mt = {         -- create metatable
  _index = t,
  _newindex = function (t,k,v)
  error("attempt to updatea read-only table", 2
end
)
  setmetatable(proxy, mt)
  return proxy

end

(记住:error的第二个参数 2,将错误信息返回给企图执行 update 的地方)作为一 个简单的例子,我们对工作日建立一个只读表:

days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}

print(days[1])       --> Sunday
days[2] = "Noday"
stdin:1: attemptto update aread-only table
时间: 2024-10-30 23:44:11

Lua_第 12 章 Metatables and Metamethods的相关文章

Lua_第16 章 Weak 表

Lua_第16 章 Weak 表 Lua自动进行内存的管理.程序只能创建对象(表,函数等),而没有执行删除对象 的函数.通过使用垃圾收集技术,Lua 会自动删除那些失效的对象.这可以使你从内存 管理的负担中解脱出来.更重要的,可以让你从那些由此引发的大部分 BUG中解脱出 来,比如指针挂起(dangling   pointers)和内存溢出. 和其他的不同,Lua 的垃圾收集器不存在循环的问题.在使用循环性的数据结构的 时候,你无须加入特殊的操作;他们会像其他数据一样被收集.当然,有些时候即使更

Lua_第27章 User-Defined Types in C

Lua_第27章  User-Defined Types in C 在上一章,我们讨论了如何使用 C 函数扩展 Lua 的功能,现在我们讨论如何使用 C 中新创建的类型来扩展 Lua.我们从一个小例子开始,本章后续部分将以这个小例子 为基础逐步加入 metamethods 等其他内容来介绍如何使用 C 中新类型扩展 Lua. 我们的例子涉及的类型非常简单,数字数组.这个例子的目的在于将目光集中到 API 问题上,所以不涉及复杂的算法.尽管例子中的类型很简单,但很多应用中都会用到这 种类型.一般情

Lua_第19章 String 库(上)

Lua_第19章String 库 Lua解释器对字符串的支持很有限.一个程序可以创建字符串并连接字符串,但不能截取子串,检查字符串的大小,检测字符串的内容.在 Lua中操纵字符串的功能基本来自于 string 库. String 库中的一些函数是非常简单的:string.len(s)返回字符串 s 的长度;string.rep(s, n)返回重复 n 次字符串 s 的串;你使用 string.rep("a", 2^20)可以创建一个 1M bytes 的字符 串(比如,为了测试需要);

公开课视频-《第11章 配置-Citrix-企业网盘-第12章 配置-UPM》

***************************************************************************************** <大企业云桌面部署实战>-培训班-即将开课,包学包会,欢迎咨询:3313395633(QQ) ***************************************************************************************** <大企业云桌面部署实战>-精讲课

JS读书笔记:《JavaScript框架设计》——第12章 异步处理

一.何为异步   执行任务的过程可以被分为发起和执行两个部分. 同步执行模式:任务发起后必须等待直到任务执行完成并返回结果后,才会执行下一个任务. 异步执行模式:任务发起后不等待任务执行完成,而是马上执行下一个任务,当任务执行完成时则会收到通知. 面对IO操作频繁的场景,异步执行模式可在同等的硬件资源条件下提供更大的并发处理能力,也就是更大的吞吐量. 但由于异步执行模式打破人们固有的思维方式,并且任务的发起和任务的执行是分离的,从而提高编程的复杂度. 多线程.多进程均可实现异步模式. 二.从回调

第 12 章 命令模式【Command Pattern】

以下内容出自:<<24种设计模式介绍与6大设计原则>> 今天讲命令模式,这个模式从名字上看就很简单,命令嘛,老大发命令,小兵执行就是了,确实是这个意思,但是更深化了,用模式来描述真是是世界的命令情况.正在看这本书的你,我猜测分为两类:已经工作的和没有工作的,先说没有工作的,那你为啥要看这本书,为了以后工作呗,只要你参见工作,你肯定会待在项目组,那今天我们就以项目组为例子来讲述命令模式. 我是我们部门的项目经理,就是一个项目的头,在中国做项目,项目经理就是什么都要懂,什么都要管,做好

《深入Java虚拟机学习笔记》- 第12章 整数运算

Java虚拟机提供几种进行整数算术运算的操作码,他们执行基于int和long类型的运算.当byte.short和char类型值参与算术运算时,首先会将它们转换为int类型.这些操作码都不会抛出异常,溢出在这里通常可以被忽略. 整数加法 操作码 操作数 说明 iadd (无) 从栈中弹出两个int类型数,相加,然后将所得int类型结果压回栈 ladd (无) 从栈中弹出两个long类型数,相加,然后将所得long类型结果压回栈 将一个常量与局部变量相加 操作码 操作数 说明 iinc vindex

ASM:《X86汇编语言-从实模式到保护模式》第12章:存储器的保护

12章其实是11章的拓展,代码基本不变,就是在保护模式下展开讨论. ★PART1:存储器的保护机制 1. 修改段寄存器的保护 当执行把段选择子传到段寄存器的选择器部分的时候,处理器固件在完成传送之前,要检查和确认选择子是正确的,并且该选择子选择的描述符也是正确的.假如索引号是正确的,也就是说明索引号8+7要小于等于边界.如果超过边界,那么处理器就会终止处理,产生异常中断13,同时段寄存器的原值保持不变. 同时处理器还要对描述符的类别进行检查,如果描述符的类别进行确认,举个例子来说,如果描述符的类

《构建之法》第10、11、12章

第10章问题:书上说一些好的功能还会有副作用,这里所说的副作用是指什么?是指功能没完善还是?(第十章10.2.1  P196第五) 第11章问题:开发阶段还有日常管理,这样不会浪费团队做项目的时间,做一个项目既要做Sprint计划,又要每日例会,还要规格说明书.功能说明书.列典型用户与典型场景                   描述等等,当这些完成后,确定还有时间弄项目吗?(第十一章11.2节) 第12章问题:用户体验和质量,有时候并不能同时做到很好,那么这两个当中那个要重要一点?要牺牲哪个取