Интерфейсы C #. Неявная реализация против явной реализации

632

Каковы различия в реализации интерфейсов неявно и явно в C #?

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

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


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

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

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

Себ Нильссон
источник
Читайте полную статью об интерфейсах C #: planetofcoders.com/c-interfaces
Гаурав Агравал
Да, явных интерфейсов следует избегать, и для реализации ISP (принцип разделения интерфейса) следует использовать более профессиональный подход. Вот подробная статья о том же codeproject.com/Articles/1000374/…
Shivprasad Koirala

Ответы:

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.

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

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

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

mattlant
источник
8
Я знаю, что этот пост старый, но я нашел его очень полезным - одна вещь, на которую следует обратить внимание, если она не ясна, потому что это было не для меня, это то, что в примере у неявного есть publicключевое слово ... иначе вы получите ошибку
jharr100
CLR Джеффри Рихтера через C # 4 ed ch 13 показывает пример, где приведение не требуется: внутренняя структура 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); Объект o = новый объект (); Int32 n = v.CompareTo (v); // Нет бокса n = v.CompareTo (o); // ошибка времени компиляции}
Энди Дент
1
Сегодня я столкнулся с редкой ситуацией, которая требовала использования явного интерфейса: класс с полем, созданным конструктором интерфейса, который создает поле как личное (Xamarin предназначается для iOS, используя раскадровку iOS). И интерфейс, где имеет смысл выставить это поле (общедоступный только для чтения). Я мог бы изменить имя геттера в интерфейсе, но существующее имя было наиболее логичным для объекта. Так что вместо этого я сделал явную реализацию со ссылкой на частное поле: UISwitch IScoreRegPlayerViewCell.markerSwitch { get { return markerSwitch; } }.
ToolmakerSteve
1
Ужасы программирования. Ну, красиво развенчан!
Liquid Core
1
@ToolmakerSteve Другая ситуация (довольно распространенная), которая требует явной реализации по крайней мере одного члена интерфейса, - это реализация нескольких интерфейсов, которые имеют члены с одинаковой сигнатурой, но разными типами возврата. Это может произойти из - за наследование интерфейсов, как это происходит с IEnumerator<T>.Current, IEnumerable<T>.GetEnumerator()и ISet<T>.Add(T). Это упоминается в другом ответе .
Фог
201

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

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

  1. Работая напрямую с интерфейсом, вы не подтверждаете и не привязываете свой код к базовой реализации.
  2. Если, например, у вас уже есть, скажем, открытое свойство Name в вашем коде, и вы хотите реализовать интерфейс, который также имеет свойство Name, то если вы сделаете это явно, они будут разделены. Даже если бы они делали то же самое, я бы все же делегировал явный вызов свойству Name. Вы никогда не знаете, возможно, вы захотите изменить то, как Name работает для нормального класса и как Name, свойство интерфейса работает позже.
  3. Если вы реализуете интерфейс неявно, то ваш класс теперь демонстрирует новые поведения, которые могут иметь отношение только к клиенту интерфейса, и это означает, что вы недостаточно кратко сохраняете свои классы (мое мнение).
Фил Беннетт
источник
2
Вы делаете некоторые хорошие моменты здесь. особенно А. Я обычно пропускаю свои уроки как интерфейс в любом случае, но я никогда не думал об этом с этой точки зрения.
Mattlant
5
Я не уверен, что согласен с пунктом C. Объект Cat может реализовывать IEatable, но Eat () является основной частью. Могут быть случаи, когда вы захотите просто вызвать Eat () для Cat, когда вы используете «сырой» объект, а не через интерфейс IEatable, нет?
LegendLength
59
Я знаю некоторые места, где Catдействительно IEatableнет возражений.
Умберто
26
Я полностью не согласен со всем вышеперечисленным и скажу, что использование явных интерфейсов - это рецепт катастрофы, а не ООП или ООД по их определению (см. Мой ответ о полиморфизме)
Валентин Кузуб,
2
Смотрите также: stackoverflow.com/questions/4103300/…
Зак Яннсен,
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: Через 2 года все, что я могу сказать, это «Хороший вопрос». Я понятия не имею, разве что я скопировал этот код из того, над чем работал abstract.
Мэтью Шарли
4
@Tassadaque, вы правы, но я думаю, что смысл поста Мэтью Шарли выше не потерян.
funkymushroom
35

По словам Джеффри Рихтера из CLR с помощью C #
( EIMI означает Е Xplicit Я nterface М еню Я mplementation)

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

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

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

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

Валентин Кузуб
источник
2
Реализация интерфейсов, хотя и легальная, в лучшем случае сомнительна. Явные реализации, как правило, должны связываться с виртуальным методом либо напрямую, либо с помощью логики обтекания, которая должна связываться с производными классами. Хотя, безусловно, возможно использовать интерфейсы способами, которые враждебны соответствующим соглашениям ООП, это не означает, что их нельзя использовать лучше.
суперкат
4
@Valentin Что означают EIMI и IEMI?
Дзенни
Явная реализация метода интерфейса
scobi
5
-1 для «Обычно я вижу интерфейсы как полу (в лучшем случае) функцию ООП, она обеспечивает наследование, но не обеспечивает настоящий полиморфизм». Я категорически не согласен. Наоборот, все интерфейсы - это полиморфизм, а не наследование. Они приписывают несколько классификаций к типу. Избегайте IEMI, когда можете, и делегируйте, как подсказал @supercat, когда не можете. Не избегайте интерфейсов.
Алуан Хаддад
1
«EIMI не может быть вызван производным типом». << ЧТО? Это не правда. Если я явно реализую интерфейс для типа, то я наследую от этого типа, я все еще могу привести его к интерфейсу для вызова метода, точно так же, как и для типа, для которого он реализован. Так что не уверен, о чем ты говоришь. Даже внутри производного типа я могу просто привести «this» к рассматриваемому интерфейсу, чтобы получить явно реализованный метод.
Трийнко
34

Причина № 1

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

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

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

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

Теперь другой класс (например, презентатор ) с меньшей вероятностью будет зависеть от реализации 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 # нет ключевого слова, которое заставляет нас помечать метод как неявную реализацию, поэтому компилятор может выполнять дополнительные проверки. Виртуальные методы не имеют ни одной из вышеперечисленных проблем из-за обязательного использования 'override' и 'new'.

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

Это самодокументирование

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

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

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

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

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

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

Связанный: Причина 2 выше Джон .

И так далее

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

Проблемы

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

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

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

  1. Добавьте public и передайте им методы интерфейса для реализации. Обычно это происходит с более простыми интерфейсами при внутренней работе.
  2. (Мой предпочтительный метод) Добавьте public IMyInterface I { get { return this; } }(который должен быть встроен) и вызовите foo.I.InterfaceMethod(). Если несколько интерфейсов нуждаются в этой способности, расширьте имя за пределы I (по моему опыту, это редко случается).
scobi
источник
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 ... }

Не так приятно.

Supercat
источник
2

Одним из важных применений явной реализации интерфейса является необходимость реализации интерфейсов со смешанной видимостью. .

Проблема и решение хорошо объяснены в статье C # Внутренний интерфейс .

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

nrodic
источник
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 #, даже не позволяют этого .

Марк Сигрист
источник