2.4 元表及元方法

Lua 中的每个值都可以有一个 元表。这个 元表 就是一个普通的 Lua 表,它用于定义原始值在特定操作下的行为。如果你想改变一个值在特定操作下的行为,你可以在它的元表中设置对应域。例如,当你对非数字值做加操作时,Lua 会检查该值的元表中的 "__add" 域下的函数。如果能找到,Lua 则调用这个函数来完成加这个操作。

在元表中事件的键值是一个双下划线()加事件名的字符串;键关联的那些值被称为 元方法。在上一个例子中,add 就是键值,对应的元方法是执行加操作的函数。

你可以用 getmetatable 函数来获取任何值的元表。Lua 使用直接访问的方式从元表中查询元方法(参见rawget)。所以,从对象 o 中获取事件 ev 的元方法等价于下面的代码:

  1. rawget(getmetatable(o) or {}, "__ev")

你可以使用 setmetatable来替换一张表的元表。在 Lua 中,你不可以改变表以外其它类型的值的元表(除非你使用调试库(参见§6.10));若想改变这些非表类型的值的元表,请使用 C API。

表和完全用户数据有独立的元表(当然,多个表和用户数据可以共享同一个元表)。其它类型的值按类型共享元表;也就是说所有的数字都共享同一个元表,所有的字符串共享另一个元表等等。默认情况下,值是没有元表的,但字符串库在初始化的时候为字符串类型设置了元表(参见 §6.4)。

元表决定了一个对象在数学运算、位运算、比较、连接、取长度、调用、索引时的行为。元表还可以定义一个函数,当表对象或用户数据对象在垃圾回收(参见§2.5)时调用它。

对于一元操作符(取负、求长度、位反),元方法调用的时候,第二个参数是个哑元,其值等于第一个参数。这样处理仅仅是为了简化 Lua 的内部实现(这样处理可以让所有的操作都和二元操作一致),这个行为有可能在将来的版本中移除。(使用这个额外参数的行为都是不确定的。)

接下来是元表可以控制的事件的详细列表。每个操作都用对应的事件名来区分。每个事件的键名用加有 '' 前缀的字符串来表示;例如 "add" 操作的键名为字符串 "add"。

  • __add: + 操作。如果任何不是数字的值(包括不能转换为数字的字符串)做加法,Lua 就会尝试调用元方法。首先、Lua 检查第一个操作数(即使它是合法的),如果这个操作数没有为 "__add" 事件定义元方法,Lua 就会接着检查第二个操作数。一旦 Lua 找到了元方法,它将把两个操作数作为参数传入元方法,元方法的结果(调整为单个值)作为这个操作的结果。如果找不到元方法,将抛出一个错误。
  • __sub: - 操作。行为和 "add" 操作类似。
  • __mul: * 操作。行为和 "add" 操作类似。
  • __div: / 操作。行为和 "add" 操作类似。
  • __mod: % 操作。行为和 "add" 操作类似。
  • __pow: ^ (次方)操作。行为和 "add" 操作类似。
  • __unm: - (取负)操作。行为和 "add" 操作类似。
  • __idiv: // (向下取整除法)操作。行为和 "add" 操作类似。
  • __band: & (按位与)操作。行为和 "add" 操作类似,不同的是 Lua 会在任何一个操作数无法转换为整数时(参见 §3.4.3)尝试取元方法。
  • __bor: | (按位或)操作。行为和 "band" 操作类似。
  • __bxor: ~ (按位异或)操作。行为和 "band" 操作类似。
  • __bnot: ~ (按位非)操作。行为和 "band" 操作类似。
  • __shl: << (左移)操作。行为和 "band" 操作类似。
  • __shr: >> (右移)操作。行为和 "band" 操作类似。
  • __concat: .. (连接)操作。行为和 "add" 操作类似,不同的是 Lua 在任何操作数即不是一个字符串也不是数字(数字总能转换为对应的字符串)的情况下尝试元方法。
  • __len: # (取长度)操作。如果对象不是字符串,Lua 会尝试它的元方法。如果有元方法,则调用它并将对象以参数形式传入,而返回值(被调整为单个)则作为结果。如果对象是一张表且没有元方法,Lua 使用表的取长度操作(参见 §3.4.7)。其它情况,均抛出错误。
  • __eq: == (等于)操作。和 "add" 操作行为类似,不同的是 Lua 仅在两个值都是表或都是完全用户数据且它们不是同一个对象时才尝试元方法。调用的结果总会被转换为布尔量。
  • __lt: < (小于)操作。和 "add" 操作行为类似,不同的是 Lua 仅在两个值不全为整数也不全为字符串时才尝试元方法。调用的结果总会被转换为布尔量。
  • __le: <= (小于等于)操作。和其它操作不同,小于等于操作可能用到两个不同的事件。首先,像 "lt" 操作的行为那样,Lua 在两个操作数中查找 "le" 元方法。如果一个元方法都找不到,就会再次查找 "lt" 事件,它会假设 a <= b 等价于 not (b < a)。而其它比较操作符类似,其结果会被转换为布尔量。
  • __index: 索引 table[key]。当 table 不是表或是表 table 中不存在key 这个键时,这个事件被触发。此时,会读出 table 相应的元方法。 尽管名字取成这样,这个事件的元方法其实可以是一个函数也可以是一张表。如果它是一个函数,则以 tablekey 作为参数调用它。如果它是一张表,最终的结果就是以 key 取索引这张表的结果。(这个索引过程是走常规的流程,而不是直接索引,所以这次索引有可能引发另一次元方法。)

  • __newindex: 索引赋值 table[key] = value 。和索引事件类似,它发生在table 不是表或是表 table 中不存在key 这个键的时候。此时,会读出 table 相应的元方法。 同索引过程那样,这个事件的元方法即可以是函数,也可以是一张表。如果是一个函数,则以 tablekey、以及 value 为参数传入。如果是一张表,Lua 对这张表做索引赋值操作。(这个索引过程是走常规的流程,而不是直接索引赋值,所以这次索引赋值有可能引发另一次元方法。)

一旦有了 "newindex" 元方法,Lua 就不再做最初的赋值操作。(如果有必要,在元方法内部可以调用 rawset来做赋值。)

  • __call: 函数调用操作 func(args)。当 Lua 尝试调用一个非函数的值的时候会触发这个事件(即 func 不是一个函数)。查找 func 的元方法,如果找得到,就调用这个元方法,func 作为第一个参数传入,原来调用的参数(args)后依次排在后面。