Skip to content

2.4 - 元表和元函数

每个值都可以有 元表(metatable)元表 是定义了原始数据在某些事件下行为的一个普通Lua表。你可以通过设置其元表的某些特定属性来改变某个值的某些行为。举个例子,一个非数字值进行加法操作时,Lua会在这个值的元表中查找__add属性函数,找到了的情况下,Lua就会调用这个函数来执行加法操作。

元表中的每个事件对应的键都是一个字符串,内容是以两个下划线做前缀的事件名,其相应的值被称为 元值(metavalue)。对于大部分事件,其元值必须是一个称为 元函数(metamethod) 的方法。在上边说的例子里,键值是“_add”字符串且元函数是一个用来做加法操作的方法。若非另有说明,元函数实际上可以是任意可调用的值,它要么是个函数,要么是个带有元方法“__call”的值。

你可以使用getmetatable方法来查询任何值的元表。Lua使用原始访问(参见rawget)来查询元表中的元函数。

你可以使用setmetatable方法来替换表的元表。你不能从Lua代码中改变其他类型的元表,除非使用调试库(参见6.10)。

表和full userdata有单独的元表,尽管多个表和userdata之间可以共享它们的元表。其他类型的值共享每个类型的单独元表;即,存在一个单独的元表给所有数字使用,一个单独的元表给所有的字符串使用,等等。默认情况下,值没有元表,但是字符串库给字符串类型设置了一个元表(参见6.4)。

下面给出了关于元表控制的操作的详细列表。每种事件由对应的键标识。按约定,所有的元表键由两个下划线后跟小写拉丁字母组合而成。

  • __add: 加法(+)操作。如果任何一个加法操作的操作数不是一个数字,Lua将尝试调用元函数。它从第一个操作数开始检查(即使它是数字),如果它没有为__add定义元函数,Lua将继续检查第二个操作数。如果Lua可以找到了元函数,那么Lua将两个操作数为参数来调用元函数,且将调用结果(调整为单个值)作为作为操作的结果。反之,如果没有找到元函数,Lua会抛出一个错误。
  • __sub: 减法(-)操作。行为类似于加法操作。
  • __mul: 乘法(*)操作。行为类似于加法操作。
  • __div: 除法(/)操作。行为类似于加法操作。
  • __pow: 幂(^)操作。行为类似于加法操作。
  • __unm: 取负(一元 -)操作。行为类似于加法操作。
  • __idiv: 整除(//)操作。行为类似于加法操作。
  • __band: 按位与(&)操作。行为类似于加法操作,不同之处在于当操作数既不是整数也不是可强转到整数的浮点数时(参见3.4.3),Lua将尝试调用元函数。
  • __bor: 按位或(|)操作。行为类似于按位与操作。
  • __bxor: 按位异或(~)操作。行为类似于按位与操作。
  • __bnot: 按位取反(一元 ~)操作。行为类似于按位与操作。
  • __shl: 位左移(<<)操作。行为类似于按位与操作。
  • __shr: 位右移(>>)操作。行为类似于按位与操作。
  • __concat: 连接(..)操作。行为类似于加法操作,不同之处在于当操作数既不是字符串也不是数字时(数字定能被转换为一个字符串),Lua将尝试调用元函数。
  • __len: 取长(#)操作。如果对象不是一个字符串,Lua将尝试调用其元函数。如果元函数存在,则调用将对象作为参数调用元函数,并将调用结果(通常调整为单个值)作为操作结果。如果元表不存在但是对象是table,Lua使用表的取长操作(参见3.4.7)。否则,Lua抛出将会抛出错误。
  • __eq: 判断相等(==)操作。行为类似于加法操作,不同之处在于当这些值都是表或都是full userdata且它们底层不相等时,Lua将会尝试调用元函数。其结果总是会被转换为一个布尔值。
  • __lt: 判断小于(<)操作。行为类似于加法操作,不同之处在于当这些值既不是都是数字也不都是字符串时,Lua将尝试调用元函数。另外,其结果总是会被转换为一个布尔值。
  • __le: 判断小于等于(<=)操作。行为类似于判断小于操作。
  • __index: 访问索引(table[key])操作。这个操作发生在table不是一个表或者key不存在于table中的情况下。此时将会在table的元表中查找其元值。
    此事件的元值可以是一个方法、一个表、或者任何带有__index元值的值。如果是方法,它会将tablekey作为参数来调用,调用结果(调整为单值)作为操作结果。否则,最终的结果是其元值索引key的结果。此索引是常规索引,而非直接索引,所以可以出发其他的__index元值。
  • __newindex: 赋值索引(table[key])操作。像index事件一样,在table不是一个表或者key不存在于table中时,将会在table的元表中查找其元值。
    与索引类似,此元值可以是方法、表、或者任何带有__newindex元值的值。如果是方法,它会将tablekeyvalue作为参数来调用。否则,Lua将再次对这个元值做索引赋值。这里的赋值流程是常规赋值,而不是直接的赋值,所以它可能会触发其他地方的__newindex元值。
    无论何时,当__newindex元值被调用,Lua不会执行任何更多的赋值操作。如果需要,元函数自身可以调用rawset来做赋值。
  • __call: 调用方法func(args)操作。此事件发生在Lua尝试调用一个non-function值(即,func不是个方法)的时候。将在func中寻找此元函数。如果存在,会将func作为第一个参数,再在后边加上其原本调用的参数列表来调用此元函数。所有此操作的结果都将作为其调用结果。这是唯一一个允许多个返回结果的元函数。

除了上述列表外,解释器还遵循了以下元表中的键:__gc(参见2.5.3),__close(参见3.3.8),__mode(参见2.5.4),以及__name。(tostring和错误消息中可能会用到包含字符串的__name。)

对于一元操作(取负、取长度和按位取否)而,元函数是使用虚拟的第二个操作数来计算和调用的,其等于第一个操作数。这个多余的操作数只是为了简化Lua的内部结构(使得这些操作和二元操作做相似的行为),并且在未来的版本中可能会删掉这些。对于大部分用途,这个多余的操作数都是无所谓的。

因为元表其实是常规的表,所以它们可以包含任意属性,而不只是定义上边提到的事件名。有些标准库中的函数就为了其自己的目的而使用了元表上的其他属性。

在一个好的实现中,会在给一些对象设置元表前把所有需要的元函数都加到一个表上;特别是__gc元函数,它只能用这种方式才能起效(参见2.5.3)。在一个对象被创建后立刻设置元表也是个好的实现。