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

时间:2020-03-06 14:49:21  来源:igfitidea点击:

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

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

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

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

我认为,在我们未将事物作为接口传递时,该指南在IoC之前非常有效。

有人也可以谈谈这方面吗?

解决方案

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

我主要使用显式的来保持实现的整洁,或者在我需要两种实现时。但是不管我很少使用它。

我敢肯定还有其他人会使用/不使用它的更多理由。

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

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

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

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

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

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

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

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

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

除了已经提供的出色答案外,在某些情况下,还要求显式实现以使编译器能够确定所需的内容。以" 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",因此我们也需要这样做。但是请放心,通用版本和普通版本都使用相同的方法签名(此方法忽略返回类型)来实现功能。这是完全合法的。编译器如何解析要使用的?它迫使我们最多只有一个隐式定义,然后它可以解析所需的任何内容。

IE。

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核心接口的某些抽象层的绝妙事实似乎已经积累。

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

/// <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,否则代码将无法编译。

原因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服务器控件,则可能需要两个接口:

  • 该类的主界面,供网页开发人员使用;和
  • 我为演示者开发的用于处理控件逻辑的"隐藏"界面

下面是一个简单的示例。这是一个列出客户的组合框控件。在此示例中,网页开发人员对填充列表不感兴趣。相反,他们只是希望能够通过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; }
}

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

但这不是银炮弹

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