【发布时间】:2010-09-27 14:45:46
【问题描述】:
在多态的 C# 示例中,有一个 Cat 类继承了一个名为 AnimalBase 的类和一个名为 IAnimal 的接口。
有问题的链接是:http://en.wikipedia.org/wiki/Polymorphism_in_object-oriented_programming
我的问题是,为什么同时使用基类和接口?为什么不是其中一个?我的观点是,实现多态只需要一个抽象类。
谢谢
【问题讨论】:
在多态的 C# 示例中,有一个 Cat 类继承了一个名为 AnimalBase 的类和一个名为 IAnimal 的接口。
有问题的链接是:http://en.wikipedia.org/wiki/Polymorphism_in_object-oriented_programming
我的问题是,为什么同时使用基类和接口?为什么不是其中一个?我的观点是,实现多态只需要一个抽象类。
谢谢
【问题讨论】:
“从基类继承允许您继承 BEHAVIOR,而实现接口仅允许您指定 INTERACTION”的说法是绝对正确的。
但更重要的是,接口允许静态类型语言继续支持多态性。一个面向对象的纯粹主义者会坚持认为一种语言应该提供继承、封装、模块化和多态性,以便成为一种功能齐全的面向对象语言。在动态类型或鸭子类型的语言(如 Smalltalk)中,多态是微不足道的。然而,在静态类型语言(如 Java 或 C#)中,多态性远非微不足道(事实上,从表面上看,它似乎与强类型的概念不一致。)
让我演示一下:
在动态类型(或鸭子类型)语言(如 Smalltalk)中,所有变量都是对对象的引用(不多也不少。)所以,在 Smalltalk 中,我可以这样做:
|anAnimal|
anAnimal := Pig new.
anAnimal makeNoise.
anAnimal := Cow new.
anAnimal makeNoise.
那个代码:
makeNoise。相同的 Java 代码看起来像这样(假设 Duck 和 Cow 是 Animal 的子类:
Animal anAnimal = new Pig();
duck.makeNoise();
anAnimal = new Cow();
cow.makeNoise();
这一切都很好,直到我们介绍蔬菜类。蔬菜有一些与动物相同的行为,但不是全部。例如,动物和蔬菜可能都可以生长,但显然蔬菜不会发出噪音,动物也无法收割。
在 Smalltalk 中,我们可以这样写:
|aFarmObject|
aFarmObject := Cow new.
aFarmObject grow.
aFarmObject makeNoise.
aFarmObject := Corn new.
aFarmObject grow.
aFarmObject harvest.
这在 Smalltalk 中工作得非常好,因为它是鸭子类型的(如果它像鸭子一样走路,并且像鸭子一样嘎嘎 - 它就是鸭子。)在这种情况下,当消息发送到对象时,查找在接收者的方法列表上执行,如果找到匹配的方法,则调用它。如果没有,则会抛出某种 NoSuchMethodError 异常 - 但它都是在运行时完成的。
但是在 Java 这种静态类型语言中,我们可以为变量分配什么类型?玉米需要从蔬菜继承,以支持生长,但不能从动物继承,因为它不会发出噪音。 Cow 需要从 Animal 继承来支持 makeNoise,但不能从 Vegetable 继承,因为它不应该实现收获。看起来我们需要多重继承 - 从多个类继承的能力。但事实证明这是一个非常困难的语言特性,因为会出现所有边缘情况(当多个并行超类实现相同的方法时会发生什么?等等)
接口随之而来...
如果我们创建 Animal 和 Vegetable 类,每个都实现 Growable,我们可以声明我们的 Cow 是 Animal 而我们的 Corn 是蔬菜。我们还可以声明动物和蔬菜都是可生长的。这让我们写这个来发展一切:
List<Growable> list = new ArrayList<Growable>();
list.add(new Cow());
list.add(new Corn());
list.add(new Pig());
for(Growable g : list) {
g.grow();
}
它让我们这样做,发出动物的声音:
List<Animal> list = new ArrayList<Animal>();
list.add(new Cow());
list.add(new Pig());
for(Animal a : list) {
a.makeNoise();
}
鸭类型语言的最大优势是您可以获得非常好的多态性:一个类提供行为所要做的就是提供方法(还有其他权衡,但在讨论类型时这是一个重要的权衡。)只要每个人都表现得很好,并且只发送与定义的方法匹配的消息,一切都很好。缺点是直到运行时才会发现下面的错误类型:
|aFarmObject|
aFarmObject := Corn new.
aFarmObject makeNoise. // No compiler error - not checked until runtime.
静态类型语言提供了更好的“契约式编程”,因为它们会在编译时捕获以下两种错误:
Animal farmObject = new Corn(); // Compiler error: Corn cannot be cast to Animal.
farmObject makeNoise();
--
Animal farmObject = new Cow();
farmObject.harvest(); // Compiler error: Animal doesn't have the harvest message.
所以....总结一下:
接口实现允许您指定对象可以做哪些事情(交互),类继承允许您指定事情应该如何做(实现)。
接口为我们提供了“真正的”多态性的许多好处,而不会牺牲编译器类型检查。
【讨论】:
当你想重用 BEHAVIOR 时使用基类
当您想要控制类如何与其他对象交互时,可以使用接口。它以精确的方式定义交互。
根据我的经验,您想要控制类交互方式的次数远远超过您想要重用行为的次数。
【讨论】:
拥有一个抽象类可以让您以一种通用的方式实现部分/大部分成员。拥有一个接口意味着当您想将它用于多态性时,您不会被限制仅使用该抽象基类。
我认为两者兼有没有任何矛盾。
【讨论】:
接口使您能够跨类继承的多态行为。缺点是您不能(直接)继承默认实现。使用类多态性,您只能在类层次结构中获得该多态行为,但您可以继承常见/默认行为。通过继承和提供接口,您提供了一个契约(接口),但可以获得继承的简单实现好处,同时仍然允许其他人在基类限制之外支持“契约”。
【讨论】:
接口强制执行“行为”。任何声明为实现指定接口的类都必须使用接口中声明的签名来实现成员。即,它们必须公开指定的行为......它们不必以相同的方式实现行为,但它们必须具有相同的行为...即鸟和蠕虫都“CanMove”,因此它们都必须实现能够“移动”的行为,指定它们都必须实现接口 ICanMove 这样做... 他们如何做到这一点是实现的功能。
基类用于“实现”的重用...
这就是为什么接口的命名约定建议在 IEnumerable、ICanMove、ICanLog 等中使用“I[动词/副词]”。
您使用基类将通用实现放在一个地方。如果抽象基类在任何成员中都没有实现,那么它的功能与接口相同,接口不能有实现
【讨论】:
基类和接口实际上有很多不相关的用途。基类的主要目的是让您的继承类能够导入一些常用功能。接口的主要目的是让 other 类能够提出问题,“这个对象是否支持接口 X”?
【讨论】:
所以我想我可以继承一个用于常见行为的动物类,比如走路、睡觉等,然后有一个专门的接口(也许是狮子),它包含特定的行为和属性——比如动物很危险,吃人等等。
【讨论】: