12.3 多重继承的问题——还是有冲突
多重继承看起来真的很方便。但是,使用多重继承时该如何解决名字解释的问题呢?当问到类中 x 值是什么时,该如何回答呢?
首先,如果这个类本身知道答案,就直接给出回答(图 12.8)。

图 12.8 名字解释之 1
其次,如果这个类本身不知道答案,就去问它的父类再给出回答(图 12.9)。

图 12.9 名字解释之 2
那么,像下面的代码中展现的多个父类具有相同名字的方法时会怎么样呢?
Python
class ParentA:
x = "A"
class ParentB:
x = "B"
class Child(ParentA, ParentB):
pass
print Child.x # 正确输出是什么?
究竟应该调用哪个方法呢?这里再一次出现了名字解释的问题(图 12.10)。

图 12.10 名字解释之 3
解决方法 1:禁止多重继承
Java 语言中就禁止了类的多重继承。只要不认可类的多重继承这种方式,就不会有上述问题。这样可以把问题解决得很干脆,只是会以失去多重继承的良好便利性为代价。
除此之外,在 Java 语言及其相关库中也可以观察到舍弃作为实现方式再利用的继承的倾向。比如图形工具套装的实现就是这样。1995 年发布的 Abstract Window Toolkit(AWT)中,它是通过继承对各种方法进行重载的。而现在在 Eclipse 等语言中使用的 Standard Widget Toolkit(SWT)却不允许再使用继承。
委托
取而代之发展起来的概念是委托 6。这种方法定义了具有待使用实现方式的类的对象,然后根据需要使用该对象来处理。使用继承后,从类型到命名空间都会被一起继承,从而导致问题的发生,这种方法只是停留在使用对象的层面上。
6委托(delegation)也叫做聚集(aggregation)或者咨询(consultation)。
下面一段代码显示了一个使用委托的例子。
Java
public class TestDelegate {
public static void main(String[] args){
new UseInheritance().useHello(); // -> hello!
new UseDelegate().useHello(); // -> hello!
}
}
class Hello{ ❶
public void hello(){
System.out.println("hello!");
}
}
class UseInheritance extends Hello { ❷
public void useHello(){
hello(); ❸
}
}
class UseDelegate { ❹
Hello h = new Hello(); ❺
public void useHello(){
h.hello(); ❻
}
}
显示“Hello !”的方法 hello 为类 Hello 所持有(❶)。类 UseInheritance 通过继承类 Hello 自身也持有了方法 hello(❷)并加以使用(❸)。与之不同,类 UseDelegate 并没有继承类 Hello(❹),而是通过句❺持有了类 Hello 的对象。当有需要使用时通过句❻将需要的处理委托给该对象操作。
与从多个类中继承实现强耦合的方式相比,使用委托进行耦合的方式显然要更好一些。对于委托的使用,也不需要在源代码中写死,而是可以通过配置文件在合适的时候注入运行时中去。这个想法催生了依赖注入(Dependency Injection)的概念。
接口
刚刚提到 Java 语言中禁止了多重继承,但它也具备实现多重继承的功能。这就需要借助接口(interface)7。
7Java 语言中类的继承用 extends,接口的继承用 implements 来区别表示。另外接口的继承也称为实现。
接口是没有实现方式的类。它的功能仅仅在于说明继承了该接口的类必须持有某某名字的方法。多重继承中发生的问题是多种实现方式相冲突时选取哪个的问题。而在接口的多重继承中,尽管有多个持有某某方法的信息存在,但这仅仅表明持有某某方法,不会造成任何困扰。下面一段代码中就继承了持有相同名字的方法的两个类,编译时不会发生错误 8。
8Java 语言中可以对名字相同但参数类型不同的方法进行重载。因此,准确来讲, 这里说的名字应该是签名。
Java
public class TestMultiImpl implements Foo, Bar {
public void hello(){
System.out.println("hello!");
}
}
interface Foo {
public void hello();
}
interface Bar {
public void hello();
}
这段代码中,类 TestMultiImpl 继承了 Foo 和 Bar 两个接口。如果这个类中不实现 public void hello (),编译时将出现“没有实现应该实现的方法”这样的错误 9。也就是说,继承了接口 Foo 后,这个类就作为一种类型表现出必须持有 public void hello () 的特点,可以让编译器对它进行类型检查。
9具体来讲,提示的错误消息应该是“TestMultiImpl 不是 abstract 的,Foo 中 的 abstract 方法 hello( ) 没有被重载”。
Java 语言为了仅实现功能上的多重继承引入了接口。PHP 语言和 Java 语言一样不认可多重继承,并从 2004 年发布的 PHP5 开始引入了接口的概念。
解决方法 2:按顺序进行搜索
曾经也有些语言试图通过明确定义搜索顺序来解决冲突问题。但是该如何定义呢?如果单纯定义说当前类回答不了时就去检查首先书写的(左边的)类,按这样的顺序(深度优先搜索法)可行吗?(图 12.11)

图 12.11 从左边开始搜索的顺序
深度优先搜索法的问题
遗憾的是这种方法可能造成不自然的结果。请看下面的代码。
Python
class Parent:
x = "A"
class Child(Parent):
x = "B"
print Child.x #-> 重载之后变成B
class Base:
x = "A"
class Derived1(Base):
pass
class Derived2(Base):
x = "B"
class Multi(Derived1, Derived2):
pass
print Multi.x #-> 该输出何值?
从类 Base 继承了两个类 Derived1 和 Derived2。又有一个类 Multi 从这两个类继承出来。这样产生的继承关系叫菱形继承。
方法是可以被重载(override)的。图 12.12 的左边的 Parent 中定义的 x 在 Child 中就被重载了。因此 Child 中的 x 值变成了重载后的值。那么图 12.12 右边的菱形继承中 Derived2 方法重载之后,Multi.x 的值应该是多少呢?

图 12.12 重载和菱形继承
Multi 首先检查左边的父类 Derived1,Derived1 再检查它的父类 Base,结果是 A。Python 2.1(2001 年)就是遵循这种方式的(深度优先搜索法)。然而使用这种方法的话,初始的 x 和重载后的 x 混合后的结果仍然是初始的 x。这样一来,好不容易重新定义的 x 丢失了。
为了回避这一问题,从 Python 2.3(2003 年)开始使用 C3 线性化 10。
102001 年发布的 Python 2.2 中引入了一种新风格的类(new-style class),同时决定方法搜索顺序的算法也得到了改良。但是,文档上的理论和实际实现之间有种种差异,几经波折之后,Python 2.3 还是确定采用 C3 线性化。
C3 线性化确定顺序
C3 线性化是于 1996 年提出来的一种算法 11,它对类进行编号以满足以下两个约束条件:
11在 OOPSLA'96 的活动上,Kim Barrett 等人发表了题为“A Monotonic Superclass Linearization for Dylan”的演讲。
父类不比子类先被检查
如果是从多个类中继承下来则优先检查先书写的类
之所以出现重载后的值变回初始值,就是因为父类先于子类被检 查。因此,在此有针对性地加上了第一个结束条件(图 12.13)。

图 12.13 左:深度优先的搜索顺序 右:C3 线性化的顺序
下面的代码反映了使用深度优先搜索法的 Python 2.1 和采用了 C3 线性化顺序的 Python 2.3 以后的版本之间程序行为的差别。
Python 2.1
# 尽管在Derived2中对x进行了重新定义
# 到Multi后又回到了原来的情况
class Base:
x = 'A'
class Derived1(Base): pass
class Derived2(Base):
x = 'B' # 重新定义
class Multi(Derived1, Derived2): pass # Derived1在左边
print Multi.x #-> 'A'
# ↑没有使用Derived2中的定义
Python 2.3以降
# new-style class(继承自object,使用C3线性化)
# Derived2中定义的'B'被Multi继承下来了
class Base(object):
x = 'A'
class Derived1(Base): pass
class Derived2(Base):
x = 'B' # 重新定义
class Multi(Derived1, Derived2): pass
print Multi.x #->'B'
# ↑使用了Derived2中的定义
Perl 语言曾经允许在库的层面交替使用深度优先搜索法、广度优先搜索法 12 和 C3 线性化。从 Perl 6 开始已采用 C3 线性化的方法作为默认的程序行为。
12简单来说,这是一种先将所有直接父类穷尽再去检查父类的父类的搜索方法。
解决方法 3:混入式处理
原本,问题是指从一个类到它的祖先类 13 有多种追溯方法。既然如此,定义仅包含所需功能的类并把它与需要添加这些功能的更大的类糅合在一起不就行了吗?我们把这种设计方针、混入式处理方式和用来混入的小的类统称为混入处理(Mix-in)。据说 14Mix-in 一词起源于 C++ 语言设计者斯特劳斯特卢普经常光顾的 MIT(麻省理工学院)附近的一家甜品屋,指的是把坚果、葡萄干和冰淇淋混合在一起 15。
13祖先类是包括了父类以及父类的父类等的统称。
14请参考斯特劳斯特卢普《C++ 语言的设计与演化》,http://www.stroustrup.com/dne.html。
15笔者也曾去过那家甜品屋吃过冰淇淋,能按自己独特的需求配制出心仪的口味的冰淇淋是件挺有趣的事情。
菱形继承在使用了 Mix-in 之后就可以变形为非菱形继承。图 12.14 的左侧展示的是类 AB 从类 A 继承并添加了方法 B,类 AC 从类 A 继承并添加了方法 C,进而从类 AB 和类 AC 做多重继承得到了类 ABC。右侧展示的是通过从类 A 和做混入处理用的类 B、类 C 按实际需要继承得到类 AB、类 AC、类 ABC。这种方法不仅消除了菱形继承,而且继承树的深度也减少了一层。

图 12.14 左:菱形继承 右:使用 Mix-in 消除菱形继承
Python 语言中
Mix-in 这种编程风格本身并不依赖于语言处理器,即使不被支持也可以使用。在 12.2 节 SocketServer.py 的多重继承这个例子中,ForkingMixIn 和 ThreadingMixIn 就是为实现混入式多重继承的小型的类。通常这些小型的类最小限度地定义了一些方法,起到了作为代码再利用单位的作用。然而它们却不用于单独创建实例。为了表明这一点,Python 语言会在该类的名字中加上 MixIn 来标识。
Ruby 语言中
Ruby 语言采用的规则是:类是单一继承的而模块则可以任意数量地做混入式处理。模块无法创建实例,但可以像类一样拥有成员变量和方法。也就是说,模块实质上是从类中去除了实例创建功能。即使类的多重继承被禁止了,通过使用模块的 Mix-In 方式照样可以实现对实现方式的再利用 16。
16Mix-in 并不能解决所有的名字冲突的问题。图 12.14 右侧的类 A 和 mixin B 或者 mixin B 和 mixin C 有相同名字的方法时,仍然会导致冲突的产生。
下面的代码中,模块 Hello 中定义了方法 hello,模块 Bye 中定义了方法 bye,类 Greeting 是这两个类混入式处理的结果。最后两行代码的运行结果证实了 Greeting 的对象同时具有方法 hello 和方法 bye。
Ruby
module Hello
def hello
puts "hello!"
end
end
module Bye
def bye
puts "bye!"
end
end
class Greeting
include Hello
include Bye
end
Greeting.new.hello #-> hello!
Greeting.new.bye #-> bye!
解决方法 4:Trait
多重继承在一开始是如何导致问题发生的?发表于 2002 年的一篇关于 Trait 的论文 17 很好地整理了其中的问题点。
17Nathanael Schaerli, Stephane Ducasse, Oscar Nierstrasz, Andrew Black,“Traits: Composable Units of Behaviour”, 2002.
类具有两种截然相反的作用。一种是用于创建实例的作用,它要求类是全面的、包含所有必需的内容的、大的类。另一种是作为再利用单元的作用,它要求类是按功能分的、没有多余内容的、小的类。
当类用于创建实例时,作为再利用单元来说就显得太大了。既然如此,如果把再利用单元的作用特别化,设定一些更小的结构(特性=方法的组合)是不是可以呢?这就是 Trait 的初衷。这和类另外去定义再利用单元的方法不同,和 Ruby 语言中的模块很相似。那么它们之间有什么差别呢?
名字冲突时的程序行为
我们首先来看 Ruby 语言的问题点。
Ruby 1.9.3
module Foo
def hello
puts "foo!"
end
end
module Bar
def hello
puts "bar!"
end
end
class Foobar
include Foo
include Bar
end
class Barfoo
include Bar
include Foo
end
Foobar.new.hello #-> bar!
Barfoo.new.hello #-> foo!
在这段代码中,类 Foobar 中 include 了模块 Foo 和模块 Bar。但是这两个模块都拥有方法 hello,于是产生了冲突。Ruby1.9.3 规定发生类似的冲突时,默认选取最后被 include 的类 Bar 中的方法 hello。这样一来,程序员就会忽略冲突的存在。一旦 include 的顺序变成类 Barfoo 中那样,程序行为又不一样了。
Trait 的设计使得即使顺序改变了程序的行为也不会变。发生名字冲突时,程序会明确地发布错误信息。作为 Smalltalk 语言处理器之一的 Squeak 就提供了为方法取别名和指定不参与冲突的方法等冲突的解决方法。
提供的方法和所需的方法
实际上前面章节提到的 Python 语言中 Mix-In 的例子里也存在问题。在 Python 语言的标准库中,将 ForkingMixIn 混入类 UDPServer 中创建了一个新的类。ForkingMixIn 属于不能创建实例的类。那 UDPServer 是能够创建实例的类么?比如,如果没有 ForkingMixIn 提供的方法 process_request,程序还能执行么?如果是那种非混入式地提供方法 process_request 不可的类,那么“可以单独用来创建实例”这一说法就多少显得有些误导了。
Python 的问题在于缺少一种手段来表明类在什么状态下是可以创建实例的。Scala 语言的 Trait 技术中提供了方法来声明何种方法为必要的。如图 12.15,Trait 提供的方法用圆圈、Trait 需要的方法用箭头表示。UDPServer Trait 需要方法 process_request,同时 Forking Trait 提供了方法 process_request。通过两者的组合得到了可以创建实例的类 ForkingUDPServer。

图 12.15 提供的方法和需要的方法
其他各功能
另外,可以利用已有的 Trait,通过改写某些方法定义新的 Trait 实现继承。还可以通过组合多个 Trait 实现新的 Trait。这就是 Trait 的概要说明。它一方面把问题妥当地分而治之,一方面又因为功能繁多令人困惑。读者们想必都还记得 goto 语句就是因为其功能过于强大而退出历史的舞台的吧。所以说力量过于强大未必是件好事。
Trait 逐渐被广泛采纳
Trait 最早是在 Squeak 语言中引入的。同样使用了 Trait 的还有 Scala 语言。但是或许是因为这两种语言对类型的态度的差别,其中 Trait 的实现方式各有繁简。Perl 6 也引入了和 Trait 相似的功能,称为 Roll。PHP 语言也从 5.4 版本开始引入这种功能。Ruby 语言则在 2.0 版本中引入了 mix method,可以像 Trait 一样操作模块 18。
18RubyConf 2010 Keynote(2), http://www.rubyist.net/~matz/20101113.html.
