6

多重继承的演变

 3 years ago
source link: https://www.raychase.net/2338
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

多重继承的演变 | 四火的唠叨Skip to content

think

本来想告一段落别写编程范型的东西,但是这个话题最近发现很有意思,就拣出来唠一唠。从中除了能看出很多有趣的语言特性,观察不同语言的设计,还可以发现程序语言的发展过程。这里谈到的语言特性,都是从 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 模块。

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

Posted in Programming Paradigm, RecommendedTagged C++, Go, Java, JavaScript, Ruby, Scala, 多重继承, 接口, 继承 15,291 次阅读

2 thoughts on “多重继承的演变”

  1. 7a093e026145fd179958d869e91925b1?s=32&d=mystery&r=glumm says:

    Human 和 Horse 并不能生出 Centaur,为什么不考虑把 Centaur 作为独立的 Animal,而不是继承自 Human 和 Horse。

  2. ?s=32&d=mystery&r=gAnonymous says:

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

Leave a Reply Cancel reply

Your email address will not be published.

Comment

Name

Email

Website

Save my name, email, and website in this browser for the next time I comment.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK