实现接口的抽象基类

25

假设我有一个抽象基类,比如说:

abstract class Item : IDisplayable
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public abstract void Print();

    }

我有一个继承自它的类,如下:

  class Chair: Item
 {
    public int NumberOfLegs {get;set;}

    public void Print()
    {
     Console.WriteLine("Here is a simple interface implementation");
    }
 }

interface IDisplayable
{
void Print();
}

子类并没有明确说明它也实现了该接口,但通过简单的继承它将实现该接口。如果我们在子类中显式添加接口,程序将以相同的方式运行(至少在我的简单示例中是这样)。显式实现接口是一个好主意还是坏主意,还是纯粹是个人喜好的问题?


2
注意,随着添加的内容,Item 至少需要将 Print() 声明为 abstract,如果没有实现它,它将无法编译。 - Jesse C. Slicer
8
投票关闭此问题是错误的。这是一个非常好的问题。 - Eric Lippert
@EricLippert,您的意见是实现每个接口在子类上都是冗余的吗? - wootscootinboogie
3
我认为你只应该编写具有你打算表达的含义的代码。如果你不打算重新实现接口语义,那就不要编写重新实现接口的代码! - Eric Lippert
1
另外,你是否打算将 Chair.Print 标记为 override - Eric Lippert
显示剩余5条评论
3个回答

46

如果我们将接口明确添加到子类中,程序将会运行相同(至少在我简单示例中是这样的)。

程序不一定会运行相同;你的示例不足以说明差异。

明确实现接口是好还是坏,还是纯粹是个人喜好?

除非你打算确保接口重新实现语义,否则这是一个坏主意。

让我简要说明一下。这个程序做什么?

using System;
interface IFoo { void Bar(); void Baz(); }
class Alpha : IFoo
{ 
    void IFoo.Bar() 
    {
        Console.WriteLine("Alpha.Bar");
    }
    void IFoo.Baz()
    {
        Console.WriteLine("Alpha.Baz");
    }
}
class Bravo : Alpha
{
    public void Baz()
    {
        Console.WriteLine("Bravo.Baz");
    }
}
class CharlieOne : Bravo
{
    public void Bar() 
    {
        Console.WriteLine("CharlieOne.Bar");
    }
}
class CharlieTwo : Bravo, IFoo
{
    public void Bar() 
    {
        Console.WriteLine("CharlieTwo.Bar");
    }
} 
class Program
{
    static void Main()
    {
        IFoo foo = new Alpha();
        foo.Bar();
        foo.Baz();
        foo = new Bravo();
        foo.Bar();
        foo.Baz();
        foo = new CharlieOne();
        foo.Bar();
        foo.Baz();
        foo = new CharlieTwo();
        foo.Bar();
        foo.Baz();
     }
}

在继续阅读之前,请认真考虑一下这个程序的输出,试着预测一下

现在运行它。 你得到了期望的输出吗? 你的直觉哪里出错了?

现在你看出来CharlieOneCharlieTwo之间的区别了吗?CharlieTwo中重新实现IFoo可能会导致接口绑定选择Bravo.Baz,即使Bravo没有重新实现IFoo

另一方面:如果你期望Bravo.Baz被分配给接口槽,只是因为它存在,那么你就会看到未能重新实现接口会导致代码不正确。为了让Bravo.Baz替换Alpha.IFoo.BazBravo必须重新实现IFoo

这里的要点是:当你重新实现一个接口时,所有接口绑定都会从头开始重新计算。这可能会导致程序语义变化,所以只有在确实需要重新实现接口时才这样做

这也说明了另一种脆弱基类失败的形式。假设你在编写Charlie时,Bravo没有Baz方法。如果你编写Charlie来重新实现IFoo,那么Bravo的作者在之后添加Baz - 也许Bravo的作者在你公司的另一个团队 - 将更改Charlie中的接口绑定,即使那不是Bravo的作者所期望的。

有关详细信息,请参见我的文章:

http://blogs.msdn.com/b/ericlippert/archive/2011/12/08/so-many-interfaces-part-two.aspx


1
非常感谢您抽出时间通过技术赋予陌生人力量。+1 - wootscootinboogie
1
@wootscootinboogie:不用谢,这是我的荣幸。我从这个网站上学到了很多关于人们如何使用产品以及他们犯的错误。 - Eric Lippert

4

由于 Item 继承自 IDisplayable,因此任何从 Item 派生的内容也必须实现 IDisplayable。因此,将 IDisplayable 显式添加到这些类中是多余的。

编辑:@EricLippert 在下面提出了有关重新实现接口的影响的重要观点。我保留此答案以保留评论中的讨论。


足够简单,我并不是在寻找什么惊天动地的启示,主要只是惯例和现实世界的例子。 - wootscootinboogie
这是多余的,但在某些情况下这样做很有用,也许是为了清楚地说明类实现了什么?(个人认为,通常情况下这是有用的...) - Matthew Watson
2
有一个像“Item”这样的通用名称,我同意@MatthewWatson的看法。为了避免这种情况,我建议您使用来自您领域的更具体的术语,暗示该项可显示。即使是“DisplayableItem”也会有所改善。 - neontapir
我想使用一个非常通用的例子,以便人们可以理解主要内容而不涉及具体细节,看起来我找到了答案:它是...不必要的冗余。 - wootscootinboogie
这个答案误导性地不完整。 - Eric Lippert

0
当你说“显式实现接口”时,我认为你的意思是在类声明中包含接口。显式实现接口有不同的具体含义。 话虽如此,声明派生类实现基类实现的接口是不正确的,尽管编译器会允许它,因为派生类不包含该接口的任何实现。 考虑一下如果基类显式实现了接口会发生什么。
abstract class Item : IDisplayable
{
    void IDisplayable.Print() { ... }
}

现在派生类无法访问基类方法,派生类的消费者也无法访问。当然,消费者应该使用接口而不是具体的派生类,这样就可以访问显式接口方法。但在派生类本身的范围内,它可能不知道基类实现的接口。只知道某些成员是抽象的、受保护的或虚拟的。


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