多重继承的演变

多重继承的演变

本来想告一段落别写编程范型的东西,但是这个话题最近发现很有意思,就拣出来唠一唠。从中除了能看出很多有趣的语言特性,观察不同语言的设计,还可以发现程序语言的发展过程。这里谈到的语言特性,都是从C++的多重继承演变而来的,都没法完整地实现和代替多重继承本身,但是有了改进和变通,大部分功能保留了下来,又避免了多重继承本身的问题。

C++的多重继承

这个问题我觉得需要从老祖宗C++谈起,我记得刚开始学C++的时候老师就反复教育我们,多重继承的问题。比如说二义性问题,也就是说,两个父类如果定义了同名的方法,调用它的时候编译器就不知道怎么办了。

但是需要说清楚的是,多重继承确实是有其使用场景的,继承表示的是“is a”的关系,比如人、马,都是切实存在的实体类,而非某一种抽象,有一种动物叫做人马兽,既为人,也为马,那么不使用多重继承就无法表现这种关系。

就上面的问题,针对人(Human)、马(Horse)和半人马兽(Centaur),其中一种解决的办法是引入虚基类动物(Animal),作为Human和Horse的共同基类,Centaur同为Human和Horse的子类,这样只要:

  • Animal虚基类里面定义的纯虚方法被Human、Horse之任一实现,不实现的一侧继续声明其为纯虚函数,
  • 或者无论Human、Horse中是否实现,Centaur中实现即可。

具体来说,哭(cry)这个行为应该是人、马和半人马都应当具备的行为,只是实现不同:

class Animal {
	public:
		Animal(){...}
		virtual void cry() = 0;
};

class Human : virtual public Animal {
	public Human() : Animal() {}
	void cry() {
		// human impl
	}
};
class Horse : virtual public Animal {
	public Horse() : Animal() {}
	void cry() {
		// horse impl
	}
};

class Centaur : public Human, public Horse {
	public Centaur() : Human(), Horse() {}
	void cry() {
		// centaur impl
	}
};

Java中实现多个接口

首先,必须说明的是,在Java倡导使用实现多接口来代替多重继承的功能,实际是不合理的,真正的多重继承场景是难以使用实现多接口来代替的。确实多重继承有其问题,但是因为这个问题,就把多重继承粗暴地从语言特性中抹去,是有些因噎废食了。

public interface Cryable {
	public void cry();
}
public interface Speak {
	public void speak();
}
class Human implements Cryable, Speak {...}
class Horse implements Cryable {...}
class Centaur implements Cryable, Speak {...}

以上是Java中的一个例子,人能哭,人也能说;但是马只能哭,不能说;而半人马呢,和人一样,会哭也会说。代码很容易理解,但是从中非常容易看出,半人马和人、马之间的紧密的“is a”的关联关系就丢失了。就这个问题,Java中还有一些其它的实现方式,比如把Human和Horse都变成接口:

public interface Cryable {
	public void cry();
}
public interface Speak {
	public void speak();
}

public interface Human extends Cryable, Speak {}
public interface Horse extends Cryable {}
public class Centaur implements Human, Horse {...}

这个方法倒是近似保留了半人马和人、马之间的联系,但是为此强行把本该属于实体类的人和马都变成了接口,也不甚合理。

值得一提的是,Java中实现多个接口的做法是介于多重继承和鸭子类型(Duck Typing)中间的方案,即既没有多重继承“is a”的明确定义,又不像常规鸭子类型那样在编译期缺少任何方法接口定义的约束,下面我还会介绍其它几种语言对多重继承的改进和变异。

JavaScript的构造继承和拷贝继承

JavaScript彻底从语言层面丢掉了接口约束,变成了真真正正的鸭子类型,使用构造继承和拷贝继承可以模拟多重继承

var Human = function() {  
    this.name = function(name){...};
};
var Horse = function() {
    this.jump = function(){...};
};
var Centaur = function() {
	Human.call(this);
	Horse.call(this);
};

这就是构造继承,Centaur同时具备了Human和Horse的成员方法。

拷贝继承的示例代码就不写了,Centaur的定义中,分别遍历Human和Horse,把Human和Horse的成员方法和属性一一取下来按到Centaur自己身上。

完成这样的行为以后,Centaur能够命名也能够跳跃,具备了二者的共性,它即可以被当做人,也可以被当做马。那么Centaur就是人、也就是马,这就是鸭子类型(只要会嘎嘎叫,就可以视作鸭子来调用);但是,在使用instanceof判断Centaur的实例是否是Human或者Horse时:

var centaur = new Centaur();
console.log(centaur instanceof Horse);
console.log(centaur instanceof Human);

都会打印false,所以,JavaScript对多重继承的模拟也只是模拟而已,根本不是真正的多重继承。JavaScript本质上是不存在多重继承的,就连继承的实现,也没有一种方法是完美的——详情请阅读《JavaScript实现继承的几种方式》

Go语言的Structural Typing

Structural Type,结构类型,本质上来说,它就是静态语言中的鸭子类型。不用显式声明某实体类实现自某一个接口,只要这个实体类具备了这个接口的方法,那么它就是这个接口的实现。

type Human struct {...}
type Horse struct {...}
type Centaur struct {
	Human
	Horse
}

type Speakable interface {
	speak()
}
type Jumpable interface {
	jump()
}

func (this *Human) speak() {...}
func (this *Horse) jump() {...}

Centaur里面包含了Human和Horse,这使得Centaur同时具备了Human和Horse的成员方法。很显然,这也不算多重继承,但是实现了类似的功能。类之间的层次关系部分丢失:没有丢的是Human和Horse与Centaur之间存在联系,但是变成了关联关系,并非继承关系。

Scala的Trait

Trait,直译叫做特征,Scala不能实现多重继承,但是类似地,也通过一种特定的语义语法引入其它类的功能:

class Human() {
	def speak() = ...
}
trait Horse {
	def jump() = ...
}
class Centaur() extends Person with Horse

如上,Centaur真正的父类其实是Human,这才是实实在在的继承关系,但是一样具备了Horse的功能。Trait的功能还是要略比真正的继承弱一些,这个例子中在实现某特征的时候,就没有办法调用该特征类的构造器(创建特征实例)。

Ruby的Mixin

Mixin,混入,可以让目标对象获得某一个模块的功能,在Groovy里面也有类似的特性。

class Human
	def speak
	end
end

module Horse
	def jump
	end 
end

class Centaur < Human
	include Horse
end

和前面说的Trait非常类似,只有Human是Centare真真正正的父类,Horse甚至连类都不是,这是它局限的地方。

=======================================================================================

【2014-3-23】昨天和一位朋友讨论了这篇文章中提到的多重继承的问题,他的观点是,文中除了Java以外,剩下的几种,JavaScript的构造/拷贝继承、Go的Structural Type,或者是Trait、Mixin,这些都是多重继承,而且大同小异,最多算是不同的实现方式而已;Ruby的作者松本行弘在他的《松本行弘的程序世界》书中也是这样的观点,因为“继承”最重要的事情是具备父类的“特征”和“功能”,这些都做到了。但是文中我没有这样认为的原因是,这些实现形式都丢失了子类和父类之间的联系。

另外一件事是Ruby的Mixin,上面的这个例子举得不是很好,Human是类,而Horse是模块,从这点上来说,这本身就不对等了,合理的办法是让Human和Horse都变成类,而分别创建名为Jump和Speak的模块——让Human引入Speak模块,让Horse引入Jump模块,而Centaur引入Speak和Jump模块。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:

One comment

  1. 匿名 说道:

    另外一件事是Ruby的Mixin,上面的这个例子举得不是很好,Human是类,而Horse是模块,从这点上来说,这本身就不对等了,合理的办法是让Human和Horse都变成类,而分别创建名为Jump和Speak的模块——让Human引入Speak模块,让Horse引入Jump模块,而Centaur引入Speak和Jump模块。 都引入模块的话不是没了is-a的关系跟JAVA一样了吗?

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>