Интерфейсы C #. Неявная реализация против явной реализации - PullRequest
605 голосов
/ 27 сентября 2008

Чем отличаются реализации интерфейсов неявно и явно в C #?

Когда вы должны использовать неявное и когда вы должны использовать явное?

Есть ли плюсы и / или минусы одного или другого?


Официальное руководство Microsoft (из первого издания Framework Design Guidelines ) гласит, что использование явных реализаций не рекомендуется , поскольку это дает коду неожиданное поведение.

Я думаю, что это руководство очень действительно в период до IoC , когда вы не передаете вещи как интерфейсы.

Может ли кто-нибудь затронуть и этот аспект?

Ответы [ 11 ]

464 голосов
/ 27 сентября 2008

Неявный - это когда вы определяете свой интерфейс с помощью члена вашего класса. Явное - это когда вы определяете методы в вашем классе на интерфейсе. Я знаю, это звучит странно, но вот что я имею в виду: 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.

Я использую явное в первую очередь для поддержания чистоты реализации или когда мне нужны две реализации. Но независимо от того, я редко использую это.

Я уверен, что есть и другие причины использовать / не использовать его, которые будут публиковать другие.

См. следующий пост в этой теме для отличных рассуждений за каждым.

187 голосов
/ 27 сентября 2008

Неявное определение будет состоять в том, чтобы просто добавить методы / свойства и т. Д., Требуемые интерфейсом, непосредственно к классу в качестве открытых методов.

Явное определение заставляет участников открываться только тогда, когда вы работаете с интерфейсом напрямую, а не с базовой реализацией. Это является предпочтительным в большинстве случаев.

  1. Работая напрямую с интерфейсом, вы не подтверждаете, и привязка вашего кода к базовой реализации.
  2. Если у вас уже есть, скажем, публичная собственность ваш код, и вы хотите реализовать интерфейс, который также имеет Имя свойства, делая это явно, будет держать два отдельных. Четное если бы они делали то же самое, я бы по-прежнему делегировать явное вызов свойства Name. Вы никогда не знаете, вы можете изменить как Name работает для нормального класса и как Name, интерфейс собственность работает позже.
  3. Если вы реализуете интерфейс неявно, то теперь ваш класс предоставляет новое поведение, которое может иметь отношение только к клиенту интерфейс, и это означает, что вы не держите ваши классы лаконичны достаточно (мое мнение).
65 голосов
/ 27 сентября 2008

В дополнение к уже предоставленным отличным ответам, в некоторых случаях ТРЕБУЕТСЯ явная реализация, чтобы компилятор мог выяснить, что требуется. Взгляните на 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.

31 голосов
/ 14 июня 2010

Причина № 1

Я склонен использовать явную реализацию интерфейса, когда хочу отговорить «программирование до реализации» ( Принципы проектирования из шаблонов проектирования ).

Например, в веб-приложении на основе MVP :

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

Докладчик заполняет источник данных, и разработчик веб-страницы никогда не должен знать о его существовании.

Но это не серебряное пушечное ядро ​​

Я бы не рекомендовал всегда использовать явные реализации интерфейса. Это всего лишь два примера, в которых они могут быть полезны.

30 голосов
/ 09 февраля 2011

Цитировать Джеффри Рихтера из CLR через C #
( EIMI означает E явное I интерфейс M метод I mplementation)

Это крайне важно для вас понять некоторые последствия, которые существуют при использовании EIMI. И из-за эти последствия, вы должны попытаться избегайте EIMI, насколько это возможно. К счастью, универсальные интерфейсы помогают Вы избегаете EIMI совсем немного. Но там могут быть времена, когда вам понадобится использовать их (например, реализовать два методы интерфейса с тем же именем и подпись). Вот большие проблемы с EIMI:

  • Нет документации, объясняющей, как конкретно реализует метод EIMI, и там не является Microsoft Visual Studio Поддержка IntelliSense .
  • Экземпляры типа значения упаковываются при приведении к интерфейсу.
  • EIMI не может быть вызван производным типом.

Если вы используете ссылку на интерфейс, любая виртуальная цепочка может быть явно заменена EIMI в любом производном классе, и когда объект такого типа приводится к интерфейсу, ваша виртуальная цепочка игнорируется и вызывается явная реализация. Это совсем не полиморфизм.

EIMI также можно использовать для сокрытия элементов интерфейса со слабой типизацией от реализаций базовых интерфейсов Framework, таких как IEnumerable , чтобы ваш класс не предоставлял метод не строго типизированной напрямую, а синтаксически правильный.

18 голосов
/ 01 октября 2008

В дополнение к другим уже указанным причинам в этой ситуации класс реализует два разных интерфейса, которые имеют свойство / метод с одинаковыми именем и сигнатурой.

/// <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 в первую очередь - в противном случае код не будет компилироваться.

14 голосов
/ 15 августа 2014

Я использую явную реализацию интерфейса большую часть времени. Вот основные причины.

Рефакторинг безопаснее

При смене интерфейса лучше, если компилятор может это проверить. Это сложнее с неявными реализациями.

На ум приходят два общих случая:

  • Добавление функции к интерфейсу, где уже существует класс, реализующий этот интерфейс, с методом с такой же сигнатурой, что и у нового . Это может привести к неожиданному поведению, и укусил меня несколько раз. Трудно «увидеть» при отладке, потому что эта функция, вероятно, не находится с другими методами интерфейса в файле (проблема самодокументирования, упомянутая ниже).

  • Удаление функции из интерфейса . Неявно реализованные методы будут внезапно мертвым кодом, но явно реализованные методы будут захвачены ошибкой компиляции. Даже если мертвый код хорошо хранить, я хочу, чтобы его просмотрели и продвигали.

К сожалению, в C # нет ключевого слова, которое заставляет нас помечать метод как неявную реализацию, поэтому компилятор может выполнять дополнительные проверки. Виртуальные методы не имеют ни одной из вышеперечисленных проблем из-за обязательного использования 'override' и 'new'.

Примечание: для фиксированных или редко меняющихся интерфейсов (как правило, из API поставщика) это не проблема. Однако для моих собственных интерфейсов я не могу предсказать, когда / как они изменятся.

Самодокументируемый

Если я увижу public bool Execute () в классе, потребуется дополнительная работа, чтобы выяснить, что это часть интерфейса. Кто-то, вероятно, должен будет прокомментировать это, говоря об этом, или поместить его в группу других реализаций интерфейса, все в рамках региона или группового комментария, говорящего «реализация ITask». Конечно, это работает только в том случае, если заголовок группы не за кадром.

Принимая во внимание, что 'bool ITask.Execute ()' является ясным и однозначным.

Четкое разделение реализации интерфейса

Я считаю, что интерфейсы являются более «общедоступными», чем общедоступные методы, потому что они созданы так, чтобы показать лишь небольшую часть поверхности конкретного типа. Они сводят тип к возможности, поведению, набору признаков и т. Д. И в реализации, я думаю, полезно сохранить это разделение.

Когда я просматриваю код класса, когда я сталкиваюсь с явными реализациями интерфейса, мой мозг переключается в режим «контракта кода». Часто эти реализации просто перенаправляют на другие методы, но иногда они будут выполнять дополнительную проверку состояния / параметра, преобразование входящих параметров для лучшего соответствия внутренним требованиям или даже перевод для целей управления версиями (то есть несколько поколений интерфейсов, все переходящие к общим реализациям).

(Я понимаю, что публичные контракты также являются контрактами на код, но интерфейсы намного сильнее, особенно в кодовой базе, управляемой интерфейсом, где прямое использование конкретных типов обычно является признаком кода только для внутреннего использования.)

Связано: Причина 2 выше от Jon .

И так далее

Плюс преимущества, уже упомянутые в других ответах здесь:

Проблемы

Это не все веселье и счастье. В некоторых случаях я придерживаюсь следствий:

  • Типы значений, потому что для этого потребуется бокс и меньший перф. Это не строгое правило, оно зависит от интерфейса и способа его использования. IComparable? Неявные. IFormattable? Возможно, явно.
  • Тривиальные системные интерфейсы, в которых есть методы, которые часто вызываются напрямую (например, IDisposable.Dispose).

Кроме того, может быть затруднительно выполнять приведение, когда у вас действительно есть конкретный тип и вы хотите вызвать явный интерфейсный метод. Я имею дело с этим одним из двух способов:

  1. Добавьте открытые и передайте им методы интерфейса для реализации. Обычно происходит с более простыми интерфейсами при внутренней работе.
  2. (Мой предпочтительный метод) Добавьте public IMyInterface I { get { return this; } } (который должен быть встроен) и вызовите foo.I.InterfaceMethod(). Если несколько интерфейсов нуждаются в этой способности, расширьте имя за пределы I (по моему опыту, мне редко это нужно).
8 голосов
/ 27 сентября 2008

Если вы реализуете явно, вы сможете ссылаться на элементы интерфейса только через ссылку, относящуюся к типу интерфейса. Ссылка, являющаяся типом реализующего класса, не будет предоставлять эти члены интерфейса.

Если ваш реализующий класс не является общедоступным, за исключением метода, используемого для создания класса (который может быть фабрикой или IoC контейнером), и за исключением методов интерфейса (конечно), то я не вижу никакого преимущества в явной реализации интерфейсов.

В противном случае явная реализация интерфейсов гарантирует, что ссылки на ваш конкретный реализующий класс не будут использоваться, что позволит вам изменить эту реализацию позднее. «Уверен», я полагаю, является «преимуществом». Хорошо продуманная реализация может выполнить это без явной реализации.

Недостатком, на мой взгляд, является то, что в коде реализации, который имеет доступ к непубличным членам, вы найдете приведение типов к интерфейсу и из интерфейса.

Как и многие вещи, преимущество - это недостаток (и наоборот). Явно реализующие интерфейсы гарантируют, что ваш код реализации конкретного класса не будет представлен.

6 голосов
/ 26 мая 2011

Неявная реализация интерфейса - это когда у вас есть метод с такой же сигнатурой интерфейса.

Явная реализация интерфейса - это то, где вы явно объявляете, к какому интерфейсу принадлежит метод.

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: неявные и явные реализации интерфейса

5 голосов
/ 12 мая 2014

Каждый член класса, который реализует интерфейс, экспортирует объявление, которое семантически похоже на способ написания объявлений интерфейса 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 ... }

Не так хорошо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...