Lua 元表和元方法

table 作为 Lua 中唯一的数据结构,我们可以利用 table 实现面向对象编程中的类、继承、多重继承等等。在这就介绍一下和 table 密切相关的 Lua 元表和元方法。

Lua 中的每个值都有一个元表。table 和 userdata 可以有各自独立的元表,而其他类型的值则共享其类型所属的单一元表。任何 table 都可以作为任何值的元表,而一组相关的 table 也可以共享一个通用的元表。一个 table 甚至可以作为它自己的元表。

通过 getmetatable 方法可以获取一个值的元表,而 setmetatable 方法则可以设置一个值的元表。

t = {}
print(getmetatable(t)) --> nil
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)

t2 = {}
setmetatable(t2, t2)
assert(getmetatable(t2) == t2)

在 Lua 代码中,只能设置 table 的元表。若要设置其他类型的值的元表,则必须通过 C 代码来完成。从下面的代码也可以看出 Lua 中的所有字符串值是共用一个元表的。

print(getmetatable("hi")) --> table: 0x7fd0b14074b0
print(getmetatable("hello")) --> table: 0x7fd0b14074b0
print(getmetatable(10)) --> nil
print(getmetatable(false)) --> nil
print(getmetatable(function () end)) --> nil

setmetatable("hi", {}) --> error:bad argument #1 to 'setmetatable' (table expected, got string)

元表和元方法

关于 Lua 的元表和元方法,在云风翻译的 Lua 5.3 参考手册 中有以下描述:

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

元表中的键对应着不同的 事件 名;键关联的那些值被称为 元方法。在上面那个例子中引用的事件为 "add" ,完成加操作的那个函数就是元方法。

你可以用 getmetatable 函数来获取任何值的元表。

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

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

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

接下来会给出一张元表可以控制的事件的完整列表。每个操作都用对应的事件名来区分。每个事件的键名用加有 '__' 前缀的字符串来表示;例如 "add" 操作的键名为字符串 "__add"。注意、Lua 从元表中直接获取元方法;访问元表中的元方法永远不会触发另一次元方法。下面的代码模拟了 Lua 从一个对象 obj 中获取一个元方法的过程:rawget(getmetatable(obj) or {}, "__" .. event_name)

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

  • **"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)后依次排在后面。

算术类的元方法:__add(加法)、__mul(乘法)、__sub(减法)、__div(除法)、__unm(相反数)、__mod(取模)、__pow(乘幂)。

关系类的元方法:__eq(等于)、__lt(小于)、__le(小于等于)。其他的关系操作符则没有单独的元方法,Lua 会将 a ~= b 转换为 not a == b ,将 a > b 转换为 a < b ,将 a >= b 转换为 a <= b

库定义的元方法:__tostring__metatable

函数 print 总是调用 tostring 来格式化其输出。当格式化任意值时,tostring 会检查该值是否有一个 __tostring 的元方法。如果有这个元方法,tostring 就用该值作为参数来调用这个元方法,该元方法的返回值就是 tostring 的结果。

函数 setmetatable 和 getmetatable 会触发 __metatable 元方法。当 Lua 中的值拥有该元方法时,getmetatable 就会返回这个字段的值,而 setmetatable 则会引发一个错误。因此我们可以使用 __metatable 元方法来保护任意值的元表,这样值的元表就不会被随意修改了。

t = {}
mt = {}
mt.__metatable = "not your business"
setmetatable(t, mt)

print(getmetatable(t)) --> not your business
setmetatable(t, {}) --> error:cannot change a protected metatable

table 访问的元方法:__index__newindex

算术类和关系类的元方法

算术类和关系类的元方法类似于其他编程语言中的操作符重载,我们可以利用元方法来实现任何不是数字的值(包括不能转换为数字的字符串)的算术和关系运算。

local mt = {}
mt.__add = function (a, b)
    print("call mt.__add")
    return {x = a.x + b.x, y = a.y + b.y}
end

mt.__eq = function (a, b)
    print("call mt.__eq")
    return a.x == b.x and a.y == b.y
end

mt.__tostring = function (point)
    print("call mt.__tostring")
    return string.format("[x = %f, y = %f]", point.x, point.y)
end

Point = {}
function Point.new(x, y)
    local point = {x = x, y = y}
    setmetatable(point, mt)
    return point
end

local p1 = Point.new(10, 10)
local p2 = Point.new(20, 20)
print(p1)
print(tostring(p2))

print("----------")
local p3 = p1 + p2
print(p3)

print("----------")
print(p1 == p2)

print("----------")
print(p1 ~= p2)

执行以上代码输出如下:

call mt.__tostring
[x = 10.000000, y = 10.000000]
call mt.__tostring
[x = 20.000000, y = 20.000000]
----------
call mt.__add
table: 0x7fd462504e10
----------
call mt.__eq
false
----------
call mt.__eq
true

最后

在这只是简单介绍了 Lua 中的元表和元方法的概念,以及算术类和关系类的元方法的使用。但其实 table 访问的元方法 __index__newindex 才是在 Lua 实现面向对象编程的关键,这个会在下一篇文章中介绍。


本文出自 Eddy Wiki ,转载请注明出处:http://eddy.wiki/lua-metatable.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容