默认接口方法。现在,抽象类和接口之间有什么深刻而有意义的区别?

29
我知道抽象类是一种特殊的类,不能实例化。抽象类只能被子类化(继承)。换句话说,它只允许其他类继承它,但它本身不能被实例化。优点是它可以为所有子类强制执行某些层次结构。简单来说,它是一种强制所有子类遵循相同层次结构或标准的契约。
我也知道接口不是类。它是由单词接口定义的实体。接口没有实现,它只有方法签名或换句话说,只有方法的定义而没有方法体。与抽象类的一些相似之处是,它是用于为所有子类定义层次结构或定义特定集合方法及其参数的协议。它们之间的主要区别是,一个类可以实现多个接口,但只能继承一个抽象类。由于C#不支持多重继承,因此使用接口实现多重继承。
当我们创建一个接口时,我们基本上创建了一组没有任何实现的方法,这些方法必须被实现类覆盖。优点是它提供了一种让一个类成为两个类的一部分的方式:一部分来自继承层次结构,另一部分来自接口。
当我们创建抽象类时,我们创建了一个可能具有一个或多个已完成方法但至少有一个或多个方法未完成且声明为抽象的基类。如果抽象类的所有方法都未完成,则它与接口相同。
但是,我注意到在C# 8.0中有默认接口方法。
也许我之所以问这个问题是因为我只有1-2年的编程经验,但抽象类和接口现在的主要区别是什么?
我知道我们不能在接口中定义状态,这将是它们之间唯一的区别吗?

6
说实话?如果在C#中引入模拟多重继承,它们将变得几乎相同。这是许多C#开发人员(包括我自己)完全反对该功能的主要原因之一。 - BJ Myers
2
我最近注意到,C# 6旨在展示编程语言设计的收益递减,而C# 7则旨在展示收益递减的演示。你可以看到C#8正在往何处发展。它正在成为一种四维雪球。微软需要为这些人找到一种新的工作语言。这个已经完成了。它拥有所有功能。 - 15ee8f99-57ff-4f92-890c-b56153
5
我非常喜欢非空引用类型的想法。 - 15ee8f99-57ff-4f92-890c-b56153
1
@EdPlunkett 哈!我在看完你之前的评论时也正想着恰好这个问题。可能是因为我今天早上刚读了一篇关于它们的文章。 - itsme86
我认为默认接口方法会带来混淆,除非作为Trait一致使用,并且我们应该引入命名约定(例如 IFooTrait 或者只是 FooTrait)。 - Olivier Jacot-Descombes
6个回答

13

概念

首先,类和接口之间存在一个概念上的区别。

  • 类应该描述一个“is a”的关系。例如,Ferrari是一辆Car。
  • 接口应该描述一个类型的约定。例如,Car有一个方向盘。

目前,有时即使没有“is a”关系,抽象类也会被用于代码重用,这会污染OO设计。例如,FerrariClass继承自CarWithSteeringWheel

好处

  • 因此,您可以在不引入(概念上错误的)抽象类的情况下重用代码。
  • 您可以从多个接口继承,而抽象类只能进行单一继承。
  • C#中的接口支持协变性和逆变性,而类不支持。
  • 实现接口更容易,因为一些方法有默认实现。这可以为接口的实现者节省很多工作,但用户看不到差别 :)
  • 但对我来说最重要的是(因为我是库维护者),您可以添加新方法到一个接口而不会造成破坏性的改变!在C# 8之前,如果一个接口被公开发布,它应该是固定的。因为改变接口可能会造成很多问题。

记录器接口

这个例子展示了一些好处。

您可以将(过度简化的)记录器接口描述如下:

interface ILogger
{
    void LogWarning(string message);

    void LogError(string message);

    void Log(LogLevel level, string message);
}

那么,使用该界面的用户可以轻松地使用 LogWarningLogError 记录警告和错误。但缺点是实现者必须实现所有方法。

一个更好的带有默认值的接口应该是:

interface ILogger
{
    void LogWarning(string message) => Log(LogLevel.Warning, message);

    void LogError(string message) => Log(LogLevel.Error, message);

    void Log(LogLevel level, string message);
}

现在用户仍然可以使用所有方法,但实现者只需要实现Log。此外,他可以实现LogWarningLogError

另外,在将来,您可能希望添加日志级别“Catastrophic”。 在C#8之前,如果未经修改就添加LogCatastrophic方法到ILogger中会破坏所有当前实现。


11
除了抽象类可以拥有状态,接口不能之外,两者之间并没有太大的区别。在Java中,“默认方法”或称为“虚扩展方法”已经可用一段时间了。默认方法的主要驱动力是“接口演变”,这意味着能够在未来版本中向接口添加方法,而不会破坏该接口现有实现的源或二进制兼容性。
此外,根据这篇文章提到的另外几点:

就Java而言,我认为Java具有默认方法这种较差的结构的原因是因为他们无法接受复制更好的C#扩展方法。现在,C#毫无意义地从Java中复制了默认方法,而没有提供特质(具有状态)的真正实现。 - davidbak

3

抽象类和新的默认接口方法都有其适当的用途。

A. 原因

默认接口方法并非是为了取代抽象类而引入的。

C# 8.0 的新功能中指出:

此语言特性使 API 作者能够在后续版本中向接口添加方法,而不会破坏对该接口的现有实现的源代码或二进制兼容性。现有实现继承默认实现。

此功能还使 C# 能够与针对 Android 或 Swift 的 API 相互操作,后者支持类似的功能。默认接口方法还启用类似于“特征”语言功能的场景。

B. 功能差异

抽象类和接口(即使具有默认方法)仍然存在显著差异。

以下是接口仍然不能拥有或执行的几件事情,而抽象类可以:

  • 有一个构造函数
  • 保留状态
  • 继承非抽象类
  • 拥有私有方法

C. 设计

虽然默认接口方法使接口更加强大,但是抽象 / 基类和接口仍然代表根本不同的关系。

(来自 当设计 C# 类库时,应何时选择继承而非接口?)

  • 继承描述了 is-a 关系。
  • 实现接口描述了 can-do 关系。

1
接口可以有私有方法,我们可以从其他公共成员的定义中调用它们。 - Rajeev

3

另一个使接口独特的因素是协变性/逆变性

老实说,我从未遇到过默认接口实现是解决方案的情况。我对此有些怀疑。


1
我能想到的唯一主要区别是,你仍然可以为抽象类重载默认构造函数,而接口永远不会有这个功能。
abstract class LivingEntity
{
    public int Health
    {
        get;
        protected set;
    }


    protected LivingEntity(int health)
    {
        this.Health = health;
    }
}

class Person : LivingEntity
{
    public Person() : base(100)
    { }
}

class Dog : LivingEntity
{
    public Dog() : base(50)
    { }
}

0

两个主要的区别:

  • 抽象类可以有状态,但接口不能。
  • 一个类型可以从单个抽象类派生,但可以实现多个接口。

在默认修饰符方面还有一些其他较小的差异。


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