由于 Lua
语言中的对象不是基本类型,因此在 Lua
语言中进行面向对象编程时有几种方式。我们通过使用 __index
元方法的做法实现了类的继承功能,这种做法可能是在简易、性能和灵活性方面最均衡的做法。不过尽管如此,还有一些其他的实现对某些特殊的情况下可能加合适。
多重继承的实现在于把一个函数用作 __index
元方法。
提示
当一个表的元表中的 __index
字段为一个函数时,当 Lua
不能在原来的表中找到一个键时就会调用这个函数。
基于此,就可以让 __index
元方法在其他期望的任意数量的父类中查找缺失的键。
多重继承意味着一个类可以具有多个超类。因此,我们不应该使用一个(超)类中的方法来创建子类,而是应该定义一个独立的函数 createClass
来创建子类。函数 createClass
的参数为新类的所有超类,该函数创建一个表来表示新类,然后设置新类元表中的元方法 __index
,由元方法实现多重继承。虽然是多重继承,但每个实例仍属于单个类,并在其中查找所有的方法。因此,类和超类之间的关系不同于类和实例之间的关系。尤其是,一个类不能同时成为其实例和子类的元表。我们将类保存为其实例的元表,并创建了另一个表作为类的元表。如下所示:
-- 在表'plist'的列表中查找'k' local function search (k, plist) for i = 1, #plist do local v = plist[i][k] -- 尝试第'i'个超类 if v then return v end end end function createClass (...) local c = {} -- 新类 local parents = {...} -- 父类列表 -- 在父类列表中查找类缺失的方法 setmetatable(c, { __index = function (t, k) return search(k, parents) end }) -- 将'c'作为其实例的元表 c.__index = c -- 为新类定义一个新的构造函数 function c:new (o) o = o or {} setmetatable(o, c) end return c -- 返回新类 end
让我们用一个简单的示例来演示 createClass
的用法。假设前面提到的类 Account
和另一个只有两个方法 setname
和 getname
的类 Named
:
Named = {} function Named:getname() return self.name end function Named:setname(n) self.name = n end
要创建一个同时继承 Account
和 Named
的新类 NamedAccount
,只需要调用 createClass
即可,如下所示:
NamedAccount = createClass(Account, Named)
可以像平时一样创建和使用实例:
account = NamedAccount:new{name = "Paul"} print(account:getname()) -- Paul
现在,让我们来学习 Lua
语言是如何对表达式 account:getname()
求值的;更确切的说,让我们来学习 account["getname"]
的求值过程。
首先, Lua
语言在 account
中找不到字段 "getname"
;因此,它就查找 account
的元表中的 __index
字段,在我们的示例中该字段为 NamedAccount
。由于在 NamedAccount
中也不存在字段 "getname"
,所以再从 NamedAccount
的元表中查找 __index
字段。由于这个字段是一个函数,因此 Lua
语言就调用了这个函数(即 search
)。该函数先在 Account
中查找 "getname"
;未找到后,继而在 Named
中查找并最终在 Named
中找到了一个非 nil
的值,也就是最终的搜索结果。
当然,由于这种搜索具有一定的复杂性,因此多重继承的性能不如单继承。一种改进性能的简单做法是将被继承的方法复制到子类中,通过这种技术,类的 __index
元方法就会变成:
setmetatable(c,{ __index = function (t, k) local v = search(k, parents) t[k] = v -- 保存下来用于下次访问 return v end })
使用了这种技巧后,在第一次访问过被继承的方法后,再访问被继承的方法就会像访问局部方法一样快了。这种技巧的缺点在于当系统开始运行后修改方法的定义就比较困难了,这是因为这些修改不会沿着继承层次向下传播。