Делегаты против интерфейсов в C #

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

Я знаю, что делегаты служат указателями на функции, используемыми в C ++. Дело в том, что в C # они по большей части служат альтернативой интерфейсам и полиморфизму. Поскольку я могу создавать подклассы определенного класса и предоставлять им соответствующие методы для каждого из них, что предлагает делегаты в дополнение к этому? Есть ли случаи, которые предусматривают их использование, или просто улучшается ремонтопригодность кода при использовании делегатов? Вы бы порекомендовали их широкое развертывание через интерфейсы?

Я говорю исключительно о делегатах и ​​хочу отделить их роль от роли событий.


person arjacsoh    schedule 01.01.2012    source источник
comment
Дело в том, что если в C # они служат в основном альтернативой интерфейсам и полиморфизму - объясните, пожалуйста, свои рассуждения.   -  person Oded    schedule 01.01.2012
comment
Это только (косвенный) вопрос. Я воспринимаю их функциональные возможности, как описано в моем вопросе, и хочу знать, согласны ли вы и правда ли это.   -  person arjacsoh    schedule 01.01.2012


Ответы (5)


Да, делегаты во многом похожи на интерфейсы с одним методом. Тем не мение:

  • Для них в CLR встроена поддержка.
  • Для них есть поддержка в структуре, включая возможности многоадресной передачи и асинхронный вызов.
  • Есть дополнительная поддержка языка C # / VB в виде преобразований групп методов, лямбда-выражений, анонимных методов.
  • Они обязательны для событий (т.е. события и делегаты представляют собой своего рода совпадающую пару)
  • Они означают, что вам не нужно реализовывать интерфейс в отдельном классе для каждого экземпляра делегата, который вы хотите создать.

Последний пункт является наиболее важным - рассмотрите LINQ-выражение:

var query = collection.Where(x => x > 5)
                      .Select(x => x * x);

А теперь представьте, что если бы для выражения логики x > 5 и x * x вам пришлось бы написать отдельный класс для каждого выражения и реализовать интерфейс: количество бесполезного и полезного кода было бы смешным. Конечно, язык мог быть разработан для преобразования лямбда-выражений в реализации интерфейса через отдельные классы, но тогда вы все равно потеряете возможность просто написать отдельный метод и создать делегировать с этим в качестве цели. Вы также все равно потеряете многозадачные способности.

В качестве аналогичного упражнения рассмотрите зацикленные операторы, такие как while и for. Действительно ли они нам нужны, когда у нас есть goto? Неа. Но с ними жизнь намного лучше. То же самое и с делегатами, а также с свойствами, событиями и т. Д. Все они упрощают разработку.

person Jon Skeet    schedule 01.01.2012
comment
Я бы не стал рассматривать пример Linq как показывающий преимущество делегатов; даже если тип делегата Func<int, bool> был заменен интерфейсом IFunc<int, bool>, компилятор мог бы легко определить класс, реализующий этот интерфейс, и включить общедоступное статическое свойство, возвращающее частный статический экземпляр. Такой подход будет работать примерно так же, как делегаты для лямбда-выражений, которые не закрывают никакие локальные переменные, и был бы более эффективным, чем делегаты для лямбда-выражений, которые закрываются поверх локальных. - person supercat; 03.01.2012
comment
@supercat: пример LINQ показывает, насколько полезно наличие синтаксического сахара для этого. Конечно, это можно сделать с помощью интерфейсов, но этого нет в C # и не еще в Java. Обратите внимание, что для лямбда-выражения, которое закрывает локальные переменные, у вас все равно не может быть одного экземпляра, так как вам нужно будет каждый раз создавать новый экземпляр для хранения захваченной переменной. - person Jon Skeet; 03.01.2012
comment
Думаю, я бы неправильно истолковал исходный вопрос как скорее теоретический, чем практический; если бы инфраструктура .net была спроектирована вокруг интерфейсов, а не делегатов, компиляторы почти наверняка обеспечили бы те же типы поддержки для первого, что и для второго. Что касается замыканий, в настоящее время создание замыкания требует создания как объекта для хранения переменных, так и делегата, который содержит ссылку на этот объект и желаемую функцию; если бы можно было использовать интерфейсы вместо делегатов, компилятор мог бы просто создать один объект, который бы обслуживал обе функции. - person supercat; 03.01.2012
comment
@supercat: Это правда, хотя мне интересно, оптимизировано ли это как-то внутри. В своем ответе я упомянул возможность преобразования лямбда-выражений в интерфейсы, но есть и другие преимущества, связанные с асинхронностью и многоадресностью, а также тот факт, что они гарантированно являются одним методом. , а не просто интерфейс, который мог бы расширяться (и тогда он не подходил бы, например, для обработки событий, поскольку не было бы единого метода для вызова). - person Jon Skeet; 03.01.2012
comment
@JonSkeet: Добавление участников в интерфейс всегда считалось критическим изменением. Если фреймворк определяет IAction<out T,out U> как имеющий единственный член Invoke(T p1, T p2), это все, что когда-либо будет в этом интерфейсе. Вполне возможно, что у класса может быть много реализаций IAction<T,U> для множества различных комбинаций T и U; если бы у кого-то был только экземпляр такого класса, приведенный к Object, никто бы не знал, какой интерфейс вызывать, но что с того? Обычно невозможно вызвать делегата, подпись которого не известна во время компиляции, за исключением Reflection. - person supercat; 03.01.2012
comment
@supercat: Да, это всегда критическое изменение со стороны реализации, но это также будет со стороны вызывающей. И не забывайте, что с делегатами тип объявляется не только фреймворком ... Все это можно обрабатывать в той или иной форме - но, эффективно сделав соглашение частью фреймворка / языка / CLR, он станет более аккуратным. ... очень похоже на свойства, циклы foreach и т. д. - person Jon Skeet; 03.01.2012
comment
@JonSkeet: если бы каждый тип делегата, который принимал только параметры-значения, был определен как реализующий интерфейс IAction<T,U,...>, я бы ожидал, что большая часть кода могла бы принимать типы интерфейса, а не требовать делегатов; казалось бы, лучшее из обоих миров. Такие вещи, как процедуры управления подпиской на события, потребуют явной поддержки для каждого возможного количества параметров, но если бы кто-то принял обычный шаблон событий, это не было бы проблемой. - person supercat; 03.01.2012
comment
@supercat: Если бы дженерики появились до .NET 1, возможно, был бы выбран именно такой подход. Таким образом, он не будет поддерживать параметры ref / out, и вам также понадобится IFunc, но это может сработать ... - person Jon Skeet; 04.01.2012
comment
@JonSkeet: Кстати, я только что реализовал подход, который, возможно, можно было бы добавить в .net 5.0, ничего не нарушая: пусть каждый тип делегата (например, Action<T>) определяет и реализует тип интерфейса (например, Action<T>.IInvoke). Большинство вещей, которые теперь принимают Action<T>, могут вместо этого принимать Action<T>.IInvoke, но другие классы также могут реализовать Action<T>.IInvoke. Этот подход может поддерживать ковариацию только в том случае, если это могут делать делегаты, но решение для этого - разрешить делегатам иметь параметры ковариантного типа. - person supercat; 04.01.2012
comment
@supercat: делегаты уже могут поддерживать общую дисперсию, так что ничего страшного. Это можно было бы сделать, но я не уверен, что это соответствует требованиям по выгоде / стоимости. - person Jon Skeet; 04.01.2012
comment
@JonSkeet: Делегаты на самом деле не поддерживают дисперсию; Я думаю, что Эрик Липперт писал об этом в своем блоге. Если у одной есть две переменные-делегаты типа Action<Control> и две типа Action<Button>, и всем им назначается метод подписи void f(Button p), все четыре назначения будут работать, но первые две переменные будут сравниваться не равными друг другу (также не равными два других, оба равны друг другу). Кроме того, компилятор разрешит присвоение переменных разных Action<T> типов, но не удастся во время выполнения. - person supercat; 05.01.2012
comment
@supercat: Универсальная дисперсия поддерживается для делегатов начиная с C # 4, поэтому она Action<in T> в .NET 4 и т. д. Раньше были доступны разные виды псевдоверсии. Назначения через преобразования групп методов, конечно, будут делать что-то другое, но если вы сначала создадите экземпляр делегата, вы можете назначить одну и ту же ссылку для обеих переменных, и значения будут равны. - person Jon Skeet; 05.01.2012
comment
@JonSkeet: Я не могу заставить это работать, и я не уверен, как это могло уступить способу реализации MultiCastDelegate. Насколько я понимаю, MulticastDelegate не может хранить фактические типы каких-либо делегатов, которые были объединены для его создания, поэтому, если бы Action<Button> и Action<Control> могли быть объединены, у системы не было бы возможности узнать, были ли первые создается как Action<Control> или как Action<Button>. Я что-то упускаю? - person supercat; 05.01.2012
comment
@supercat: объединение делегатов действительно создает проблему при использовании с дисперсией, но делегаты с одним действием работают нормально. Например: `Действие ‹object› x = o =› Console.WriteLine (o); Действие ‹string› y = x; Console.WriteLine (x == y); `- выводит True. (Однако для этого требуются C # 4 и .NET 4.) - person Jon Skeet; 05.01.2012
comment
@JonSkeet: В каких сценариях вы бы порекомендовали использовать интерфейсы вместо делегатов? - person Jon; 08.11.2013
comment
например, согласны ли вы с советом, данным в msdn.microsoft.com/en-us/library/ms173173%28v=vs.100%29.aspx? - person Jon; 08.11.2013
comment
@Jon: Это хорошие эмпирические правила, но я подозреваю, что есть ситуации, когда от них стоит отказаться. Я не могу сразу придумать какие-либо лучшие эмпирические правила. - person Jon Skeet; 08.11.2013

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

delegate void XYZ(int p);

interface IXyz {
    void doit(int p);
}

class One {
    // All four methods below can be used to implement the XYZ delegate
    void XYZ1(int p) {...}
    void XYZ2(int p) {...}
    void XYZ3(int p) {...}
    void XYZ4(int p) {...}
}

class Two : IXyz {
    public void doit(int p) {
        // Only this method could be used to call an implementation through an interface
    }
}
person Sergey Kalinichenko    schedule 01.01.2012

Из Когда использовать делегатов вместо интерфейсов (MSDN) < / а>:

И делегаты, и интерфейсы позволяют конструктору классов разделять объявления типов и реализацию. Данный интерфейс может быть унаследован и реализован любым классом или структурой. Делегат может быть создан для метода любого класса, если метод соответствует сигнатуре метода делегата. Ссылка на интерфейс или делегат могут использоваться объектом, который не знает класса, реализующего интерфейс или метод делегата. Учитывая это сходство, когда разработчику класса следует использовать делегат, а когда - интерфейс?

Используйте делегата в следующих случаях:

  • Используется шаблон проектирования событий.
  • Желательно инкапсулировать статический метод.
  • Вызывающей стороне нет необходимости обращаться к другим свойствам, методам или интерфейсам объекта, реализующего метод.
  • Желательна легкая композиция.
  • Классу может потребоваться более одной реализации метода.

Используйте интерфейс в следующих случаях:

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

Хорошим примером использования интерфейса с одним методом вместо делегата является IComparable или общая версия IComparable (Of T). IComparable объявляет метод CompareTo, который возвращает целое число, определяющее отношение меньше, равно или больше между двумя объектами одного типа. IComparable можно использовать как основу алгоритма сортировки. Хотя использование метода сравнения делегатов в качестве основы алгоритма сортировки было бы допустимым, это не идеально. Поскольку возможность сравнения принадлежит классу, а алгоритм сравнения не изменяется во время выполнения, интерфейс с одним методом является идеальным.

Из Делегаты против интерфейсов в C #:

Делегаты и интерфейсы - это две разные концепции в C #, но у них есть общая черта. И делегаты, и интерфейсы включают только объявление. Реализация осуществляется другим программным объектом.

person Lion    schedule 01.01.2012
comment
Полезный ответ; Если бы вы или кто-либо другой могли добавить примеры из ваших 2-5-го списка, в котором делегат может быть полезен, это тоже было бы здорово. Шаблон троеборья, я думаю, не нуждается в каких-либо примерах, поскольку он так широко обсуждается. - person Levin Magruder; 01.01.2012

Делегаты и интерфейсы - это два разных понятия в C #.

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

person aleroot    schedule 01.01.2012

Единственные реальные преимущества делегатов перед интерфейсами:

  1. Delegates could deal with different argument types even before .Net supported generics.
  2. A class can create delegates exposing multiple different methods sharing the same signature; replacing delegates with interfaces would mean that every class could expose one method that implemented each delegate-style interface without creating helper classes, but would need a helper class for each additional implementation.

Когда .net не поддерживал дженерики, делегаты были его неотъемлемой частью, поскольку объявление отдельных неуниверсальных интерфейсов для каждой отдельной сигнатуры функции, которую хотелось бы передать, было бы неработоспособным. Если бы .net с самого начала поддерживал дженерики, делегаты не были бы необходимы, за исключением определенных сценариев, связанных с отражением, и даже там, возможно, было бы наиболее полезно, чтобы тип Action<T,U> был реализацией IAction<T,U> (так что этот код, который просто нуждается в что-то, что он может Invoke, использовал бы интерфейс). Подход, основанный на интерфейсе, потребовал бы создания классов с одним методом в тех случаях, когда классам необходимо создавать делегаты, предоставляющие несколько методов, но устранил бы необходимость создания отдельных экземпляров делегата во многих распространенных случаях, когда количество методов одного данная подпись будет выставлена ​​именно одна.

Между прочим, замена делегатов интерфейсами никоим образом не помешает созданию универсального метода Combine. Действительно, ковариация интерфейсов могла бы сделать такой метод во многих отношениях лучше, чем существующий Delegate.Combine. Реализация метода, аналогичного Delegate.Remove, была бы в лучшем случае неудобной и раздражающей, но я не могу придумать никакой другой ситуации, кроме управления подпиской на события, которая потребовала бы использования Delegate.Remove, а подписку на события лучше всего обрабатывать с использованием других подходов в любом случае.

person supercat    schedule 03.01.2012