Lua元表

3.2k 词

javascript语言本身不支持面向对象,ES2015中增加了class关键字,却不过是prototype语法糖而已,本质上prototype形式的面向对象只能算是一种“模拟”,这其中很重要的原因之一是js从来没有一套完美的深拷贝方案,子类只能借助原型链获取父类方法的引用,这不能算是严格意义的继承,当然也就算不上面向对象。

和js一样,lua的面向对象需要通过table来模拟,有些行为很像js中的原型,比如下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Animal = {name = "Animal"}
Animal.__index= Animal
function ()
local re = {}
setmetatable(re, self)
return re
end
function Animal:GetName()
return self.name
end
x = Animal:new()

在lua中,元表是个很有意思的存在。上面Animal是实例出的对象x的元表,元表在某种意义上相当于js中的构造函数,而__index则类似prototype(这里__index我设置为指向自身)。

1
2
3
4
5
print(x.name)
print(x:GetName())
Animal.name = "Animal2"
print(x.name) -- Animal2
print(x:GetName()) -- Animal2

如果在x中不存在name键,对x.name的访问实际上会从x的元表中的__index键寻找,如果仍然找不到,则会在Animal的元表中继续找,若既没有元表也找不到该键,返回nil,这和js原型链如出一辙。

借助元表,我们很容易模拟面向对象中的继承和多态,比如我们来实现一个继承自Animal的Dog类。

1
2
3
4
5
6
7
8
9
Dog = {}
setmetatable(Dog, Animal)
function Dog:new()
local re = {}
setmetatable(re, self)
self.__index = self
return re
end

效果如下。

1
2
3
4
5
6
7
y = Dog:new()
print(y:GetName())
Animal.name = "Hello"
print(y.GetName()) -- Hello
Dog.name = "Dog"
print(y.GetName()) -- Dog

实际上,lua中的元表比js中的原型机制强大的多。

上述的例子是Lua中最常见的实现OO的方法,除了关键的setmetatable函数,__index键也很重要,它不仅可以是另一个table的引用,也可以是一个函数,当实例对象试着从__index寻找时便会调用这个函数,可以想象,这为多重继承的实现提供了可能,而js做不到这一点(参考)。

举个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
-- 定义两个相似的类A和B
A = {foo1 = 123, name = "A"}
A.__index = A
B = {foo2 = 456, name = "B"}
B.__index = B
function A:new()
local re = {}
setmetatable(re, self)
return re
end
function A:GetName()
print(self.name)
end
function B:new()
local re = {}
setmetatable(re, self)
return re
end
function B:GetName()
print(self.name)
end
function B:MethodOnB()
print("method on B")
end
-- 定义类C,继承自A和B
C = {}
function C:new()
local childA = A:new()
local childB = B:new()
local re = {}
setmetatable(re, {
__index = function (table, key)
if childA[key] then
return childA[key]
elseif childB[key] then
return childB[key]
else
return "not found"
end
end
})
return re
end
x = C:new()
print(x.foo1) -- 123
print(x.foo2) -- 456
x:GetName() -- A
x:MethodOnB() -- method on B

C类继承了来自A和B的方法。

注意到上面的例子中A和B都拥有GetName方法,我们可以进一步假设A和B都继承自另外一个对象,而他们各自的GetName方法其实都继承自这个对象,这就产生了经典的钻石问题(也叫菱形继承问题),即:C继承到的GetName方法到底来自A还是B?一些原生支持面向对象和多重继承的语言为了解决钻石问题,往往会采用特定的遍历算法,如Python采用的是从左到右广度优先原则,使用的是名叫“C3”的算法。而在上面这里例子里我只是简单的指定了先从A中寻找,再从B中寻找,所以C继承了A的GetName方法。

在lua中,元表除了用来模拟面向对象,还有一些不可思议的作用:自定义table间运算的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 定义加法行为
meta = {__add = function(A, B)
local re = {}
for _, val in ipairs(A) do
table.insert(re, val)
end
for _, val in ipairs(B) do
table.insert(re, val)
end
return re
end
}
a = setmetatable({1, 2, 3}, meta)
b = setmetatable({4, 5, 6}, meta)
c = a + b -- {1, 2, 3, 4, 5, 6}

除了__add,元表上可自定义的运算行为包括如下。

1
2
3
4
5
6
7
8
9
10
__add 对应的运算符 '+'.
__sub 对应的运算符 '-'.
__mul 对应的运算符 '*'.
__div 对应的运算符 '/'.
__mod 对应的运算符 '%'.
__unm 对应的运算符 '-'.
__concat 对应的运算符 '..'.
__eq 对应的运算符 '=='.
__lt 对应的运算符 '<'.
__le 对应的运算符 '<='.

而除了运算,元表甚至可以让table像函数一样调用,使用__call

1
2
3
4
5
a = setmetatable({}, {__call = function(mytable, params)
print("123"..params)
end})
a(456) -- 123456

所有上述提到的在元表上以__开头的方法统称为元方法

元表有这么多有意思的设计,也难怪lua程序员说js中的原型只能算实现了元表功能的十分之一。

话说回来,在lua中使用面向对象和在js中的感觉差不多,过去基于prototype模拟OO,很多人有不同的实现,如今js在语法层面统一了写法,而在lua中仍然有很多人尝试对上面这些例子的写法进行封装,试图让代码更容易维护和扩展,这样的折腾其实没什么意义,因为面向对象本身就不易维护。以小而精致著称的lua也不太可能提供语言层面支持,毕竟连社区都没几个,也没看到有人表达这样的诉求,函数式语言就写函数式,多好。