在 Lua 5.1 语言中,元表 (metatable) 的表现行为类似于 C++ 语言中的操作符重载,例如我们可以重载 “add” 元方法 (metamethod),来计算两个 Lua 数组的并集;或者重载 “index” 方法,来定义我们自己的 Hash 函数。

预定义操作集合

其实不仅仅是LUA, 在大多数编程语言中,每一种类型的值,都有 一套预定义的操作集合。例如,整形的数值可以进行加操作,减作等,字符串类型的数值可以进行加操作等,不同的类型预定义操作或者说预定义的行为略有不同, 在LUA语言中,table类型的值默认是不能进行加操作的。但是我们能不能让table可以进行加操作呢?答案是可以。

元表

要想使table类型的值可以进行加操作,就要为table类型的值添加一个加操作的行为,我们姑且把它叫做是非预定义的操作。这时就要用到我们要讲的元表,称之为metatable,元表也是一种数据类型,在lua语言中,我们可以为table类型的值设置metatable,来为table类型的值添加一些非预定义的操作。

在lua中为我们提供的两个方法:

  • setmetatable(table, metatable) 来为table变量设置metatable对象。
  • getmetable(table) 此方法用于获取table的metatable对象。

注意,我们使用lua只能设置table类型值的metatable,如果想设置其他类型值的metatable, 则需要通过C代码来实现。

当我们在创建一个table类型的值的时候,默认是没有metatable的,因此通过getmetatable方法返回nil。

元表的调用方式是怎样的?

比如想要实现两个表类型值的加操作,我们可以通过定义一个元表值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
local set1 = {10, 20, 30}
local set2 = {20, 40, 50}
-- 将用于重载_dd的函数。注意第一个参数是self
local union = function(self, another)
local set = {}
local result = {}
-- 利用数组来确保集合的互异性
for i,j in pairs(self) do set[j] = true end
for i,j in pairs(another) do set[j] = true end
-- 加入结果集合
for i,j in pairs(set) do table.insert(result, i) end
return result
end
setmetatable(set1, {__add = union}) -- 重载set1表一的__add元方法
local set3 = set1 + set2
for _,j in pairs(set3)
io.write(j .. " ") --> output: 20 10 40 30 50
end

计算表达式set1 + set2,具体的步骤是按照一些步骤进行的:

  1. 先判断set1和set2两者之一是否元表
  2. 检查该元表是否有一个叫__add的字段
  3. 如果找到了该字段,就调用该字段的值,这个值对应的是一个元方法(metamethod);
  4. 调用__add对应的metamethod字段set1和set2的和。

元方法

正如刚刚我们定义的add方法,具体定义某一种的操作行为的方法,叫元方法。除了add方法可以被重载之外,Lua提供的所有操作符都可以被重载:

元方法 含义
__add + 操作
__sub - 操作 其行为类似于 “add” 操作
__mul * 操作 其行为类似于 “add” 操作
__div / 操作 其行为类似于 “add” 操作
__mod % 操作 其行为类似于 “add” 操作
__pow ^(幂) 操作 其行为类似于 “add” 操作
__unm 一元 - 操作
__concat .. (字符串连接) 操作
__len # 操作
__eq == 操作 函数 getcomphandler 定义了 Lua 怎样选择一个处理器来作比较操作 仅在两个对象类型相同且有对应操作相同的元方法时才起效
__lt < 操作
__le <= 操作

除了操作符之外,如下方法也可可以被重载,下面会一次解释使用方法

元方法 含义
__index 取下标操作用于访问 table[key]
__newindex 赋值给指定下标 table[key] = value
__tostring 转换成字符串
__call 当Lua调用一个值时调用
__mode 用于弱表(week table)
__metatable 用于保护metatable不被访问

__index 元方法

下面我么实现了在表中查找键不存在是转而在元表中查找该键的功能:

1
2
3
4
5
6
7
8
9
mytable = setmetatabe({key1 = "value1"}, -- 原始表
{__index = function(self, key)
if key == "key2" then
return "metatable value"
end
end
})
print(mytable.key1, mytable.key2) --> output: value1 metatale value

关于__index元方法,有很多比较高阶的技巧,例如:__index 的元方法不需要非是一个函数,他也可以是一个表。

1
2
t = setmetatable({[1] = "hello"}, {__index = {[2] = "world"}})
print(t[1], t[2]) -->hello world

第一句代码有点绕,解释一下:先是把 {__index = {}} 作为元表,但 __index 接受一个表,而不是函数,这个表中包含 [2] = “world” 这个键值对。 所以当 t[2] 去在自身的表中找不到时,在 __index 的表中去寻找,然后找到了 [2] = “world” 这个键值对。

利用这个特性,我们可以利用__index来实现lua语言的面向对象

面向对象

在 Lua 中,我们可以使用表和函数实现面向对象。将函数和相关的数据放置于同一个表中就形成了一个对象。

下面是account.lua的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 元表对象,我们在_M中放置我们要定义的类的函数和相关的数据
local _M = {}
-- 元表,将_M赋值给__index, 这样当访问类对象的函数或者数据时找不到,就会调用_M中的
local mt = { __index = _M }
function _M.deposit (self, v)
self.balance = self.balance + v
end
function _M.withdraw (self, v)
if self.balance > v then
self.balance = self.balance - v
else
error("insufficient funds")
end
end
function _M.new (self, balance)
balance = balance or 0
-- {balance = balance} new的对象,并设置元表mt
return setmetatable({balance = balance}, mt)
end
return _M

引用代码示例:

1
2
3
4
5
6
7
8
9
10
local account = require("account")
local a = account:new()
a:deposit(100)
local b = account:new()
b:deposit(50)
print(a.balance) --> output: 100
print(b.balance) --> output: 50

上面这段代码 “setmetatable({balance = balance}, mt)”,其中 mt 代表 { __index = _M } ,这句话值得注意。根据我们上面学到的知识,我们明白,setmetatable 将 _M 作为新建表的原型,所以在自己的表内找不到 ‘deposit’、’withdraw’ 这些方法和变量的时候,便会到 __index 所指定的 _M 类型中去寻找。

__index 元方法还可以实现给表中每一个值赋上默认值;和 __newindex 元方法联合监控对表的读取、修改等比较高阶的功能,待读者自己去开发吧。

其他具体可以参考

Lua中的元表与元方法

LUA元表