为什么C#中同时存在抽象类和接口?

15
为什么C#中既有抽象类又有接口,如果我们可以通过将类中所有成员都设为抽象来实现接口功能?
这是因为:
1. 接口存在是为了实现多重继承;
2. 有接口是因为对象的CAN-DO特性应该放在接口而不是基本抽象类中。
请澄清。
11个回答

32

抽象类可以指定某些实现,但通常不包括全部实现。(话虽如此,提供没有抽象成员但有大量虚函数和“no-op”实现的抽象类是完全可能的)。接口提供没有实现,只是一个契约。

你当然可以争辩说,如果允许多重继承类,那么接口将几乎没有意义。

就个人而言,我并不关注继承中“is-a”与“can-do”的区别。这从来没有给我很好的直觉提示,帮助我做出决策,最好的方式总是尝试各种想法,看哪个感觉最灵活。(话说回来,我非常支持组合优先于继承...)

编辑:正如反驳lbushkin评论中的第三点最方便的方法...你可以通过密封来覆盖抽象方法以及非虚函数(不能进一步覆盖它):

public abstract class AbstractBase
{
    public abstract void Foo();
}

public class Derived : AbstractBase
{
    public sealed override void Foo() {}
}

继承自Derived的类不能再进一步覆盖Foo

我并不是在任何方面都建议我们要多重继承实现 - 但是如果我们确实这样做了(以及它带来的复杂性),那么一个仅包含抽象方法的抽象类几乎可以实现与接口相同的功能。(虽然还有显式接口实现,但这是我目前能想到的全部。)


1
“偏爱组合而非继承”。当然,在某些情况下,相反的方式更可取,比如任何GUI库,那里继承是一种福音。在我看来,如果有足够的层次结构,基于(正确的)继承的设计比基于组合的设计更有用。但是...无论如何,继承都比组合更难(这是OOP的缺陷吗,因为我们今天使用它?) - Pop Catalin
2
如果允许类的多重继承,则接口将在很大程度上变得无意义。我不同意这种说法。首先,接口提供了一种解耦的级别,即使是充满虚拟无操作方法的纯抽象基类也无法提供。它们有助于将类型的契约义务与必然的实现中心关注点分开。其次,由于接口没有数据成员,它们避免了数据的菱形继承问题。第三,C#允许实现接口的方法为非虚拟的(这可能很有用)-抽象类则不行。 - LBushkin
抱歉Jon,我必须在多重继承方面与你不同意。作为一个在C++中度过了很多时间的人,相信我,它是可怕的。现在大多数C++开发人员几乎以与c#/java相同的方式使用多重继承,即一个主要基类加上其他所有内容作为“混合类”。 - zebrabox
1
@LBushkin:第一点:如果您有一个仅具有纯虚方法的抽象基类,那么它如何比接口更耦合?第二点:是的,我并不是说多重继承没有缺点,也不是在呼吁它...但是您可以编写一个仅具有纯虚方法和字段的抽象类。第三点:您可以封闭基类的虚拟方法:我将编辑我的帖子以显示该内容。 - Jon Skeet

17

这不是一个琐碎的问题,而是一个非常好的问题,我在面试任何候选人时都会问到。

简而言之,抽象基类定义了一个类型层次结构,而接口定义了一个契约。

你可以将其视为“是一个”与“实现一个”的区别。例如,Account 可以是一个抽象基类,因为你可以拥有 CheckingAccountSavingsAccount 等所有派生自抽象基类 Account 的类。抽象基类还可以包含像任何正常类一样的非抽象方法、属性和字段。但是接口只包含必须被实现的抽象方法和属性。

C# 只允许从一个基类派生 - 单继承就像 Java 一样。但是你可以实现任意数量的接口 - 这是因为接口只是一个契约,你的类承诺实现。

所以,如果我有一个类 SourceFile,那么我的类可以选择实现 ISourceControl,它说:“我忠实地承诺实现ISourceControl所需的方法和属性。”

这是一个大题目,可能值得写一篇更好的文章,但我时间不多,希望这可以帮到你!


8
它们都存在,因为它们是非常不同的事物。抽象类允许实现,而接口则不允许。接口非常方便,因为它允许我对正在构建的类型进行描述(它是可序列化的,它是可食用的等等),但是它不允许我为定义的成员定义任何实现。抽象类比接口更强大,因为它允许我创建一个继承接口通过抽象和虚拟成员,但如果我选择,也可以提供某种默认或基础实现。然而,正如Spiderman所知道的那样,伴随着这种巨大的力量而来的是巨大的责任,因为抽象类在架构上更加脆弱。值得注意的一点是,CLR团队的Vance Morrrison推测在将来的CLR版本中向接口添加默认方法实现。这将极大地模糊接口和抽象类之间的区别。详情请参见this video

2

两种机制都存在的一个重要原因是因为 C# 中只允许单一继承,不像 C++ 一样可以多重继承。类继承只允许你从一个地方继承实现,而其他的必须通过实现接口来完成。

例如,假设我创建了一个类,比如 Car,并将其子类化为三个子类:RearWheelDrive、FrontWheelDrive 和 AllWheelDrive。现在我决定需要沿不同的“轴”划分我的类,比如那些带有按钮启动器和那些没有的类。我希望所有带有按钮启动器的汽车都有一个“PushStartButton()”方法,而非按钮启动器的汽车都有一个“TurnKey()”方法,并且我想能够处理与它们的启动相关的 Car 对象,而不考虑它们属于哪个子类。我可以定义接口,让我的类可以实现,例如 IPushButtonStart 和 IKeyedIgnition,这样我就有了一种通用的方式来处理这些对象的区别,而这种区别与每个对象所继承的单一基类无关。


1

你已经给出了一个很好的答案。我认为你的第二个答案才是真正的原因。如果我想让一个对象具有可比性,我不应该从可比较的基类派生。如果你考虑所有的接口,想象一下你需要处理像 IComparable 这样的基本接口的所有排列组合。

接口让我们定义一个对象提供的公开行为的契约。抽象类允许您定义行为和实现,这是非常不同的事情。


1

接口存在的目的是为了提供一个没有任何实现的类,以便.NET可以在托管环境中支持安全和功能性的多重继承。


0

这个想法很简单 - 如果你的类(YourClass)已经继承了一个父类(SomeParentClass),同时你想让你的类(YourClass)拥有一些在某个抽象类(SomeAbstractClass)中定义的新行为,你不能简单地从那个抽象类(SomeAbstractClass)继承,因为C#不允许多重继承。 然而,如果你的新行为是在一个接口(IYourInterface)中定义的,你可以轻松地从这个接口(IYourInterface)以及父类(SomeParentClass)中继承。

考虑一个类Fruit,它被两个子类(AppleBanana)继承,如下所示:

class Fruit
{
    public virtual string GetColor()
    {
        return string.Empty;
    }
}

class Apple : Fruit
{
    public override string GetColor()
    {
        return "Red";
    }
}

class Banana : Fruit
{
    public override string GetColor()
    {
        return "Yellow";
    }
}

我们在C#中有一个现有的接口ICloneable。该接口有一个如下所示的单一方法,实现该接口的类保证可以进行克隆:
public interface ICloneable
{
    object Clone();
}

现在,如果我想让我的Apple类(而不是Banana类)可克隆,我可以简单地实现ICloneable,如下所示:

 class Apple : Fruit , ICloneable
{
    public object Clone()
    {
        // add your code here
    }

    public override string GetColor()
    {
        return "Red";
    }
}

现在考虑你对于纯抽象类的论点,如果 C# 有一个纯抽象类,比如说 Clonable,而不是像这样使用接口 IClonable

abstract class Clonable
{
    public abstract object Clone();
}

你能否通过继承抽象类 Clonable 而不是 IClonable,使你的 Apple 类可克隆?像这样:

// Error: Class 'Apple' cannot have multiple base classes: 'Fruit' & 'Clonable'
class Apple : Fruit, Clonable
{
    public object Clone()
    {
        // add your code here
    }

    public override string GetColor()
    {
        return "Red";
    }
}

不行,因为一个类不能从多个类派生。

0

接口定义了一个实现类必须满足的契约;它是一种声明“这个做那个”的方式。抽象类是一个类的部分实现,根据定义是不完整的,需要派生才能完成。它们是非常不同的东西。


抽象类的实现可能已经完全完成了 - 没有任何抽象成员的要求。从抽象类派生而不覆盖任何内容很少有用,但是完全合法的。 - Jon Skeet
1
啊,说得好,不过“抽象类”的定义是不能被实例化的,所以必须有一个派生类来实例化它;我仍然认为这是“不完整的”,因为你不能单独使用它。不过,你的观点非常相关和明智。 - Paul Sonier

0
一个抽象类可以有实现,而接口只允许你创建一个实现者必须遵循的契约。使用抽象类,你可以为它们的子类提供共同的行为,而这是接口所不能做到的。

0

它们具有两个明显不同的目的。

抽象类提供了一种从定义的合同中继承对象的方式,并允许在基类中指定行为。从理论上讲,这提供了一个IS-A关系,即具体类是基类的特定类型。

接口允许类定义它们将履行的一个(或多个)合同。它们允许ACTS-AS或“可以用作”类型的关系,而不是直接继承。这就是为什么通常接口会使用形容词作为名称(IDisposable),而不是名词。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接