0%

Lua Metatables & Metamethods

Tables

table 是 Lua 中最重要的数据结构,数组、关联数组、集合、队列等都可以用table表示,若用 Lua 实现面向对象编程,class 也用 table 表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- test.lua
-- 定义数组
local test = {'a', 'b', 'c', 'd'}

-- 遍历
for k, v in pairs(test) do
print('index: ' .. k .. ' value: ' .. v)
end
-- 按索引读取
print(test[1])

-- 定义关联数组
local test1 = {
abc = 123, -- 注意:此处abc不用加引号
}
-- 关联数组赋值,此处 def 需要加引号
test1['def'] = 234

for k, v in pairs(test1) do
print('index: ' .. k .. ' value: ' .. v)
end
print(test1['def'])

输出:

1
2
3
4
5
6
7
8
9
$ lua test.lua
index: 1 value: a
index: 2 value: b
index: 3 value: c
index: 4 value: d
a
index: abc value: 123
index: def value: 234
234

Metatables & Metamethods

Metatables

Metatables allow us to change the behavior of a table. For instance, using metatables, we can define how Lua computes the expression a+b, where a and b are tables. Whenever Lua tries to add two tables, it checks whether either of them has a metatable and whether that metatable has an __add field. If Lua finds this field, it calls the corresponding value (the so-called metamethod, which should be a function) to compute the sum.

Lua 中的 table 除了像上文定义和使用的方式以外,还可以为每个 table 声明一个 metatable (元表)。元表有点类似于“默认值”的概念,就是说当我们操作一个 table,或取值或其他操作,操作会优先操作表本身,若表本身无法满足这个操作也不会马上报错,会先查看该表是否声明了元表,若有元表则尝试通过元表解决这个操作。
元表可以支持很多不同类型的操作,即 metamethod (元方法)。

Metamethods

元方法均已两个下划线开头,声明在元表内。最常见的元方法:

  • __index
  • __call

下面来看一个示例子:

1
2
3
4
5
local b = {}
local meta = {__index = {aaa = 1}}
setmetatable(b, meta)

print(b['aaa'])

其中:

  • 声明表 b
  • 声明元表 meta,其中声明元方法 __index
  • setmetatabe 将 meta 声明为 b 的元表
  • 其实元表就是一个普通的表,只是里面通过元方法声明了元素,就可以作为其他的元表
__index

__index 是最常用的元方法。通常访问 table 中不存在的索引时,将返回 nil (nil 为 Lua 语言中的“空”,类似于 C/PHP 语言的 NULL。nil 也是不能作为 table 的索引值,像字符串、整型,table 都可作为一个 table 的索引)

1
2
3
4
5
6
7
-- test.lua
-- 定义关联数组
local test1 = {
abc = 123, -- 注意:此处abc不用加引号
}
print(test1['abc'])
print(test1['def'])

输出:

1
2
3
$ lua test.lua
123
nil

既然上面用的词是“通常”,那就一定有个例。当访问表中的元素不存在时,若表声明了元表且元表中有 index 元方法,则解释器会继续去 index 对应的元表里找该元素是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
-- test.lua
local a = {}
print(a['aaa'])

local b = {}
-- 定义元表 meta
local meta = {__index = {aaa = 1}}
-- 将 meta 设置成 b 的元表
setmetatable(b, meta)

print(b['aaa'])
print(rawget(b, 'aaa'))

输出:

1
2
3
4
$ lua test.lua
nil
1
nil

可以看到,a 和 b 同时声明为空 table,但我们为 b 声明了元表后,b[‘aaa’] 若在 b 中找不到 aaa 对应的值,会去其元表 meta 中继续找,找到则返回。
其中:

1
2
3
local b = {}
local meta = {__index = {aaa = 1}}
setmetatable(b, meta)

完全等价于

1
local b = setmetatable({}, {__index = {aaa = 1}})

  • rawget & rawset
    在上面例子里最下面有个 print(rawget(b, ‘aaa’)),当我们想绕过元表,只读取表本身的元素时,可以用 rawget

    • rawget 用于获取元素
    • rawset 用于更新元素,可绕过元方法 __newindex
  • 元表的元方法可以传递
    若 table A 的元表是 table B,table C 又是 table B 的元表,且 B,C 中都有元方法 __index,则若访问 A 中的元素找不到,会去 B 中找,若 B 中找不到则会传递到 C 中继续找

__call

当一个表被当作函数调用时,会调用元表中的 __call。看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
local c = setmetatable({}, {
__call = function (cls, ...)
print("创建一个新table")

return {}
end
})

local d = c()

d['a'] = 123
print(d['a'])

输出:

1
2
3
$ lua test.lua
创建一个新table
1233

将 c 声明为一个 table,但通过函数的方式 c() 直接执行,解释器会查看 c 是否有元表且元表中是否有 __call,若有,则执行,否则会报错。

元方法还有很多,最常见的就是 index 和 call,其他的暂不介绍

Reference & Thanks