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


632

在C#中隐式显式实现接口有何区别?

什么时候应该使用隐式,什么时候应该使用显式?

彼此之间是否有优点和/或缺点?


Microsoft的官方指南(来自第一版Framework Design Guidelines)指出,不建议使用显式实现,因为它会给代码带来意想不到的行为。

我认为,在您未将事物作为接口传递的情况下,该指南在IoC之前非常有效

任何人都可以谈谈这方面吗?


阅读有关C#接口的完整文章:planetofcoders.com/c-interfaces
Gaurav Agrawal,

是的,应该避免使用显式接口,并且应该采用更专业的方法来实现ISP(接口隔离原则),这是同一codeproject.com/Articles/1000374/…
Shivprasad Koirala

Answers:


492

隐式是当您通过类中的成员定义接口时。显式的是您在接口上的类中定义方法时。我知道这听起来令人困惑,但这就是我的意思: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.

我主要使用显式的来保持实现的整洁,或者在我需要两个实现时。无论如何,我很少使用它。

我确信还有更多理由使用/不使用其他人会张贴的明确内容。

有关每个主题背后的出色推理,请参阅此线程中的下一篇文章


8
我知道这篇文章很旧,但是我发现它非常有用-需要注意的一件事是,如果不清楚,因为在我的例子中隐式的那个具有public关键字...否则,您将得到一个错误
jharr100 2014年

杰弗里·里希特(Jeffrey Richter)通过C#4 ed ch 13编写的CLR显示了一个不需要强制转换的示例:内部结构SomeValueType:IComparable {private Int32 m_x; 公共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); 对象o = new Object(); Int32 n = v.CompareTo(v); //没有装箱n = v.CompareTo(o); //编译时错误}
Andy Dent

1
今天,我遇到了一种罕见的情况,需要使用显式接口:一个类,该类具有由接口构建器生成的字段,该字段将字段创建为私有字段(Xamarin使用iOS Storyboard定位到iOS)。还有一个公开该字段的接口(公共只读)。我可以在接口中更改getter的名称,但是现有名称是该对象最符合逻辑的名称。因此,我做了一个明确的实现,它引用了私有字段:UISwitch IScoreRegPlayerViewCell.markerSwitch { get { return markerSwitch; } }
ToolmakerSteve

1
编程的恐怖。好吧,很好地揭穿了!
Liquid Core

1
@ToolmakerSteve要求显式实现至少一个接口成员的另一种情况(更常见)是实现多个接口,这些接口的成员具有相同的签名但返回类型不同。这可能发生,因为接口继承的,因为它与做IEnumerator<T>.CurrentIEnumerable<T>.GetEnumerator()ISet<T>.Add(T)。这是另一个答案中提到的。
phoog

201

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

显式定义仅在直接使用接口而不是基础实现时才强制成员公开。在大多数情况下,这是首选。

  1. 通过直接使用接口,您无需确认,而是将代码耦合到基础实现。
  2. 如果您的代码中已经有一个公共属性Name,并且您想实现一个也具有Name属性的接口,则显式地将二者分开。即使他们在做同样的事情,我仍然将显式调用委托给Name属性。您永远不会知道,您可能想更改Name在普通类中的工作方式以及Name(接口属性)以后的工作方式。
  3. 如果隐式实现一个接口,则您的类现在将暴露可能仅与该接口的客户端相关的新行为,这意味着您的类不够简洁(我认为)。

2
您在这里提出了一些要点。尤其是A。无论如何,我通常都会将类作为接口传递给我,但是从这个角度来看,我从未真正想到过它。
mattlant

5
我不确定我是否同意C点。Cat对象可能实现IEatable,但Eat()是事物的基本组成部分。在某些情况下,当您使用“原始”对象而不是通过IEatable接口时,只想在Cat上调用Eat(),不是吗?
09年

59
我知道有些地方Cat确实IEatable没有反对意见。
温贝托

26
我完全不同意上述所有观点,并且会说使用显式接口是灾难的
根源


68

除了已经提供的出色答案外,在某些情况下,还要求显式实现以使编译器能够确定所需的内容。以一个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();

PS:IEnumerable的显式定义中的一小部分间接操作有效,因为在函数内部,编译器知道变量的实际类型是StringList,这就是它解析函数调用的方式。用于实现某些.NET核心接口的某些抽象层的绝妙事实似乎已经积累。


5
@Tassadaque:两年之后,我只能说“好问题”。我不知道,除了也许我从正在处理的地方复制了该代码abstract
Matthew Scharley 2011年

4
@Tassadaque,您是对的,但是我认为上面的Matthew Scharley帖子的要点没有丢失。
funkymushroom's

35

要通过C#CLR从引用杰弗里里希特
EIMI意味着Ë xplicit 覆盖整个院落中号 ethod mplementation)

对于您而言,了解使用EIMI时存在的一些后果至关重要。并且由于这些后果,您应尝试尽可能避免EIMI。幸运的是,通用接口可以帮助您避免EIMI。但是有时仍然需要使用它们(例如,实现两个具有相同名称和签名的接口方法)。以下是EIMI的主要问题:

  • 没有文档说明类型如何具体实现EIMI方法,也没有Microsoft Visual Studio IntelliSense支持。
  • 值类型实例在投射到接口时会被装箱。
  • EIMI不能通过派生类型调用。

如果使用接口引用,则可以在任何派生类上将任何虚拟链显式替换为EIMI,并且将此类对象强制转换为接口时,将忽略虚拟链并调用显式实现。只是多态而已。

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


2
尽管合法,但接口的重新实现通常最多是可疑的。显式实现通常应该直接链接到虚拟方法,也应该通过包装逻辑将其链接到派生类上。尽管可以以对适当的OOP约定不利的方式使用接口,但这并不意味着不能更好地使用它们。
超级猫2014年

4
@Valentin EIMI和IEMI代表什么?
Dzienny 2014年

显式接口方法实现
scobi 2014年

5
-1表示“通常,我将接口视为Semi(至多)OOP功能,它提供了继承,但没有提供真正的多态性。” 我非常不同意。恰恰相反,接口都是关于多态性的,而不主要是关于继承的。他们将多个分类归于一个类型。如果可以,请避免使用IEMI;如果不能,则应按照@supercat的建议进行委派。不要避开接口。
Aluan Haddad

1
“ EIMI不能通过派生类型来调用。” <<什么?这不是真的。如果我在类型上显式实现接口,则可以从该类型派生出该类型,但仍可以将其强制转换为接口以调用该方法,就像实现该类型所必须的一样。所以不确定您在说什么。即使在派生类型中,我也可以将“ this”简单地转换为所讨论的接口,以达到显式实现的方法。
Triynko

34

原因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. 我为演示者开发的用于处理控件逻辑的“隐藏”界面

下面是一个简单的示例。这是一个列出客户的组合框控件。在此示例中,网页开发人员对填充列表不感兴趣。相反,他们只是希望能够通过GUID选择客户或获取所选客户的GUID。演示者将在第一页加载时填充该框,并且该演示者由控件封装。

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; }
}

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

但这不是银炮弹

我不建议始终使用显式接口实现。这些只是两个示例,它们可能会有所帮助。


19

除了已经说明的其他原因之外,在这种情况下,类正在实现两个不同的接口,这些接口的属性/方法具有相同的名称和签名。

/// <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属性是共享的。

显然,我们希望返回的Title值取决于我们是否将Class1视为一本书还是一本书。这是我们可以使用显式接口的时候。

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-因此,您不能显式声明它们为public(或其他方式)。

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

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


16

我大多数时候都使用显式接口实现。这是主要原因。

重构更安全

更改接口时,最好由编译器进行检查。对于隐式实现,这更困难。

我想到两种常见的情况:

  • 在接口中添加一个函数,在该接口中,实现该接口的现有类已经碰巧具有与新方法具有相同签名的方法。这可能会导致意外的行为,并且使我痛苦多次。调试时很难“看到”,因为该功能可能没有与文件中的其他接口方法一起定位(下面提到的自记录问题)。

  • 从接口删除功能。隐式实现的方法将突然变成死代码,但显式实现的方法将被编译错误捕获。即使死代码可以很好地保留,我也想被迫对其进行审查和推广。

不幸的是,C#没有关键字可以迫使我们将方法标记为隐式实现,因此编译器可以进行额外的检查。由于需要使用“覆盖”和“新”,因此虚拟方法没有上述任何一个问题。

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

自我记录

如果我在类中看到“ public bool Execute()”,它将需要更多的工作来确定它是接口的一部分。可能有人必须这样说,或者将其放在一组其他接口实现中,全部放在一个区域或分组注释中,说“ ITask的实现”。当然,这仅在组标题不在屏幕外时才有效。

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

界面实现清晰分离

我认为接口比公共方法更“公共”,因为它们被设计为仅暴露混凝土类型的一些表面积。它们将类型简化为功能,行为,特质等。在实现中,我认为保持这种分离是有用的。

当我浏览类的代码时,当我遇到显式的接口实现时,我的大脑转向“代码协定”模式。通常,这些实现只是简单地转交给其他方法,但是有时它们会进行额外的状态/参数检查,转换输入参数以更好地匹配内部需求,甚至进行翻译以实现版本控制(即多代接口都可以简化为通用实现)。

(我意识到,公众也是代码契约,但是接口要强大得多,尤其是在接口驱动的代码库中,直接使用具体类型通常是仅内部代码的标志。)

相关:乔恩(Jon)提出的上述原因2

等等

加上此处其他答案中已经提到的优点:

问题

并不是所有的乐趣和幸福。在某些情况下,我坚持使用隐式:

  • 值类型,因为这将需要装箱并降低性能。这不是严格的规则,它取决于接口及其使用方式。可比吗?隐式 IFormattable?可能是明确的。
  • 普通系统接口具有经常被直接调用的方法(例如IDisposable.Dispose)。

另外,当您实际上具有具体的类型并想要调用显式接口方法时,进行转换可能会很痛苦。我用以下两种方式之一来处理:

  1. 添加公共对象,并将接口方法转发给他们以进行实施。在内部工作时,通常使用更简单的接口。
  2. (我的首选方法)添加一个public IMyInterface I { get { return this; } }(应该内联)并调用foo.I.InterfaceMethod()。如果有多个接口需要此功能,请将该名称扩展到我之外(以我的经验,我很少有此需求)。

8

如果您明确实现,则只能通过接口类型的引用来引用接口成员。作为实现类类型的引用不会公开那些接口成员。

如果您的实现类不是公共的,除了用于创建该类的方法(可以是工厂或IoC容器)之外,而且除了接口方法(当然)外,那么我看不到显式实现的任何优势接口。

否则,显式实现接口可确保不使用对具体实现类的引用,从而允许您稍后更改该实现。我想,“确保”是“优势”。构造合理的实现无需显式实现即可完成此操作。

在我看来,缺点是您会发现自己在实现代码中可以访问非公共成员的接口强制类型转换。

像许多事物一样,优点是缺点(反之亦然)。显式实现接口将确保不暴露您的具体类实现代码。


1
好的答案,比尔。其他答案很好,但您(在您的观点之上)提供了一些其他客观观点,这使我更容易掌握。像大多数事物一样,隐式或显式实现也各有利弊,因此您只需要针对特定​​的场景或用例使用最佳方案即可。我想说的是,那些试图更好地弄清楚这些问题的人将从阅读您的答案中受益。
丹尼尔·伊格尔

6

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

显式接口实现是在其中显式声明该方法所属的接口的地方。

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:隐式和显式接口实现


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 ... }

不太好


2

显式接口实现的一个重要用途是在需要实现具有混合可见性的接口时。

C#内部接口一文中很好地解释了该问题和解决方案。

例如,如果要保护应用程序层之间的对象泄漏,则可以使用此技术指定可能导致泄漏的成员的不同可见性。


2

前面的答案解释了为什么最好使用 C#显式实现接口(出于正式原因)。但是,在一种情况下,必须强制执行显式实现:为了避免在接口为非接口public但实现类为时泄漏封装public

// Given:
internal interface I { void M(); }

// Then explicit implementation correctly observes encapsulation of I:
// Both ((I)CExplicit).M and CExplicit.M are accessible only internally.
public class CExplicit: I { void I.M() { } }

// However, implicit implementation breaks encapsulation of I, because
// ((I)CImplicit).M is only accessible internally, while CImplicit.M is accessible publicly.
public class CImplicit: I { public void M() { } }

上述泄漏是不可避免的,因为根据C#规范,“所有接口成员都隐式具有公共访问权限”。结果,public即使接口本身是例如,隐式实现也必须提供访问权限internal

C#中的隐式接口实现非常方便。实际上,许多程序员在任何时候,任何地方都可以使用它而无需进一步考虑。最好的情况下会导致表面混乱,最坏的情况下会导致封装泄漏。其他语言(例如F#)甚至不允许使用

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.