为什么是抽象类实现接口

本文关键字:接口 实现 抽象类 为什么 | 更新日期: 2023-09-27 18:09:15

我看到代码中抽象类实现接口?你为什么要这么做?而不是把抽象成员放到接口中我们可以直接把它们放到接口中,对吧?

为什么要写这样的代码?目的和需要是什么?

为什么是抽象类实现接口

接口只是与其他代码的契约。另一方面,抽象类可以包含更多内容。他们可以:

  • 有数据成员(字段)
  • 有实际的代码实现
  • 从另一个基类派生
  • 拥有受保护成员

在模板方法模式中可以找到一个很好的用例。您可以为命令提供一个接口:

public interface IMyCommand
{
    void Execute();
}

你有一组命令遵循一定的顺序。为了加强这一点,您可以让它们从抽象基类派生:

public abstract class MyTemplateClass : IMyCommand
{
    public void Execute()
    {
         MyProcessFirst();
         MyProcessSecond();
    }
    protected abstract void MyProcessFirst();
    protected abstract void MyProcessSecond();
}

现在您可以拥有遵循从抽象基类派生的模板方法的对象,而其他对象仅实现接口。

不把抽象成员放在接口中我们可以直接把它们放在接口中,对吗?

没有实现,我们不能。

因为我们可以在抽象类中拥有部分实现,所以我们可以获得重用的所有优点。

我们可以反过来问;为什么不只有抽象类而没有接口呢?

通常,特别是对于抽象类和接口的私有或内部组合,这样做确实是一种改进。但是接口仍然有一些抽象类所没有的优点。

一个明显的问题是,如果我们有,或者可能有,不是从抽象类派生的实现。另一种方法是利用接口中的差异,这是类无法做到的。

即使我们现在没有任何实现接口但不是通过抽象类的情况,如果接口是公共的(YAGNI对公共接口的应用非常不同,删除任何东西都是一个破坏性的更改),我们也值得考虑是否可以这样做。

与此相关的是,拥有一个客户端代码和您的代码可以实现的公共接口,以及在您提供的实现中使用的抽象类,这可能是一个有用的组合,因为它包含了您的实现的共性,而这些共性不一定是其他人的实现的共性。也许你会允许其他人也继承它,而不坚持它,或者你可能只有internal构造函数,所以只有你的代码可以使用它。

假设您正在开发一个具有接口IStream的流行库,该接口用于整个库中的各种api。IStream有如下方法:

int Read(byte[] buffer, int offset, int count);
void Write(byte[] buffer, int offset, int count);

但是,与其让人们直接实现该接口,您强烈建议他们继承您的抽象类Stream,它实现该接口如下:

public abstract int Read(byte[] buffer, int offset, int count);
public abstract void Write(byte[] buffer, int offset, int count);

很多人遵循您的建议,但不是每个人都阅读文档,所以有些人直接实现IStream

现在你的库的下一个版本出来了。你真的很兴奋,因为你要为你的流实现异步操作。因此,您将以下方法添加到IStream:

Task<int> ReadAsync(byte[] buffer, int offset, int count);
Task WriteAsync(byte[] buffer, int offset, int count);

现在你去更新你的抽象类使它编译。您可以使新方法变得抽象,但事实证明,还有一种不完全疯狂的替代方法(省略了错误处理):

public virtual Task<int> ReadAsync(byte[] buffer, int offset, int count)
{
    return Task.Run(() => this.Read(buffer, offset, count));
}
public virtual Task WriteAsync(byte[] buffer, int offset, int count)
{
    return Task.Run(() => this.Write(buffer, offset, count);
}

对于许多现实世界的流类型,通常会有一种不同的方式来处理异步读和写,它可能比这高效无数倍(这就是为什么它是虚拟的而不是密封的),但这对大多数消费者来说可能已经足够好了。

现在让我们看看这两组人。从抽象类继承的人自动获得IStream.ReadAsyncIStream.WriteAsync的实现,而不必自己编写任何代码。它们的流也可以在新的异步api中按原样使用,而不需要它们做任何工作,而且有可能不会很糟糕。

另一方面,实现接口的人现在必须处理自己编写IStream方法的实现,即使他们对使用异步api不感兴趣。也许他们会抛出NotSupportedException来使错误消失。现在他们需要确保他们不调用任何可能有机会调用IStream.ReadAsyncIStream.WriteAsync的东西。他们不开心。他们之所以会这样,是因为他们没有听从你的建议,但我还是很同情他们。

这是用抽象类实现接口的一大优势。事实上,有些人可能会认为IStream根本不应该存在,正是因为这个原因,抽象的Stream类应该只出现在IStream应该出现的所有api中。盲目猜测:也许这就是正是为什么有System.IO。流,但没有System.IO.IStream。虽然我个人更喜欢IStream

我看到的一个可能的原因是,如果你用抽象类实现一个接口,你不需要实现抽象类中的所有方法。您可以只声明这些方法(您间接地实现了它,但只是延迟了实现)。