C# 接口。隐式实现与显式实现

683

在C#中,实现接口的方式有隐式显式两种方式,它们之间有什么区别?

何时使用隐式和何时使用显式?

使用其中一种方法有哪些利弊呢?


微软的官方指南(取自第一版Framework Design Guidelines)指出不建议使用显式实现,因为这会导致代码出现意外行为。

我认为这个指南在非IoC时代是非常有效的,当你不以接口方式传递事物时。

有人可以讨论一下这个方面吗?


1
是的,应该避免使用显式接口,更专业的方法是实现ISP(接口隔离原则),这里有一篇详细的文章:http://www.codeproject.com/Articles/1000374/Explicit-Interface-VS-Implicit-Interface-in-Csharp - Shivprasad Koirala
13个回答

525

隐式实现 是指你通过类的成员定义接口。 显式实现 是指在接口上定义方法。我知道这听起来很困惑,但是这里是我的意思:IList.CopyTo 会被隐式实现为:

public void CopyTo(Array array, int index)
{
    throw new NotImplementedException();
}

并显式地表达为:

void ICollection.CopyTo(Array array, int index)
{
    throw new NotImplementedException();
}

隐式实现允许您通过将接口转换为创建的类以及接口本身的方式访问该接口。显式实现仅允许您将其转换为接口本身来访问该接口。

MyClass myClass = new MyClass(); // Declared as concrete class
myclass.CopyTo //invalid with explicit
((IList)myClass).CopyTo //valid with explicit.

我主要使用显式定义以保持实现的清晰,或者当我需要两个实现时使用。不管怎样,我很少使用它。

我相信还有更多使用/不使用显式定义的原因,其他人会发布。

请参见此线程中下一篇文章,其中包含每个原因的优秀论据。


9
我知道这篇文章很旧,但我觉得它非常有用。需要注意的一件事是,如果不清楚(因为对我来说并不清楚),在这个示例中,隐式的一个有public关键字...否则你会收到错误信息。 - jharr100
Jeffrey Richter的CLR via C# 4版第13章展示了一个不需要转换的示例:internal struct SomeValueType:IComparable { private Int32 m_x; public SomeValueType(Int32 x){m_x = x; } public Int32 CompareTo(SomeValueType other){...);} Int32 IComparable.CompareTo(Object other){ return CompareTo((SomeValueType)other); } }public static void Main(){ SomeValueType v = new SomeValueType(0); Object o = new Object(); Int32 n = v.CompareTo(v); //无装箱 n = v.CompareTo(o); //编译时错误 } - Andy Dent
1
今天我遇到了一个罕见的情况,需要使用显式接口:一个由接口生成器生成的字段的类,该生成器将该字段创建为私有字段(Xamarin针对iOS,使用iOS storyboard)。而且,有一个接口可以公开该字段(public readonly)。我本可以更改接口中getter的名称,但现有名称是对象最合适的逻辑名称。因此,我使用显式实现引用了私有字段:UISwitch IScoreRegPlayerViewCell.markerSwitch { get { return markerSwitch; } } - ToolmakerSteve
1
编程的恐惧。好吧,已经很好地揭穿了! - Liquid Core
1
@ToolmakerSteve 另一种需要显式实现至少一个接口成员的情况(更常见)是实现具有相同签名但返回类型不同的多个接口成员。这可能是由于接口继承而发生,就像 IEnumerator<T>.CurrentIEnumerable<T>.GetEnumerator()ISet<T>.Add(T) 一样。这在 另一个答案 中提到过。 - phoog
显示剩余2条评论

211

隐式定义是将接口要求的方法/属性等直接作为公共方法添加到类中。

显式定义强制将成员仅在直接使用接口时才暴露,而不是底层实现。这在大多数情况下都是首选。

  1. 通过直接使用接口,您不会承认并将代码与底层实现耦合在一起。
  2. 如果您已经在代码中有一个公共属性Name,并且您想要实现一个具有Name属性的接口,那么明确地执行它将使这两个属性保持分离。即使它们正在做同样的事情,我仍然会将显式调用委托给Name属性。你永远不知道,以后可能会想要更改普通类和接口属性Name如何工作。
  3. 如果您隐式实现接口,则您的类现在公开了可能仅与接口客户端相关的新行为,并且这意味着您没有使类足够简洁(我的观点)。

2
你在这里提出了一些很好的观点,特别是A。我通常将我的类作为接口传递,但我从来没有从那个角度去考虑过它。 - mattlant
5
我不确定我是否同意C点。一只猫对象可能实现IEatable接口,但是吃(Eat())是一件基本的事情。在你使用“原始”对象时,有时你只想直接调用一只猫的Eat()方法,而不是通过IEatable接口调用,对吧? - LegendLength
69
我知道一些地方,那里的“猫”可以被食用而没有异议。 - Humberto
32
我完全不同意上述观点,并认为使用显式接口是一种导致灾难的做法,而不是根据 OOP 或 OOD 的定义(请参见我关于多态性的回答)。 - Valentin Kuzub
2
请参见:https://dev59.com/DG855IYBdhLWcg3w3Ibr - Zack Jannsen
显示剩余6条评论

74

除了已经提供的优秀答案之外,有一些情况下需要显式实现,这样编译器才能弄清所需内容。 IEnumerable<T> 是一个主要例子,在使用中可能会经常遇到。

下面是一个例子:

public abstract class StringList : IEnumerable<string>
{
    private string[] _list = new string[] {"foo", "bar", "baz"};

    // ...

    #region IEnumerable<string> Members
    public IEnumerator<string> GetEnumerator()
    {
        foreach (string s in _list)
        { yield return s; }
    }
    #endregion

    #region IEnumerable Members
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
    #endregion
}
在这里,IEnumerable<string>实现了IEnumerable,因此我们也需要实现。但是要注意,通用版本和普通版本都使用相同方法签名的函数实现(C#忽略返回类型)。这是完全合法和正常的。编译器如何解析要使用哪一个?它强制你只有最多一个隐式定义,然后它可以解析它需要的任何内容。
例如:
StringList sl = new StringList();

// uses the implicit definition.
IEnumerator<string> enumerableString = sl.GetEnumerator();
// same as above, only a little more explicit.
IEnumerator<string> enumerableString2 = ((IEnumerable<string>)sl).GetEnumerator();
// returns the same as above, but via the explicit definition
IEnumerator enumerableStuff = ((IEnumerable)sl).GetEnumerator();

注意:IEnumerable的显式定义中的那个小小的间接性质之所以有效,是因为在函数内部编译器知道变量的实际类型是StringList,从而解析函数调用。这是一些.NET核心接口似乎已经积累了几层抽象的巧妙小事实。


5
两年后,我只能说“好问题”。除了我可能是从另一个抽象的项目中复制了那段代码之外,我一无所知。 - Matthew Scharley
4
@Tassadaque,你是正确的,但我认为Matthew Scharley在上面的帖子中的观点并没有被忽略。 - funkymushroom

35

引用来自CLR via C#的Jeffrey Richter的话:
(EIMI代表Explicit Interface Method Implementation)

使用EIMI时,您需要理解一些相关影响的重要性。由于这些影响,您应该尽可能避免使用EIMI。幸运的是,通用接口可以帮助您避免使用EIMI。但是在某些情况下,您仍然需要使用它们(例如实现两个具有相同名称和签名的接口方法)。以下是EIMI的主要问题:

  • 没有文档说明类型如何具体实现EIMI方法,并且没有Microsoft Visual Studio IntelliSense支持。
  • 值类型实例在转换为接口时会被装箱。
  • EIMI无法被派生类型调用。

如果您使用接口引用,则任何虚拟链都可以在任何派生类上显式替换为EIMI,当将此类对象强制转换为接口时,将忽略您的虚拟链并调用显式实现。这与多态性完全不同。

EIMI还可用于从基本Framework接口实现(例如IEnumerable<T>)中隐藏非强类型化接口成员,因此您的类不直接公开非强类型化方法,但是语法正确。


2
重新实现接口虽然合法,但通常是可疑的。显式实现通常应该链接到一个虚拟方法,直接或通过包装逻辑,这应该绑定在派生类上。虽然可以使用接口以不符合正确OOP约定的方式,但这并不意味着它们不能被更好地使用。 - supercat
4
EIMI和IEMI代表什么? - Dzienny
8
“通常我认为接口最多只是半面向对象(OOP)的特征,它提供继承,但并没有提供真正的多态性。” 我强烈反对这种观点。相反,接口与多态性有关,而不是主要与继承有关。它们将多个分类分配给一种类型。如果可以的话,避免使用IEMI,并像@supercat建议的那样委托。不要避免使用接口。 - Aluan Haddad
1
"EIMI不能被派生类型调用。"<<什么?这不是真的。如果我在一个类型上显式实现了接口,然后从该类型派生,我仍然可以将其转换为接口以调用方法,就像我对其实现的类型一样。因此,我不确定您在说什么。即使在派生类型内部,我也可以将“this”简单地转换为相关的接口,以达到显式实现方法的目的。 - Triynko
EIMI是“显式接口成员实现”。并非所有接口成员都是方法,有些是属性。 - phoog
显示剩余3条评论

35

原因 #1

当我想要防止“按照实现方式编程”时,我倾向于使用显式接口实现 (设计模式中的设计原则)。

例如,在基于MVP的Web应用程序中:

public interface INavigator {
    void Redirect(string url);
}

public sealed class StandardNavigator : INavigator {
    void INavigator.Redirect(string url) {
        Response.Redirect(url);
    }
}

现在,另一个类(如Presenter)不太可能依赖于StandardNavigator的实现,而更可能依赖于INavigator接口(因为要使用Redirect方法需要将实现转换为接口)。

原因 #2

我选择显式接口实现的另一个原因是为了保持类的“默认”接口更加清晰。例如,如果我正在开发一个ASP.NET服务器控件,我可能想要两个接口:

  1. 该类的主要接口,由网页开发人员使用;和
  2. 由我开发的Presenter使用的“隐藏”的接口来处理控件逻辑

下面是一个简单的例子。这是一个列出客户的组合框控件。在这个例子中,网页开发人员不关心如何填充这个列表,他们只想能够通过GUID选择一个客户或获取所选客户的GUID。Presenter会在第一个页面加载时填充框,而这个Presenter被该控件封装。

public sealed class CustomerComboBox : ComboBox, ICustomerComboBox {
    private readonly CustomerComboBoxPresenter presenter;

    public CustomerComboBox() {
        presenter = new CustomerComboBoxPresenter(this);
    }

    protected override void OnLoad() {
        if (!Page.IsPostBack) presenter.HandleFirstLoad();
    }

    // Primary interface used by web page developers
    public Guid ClientId {
        get { return new Guid(SelectedItem.Value); }
        set { SelectedItem.Value = value.ToString(); }
    }

    // "Hidden" interface used by presenter
    IEnumerable<CustomerDto> ICustomerComboBox.DataSource { set; }
}

演示者填充数据源,网页开发人员无需知道其存在。

但这不是银弹

我不建议总是使用显式接口实现。这只是两个可能有帮助的示例。


18

我大多数情况下使用显式接口实现,以下是主要原因。

重构更加安全

更改接口时,如果编译器可以检查,则更好。这在隐式实现中更难。

有两种常见情况:

  • 向接口添加一个函数,在此之前已经存在实现该接口的类同时具有与新函数相同签名的已有方法。这可能导致意外行为,并且多次让我遭受打击。在调试时很难"看到"它,因为该函数很可能不位于文件中的其他接口方法(下面提到的自注释问题)。

  • 从接口中删除函数。隐式实现的方法将会突然变成死代码,但显式实现的方法将被编译错误捕获。即使死代码保留下来是有益的,我也想强制审查并升级它们。

遗憾的是C#没有一个关键字强制我们将方法标记为隐式实现,所以编译器可以进行额外的检查。由于需要使用 override 和 new,虚拟方法不存在上述任何问题。

注意:对于固定或很少更改的接口(通常来自供应商API),这不是问题。但对于我的接口,我无法预测它们何时/如何更改。

自我注释

如果我在一个类中看到 'public bool Execute()',我将需要额外的工作来确定它是接口的一部分。可能需要有人在注释中写明,或者将其放在其他接口实现的组中,所有这些都在区域或分组注释下面,说明是“ITask的实现”。当然,这仅适用于组头不在屏幕外的情况。

而:'bool ITask.Execute()'是清晰且明确的。

接口实现清晰分离

我认为接口比公共方法更“公开”,因为它们被设计为仅公开具体类型的一小部分表面区域,将类型降低到能力、行为、一组特征等。在实现中,保持这种分离是有用的。

当我查看类的代码时,当我遇到显式接口实现时,我的大脑会进入“代码合同”模式。通常这些实现只是转发到其他方法,但有时它们会进行额外的状态/参数检查,将传入参数转换为更好地匹配内部要求,甚至进行版本控制翻译(即多代接口都对通用实现进行计算)。

(我意识到公共项也是代码合同,但接口更加强大,尤其是在一个以接口为驱动的代码库中,直接使用具体类型通常是内部代码的标志。)

相关:Jon提出的上述原因2

等等

此外,在其他答案中已经提到的优点:

问题

并非所有情况都愉快。有些情况下我会坚持隐式:

  • 值类型,因为这将需要装箱,并降低性能。这不是一个严格的规则,取决于接口及其预期用途。IComparable?隐式。IFormattable?可能是显式。
  • 具有经常直接调用方法的系统接口(如IDisposable.Dispose)。

此外,当你确实拥有具体类型并想要调用显式接口方法时,进行转换可能会很麻烦。我有两种处理方式:

  1. 添加公共接口并将接口方法转发到它们以进行实现。通常在内部使用较简单的接口时发生。
  2. (我更喜欢的方法)添加一个public IMyInterface I { get { return this; } }(应该被内联),然后调用foo.I.InterfaceMethod()。 如果有多个需要这种能力的接口,请将名称扩展到I之外(据我的经验,我很少需要这种情况)。

18

除了已经提到的其他原因外,这种情况是在一个类实现了两个具有相同名称和签名的属性/方法的不同接口时发生的。

/// <summary>
/// This is a Book
/// </summary>
interface IBook
{
    string Title { get; }
    string ISBN { get; }
}

/// <summary>
/// This is a Person
/// </summary>
interface IPerson
{
    string Title { get; }
    string Forename { get; }
    string Surname { get; }
}

/// <summary>
/// This is some freaky book-person.
/// </summary>
class Class1 : IBook, IPerson
{
    /// <summary>
    /// This method is shared by both Book and Person
    /// </summary>
    public string Title
    {
        get
        {
            string personTitle = "Mr";
            string bookTitle = "The Hitchhikers Guide to the Galaxy";

            // What do we do here?
            return null;
        }
    }

    #region IPerson Members

    public string Forename
    {
        get { return "Lee"; }
    }

    public string Surname
    {
        get { return "Oades"; }
    }

    #endregion

    #region IBook Members

    public string ISBN
    {
        get { return "1-904048-46-3"; }
    }

    #endregion
}

这段代码可以编译和运行,但Title属性是共享的。

显然,我们希望根据对Class1作为Book还是Person进行处理来返回Title的值。这时候我们可以使用显式接口。

string IBook.Title
{
    get
    {
        return "The Hitchhikers Guide to the Galaxy";
    }
}

string IPerson.Title
{
    get
    {
        return "Mr";
    }
}

public string Title
{
    get { return "Still shared"; }
}

请注意,显式接口定义被推断为公共的,因此您不能显式地将它们声明为public(或其他)。

还要注意,您仍然可以拥有一个“共享”版本(如上所示),但尽管这是可能的,但存在这样一个属性是值得怀疑的。也许它可以用作 Title 的默认实现,以便现有代码不必修改就能将 Class1 强制转换为 IBook 或 IPerson。

如果您没有定义“共享”(隐式)标题,则 Class1 的使用者必须首先明确地将 Class1 的实例强制转换为 IBook 或 IPerson - 否则,代码将无法编译。


8
如果你显式实现接口,你只能通过与该接口类型相同的引用来引用接口成员。与实现类类型相同的引用将不会暴露这些接口成员。
如果你的实现类不是公共的(除了用于创建类的方法(可能是工厂或IoC容器)和接口方法(当然),那么我认为显式实现接口没有任何优势。
否则,显式实现接口可以确保不使用对你具体实现类的引用,从而允许你以后更改该实现。我想,“确保”就是“优点”。一个良好设计的实现可以在不显式实现接口的情况下完成这一点。
在我看来,缺点是你会发现自己需要在实现代码中将类型强制转换为/从接口,以便访问非公共成员。
像许多事情一样,优点也是缺点(反之亦然)。显式实现接口将确保你的具体类实现代码不被暴露。

1
很好的回答,比尔。其他答案也很好,但你提供了一些额外的客观观点(除了你的意见),这使我更容易理解。就像大多数事物一样,隐式或显式实现都有利弊,因此你只需为特定场景或用例使用最佳实践即可。我认为那些试图更好地理解这一点的人们将受益于阅读你的答案。 - Daniel Eagle

6
每个实现接口的类成员都会导出一个声明,语义上类似于VB.NET中编写接口声明的方式,例如:

Public Overridable Function Foo() As Integer Implements IFoo.Foo

尽管类成员的名称通常与接口成员相匹配,并且类成员通常是公共的,但这些都不是必需的。我们也可以声明:

Protected Overridable Function IFoo_Foo() As Integer Implements IFoo.Foo

在这种情况下,类及其派生类将被允许使用名称“IFoo_Foo”访问类成员,但外部世界只能通过将其强制转换为“IFoo”来访问该特定成员。这种方法通常适用于接口方法在所有实现中都具有指定行为,但仅对某些实现具有有用行为的情况[例如,只读集合的“IList<T>.Add”方法的指定行为是抛出“NotSupportedException”异常]。不幸的是,在C#中实现接口的唯一正确方式是:
int IFoo.Foo() { return IFoo_Foo(); }
protected virtual int IFoo_Foo() { ... real code goes here ... }

不如之前好。

5

隐式接口实现是指您拥有与接口相同签名的方法。

显式接口实现是指您明确声明该方法属于哪个接口。

interface I1
{
    void implicitExample();
}

interface I2
{
    void explicitExample();
}


class C : I1, I2
{
    void implicitExample()
    {
        Console.WriteLine("I1.implicitExample()");
    }


    void I2.explicitExample()
    {
        Console.WriteLine("I2.explicitExample()");
    }
}

MSDN: 隐式和显式接口实现


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