Делегати срещу интерфейси в 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: Комбинирането на делегати наистина създава проблем, когато се използва с вариация - но делегатите с едно действие работят добре. Например: ` Action‹object› x = o =› Console.WriteLine(o); Действие‹низ› 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 поддържаше генерични кодове от самото начало, делегатите нямаше да са необходими, освен за определени сценарии, включващи Reflection, и дори там може би щеше да е най-полезно типът Action<T,U> да бъде реализация на IAction<T,U> (така че кодът, който просто се нуждае нещо, което може Invoke би използвало интерфейса). Подход, базиран на интерфейс, би изисквал създаването на класове с един метод в случаите, когато класовете трябва да създадат делегати, излагащи множество методи, но би премахнал необходимостта от създаване на отделни екземпляри на делегати в много често срещани случаи, когато броят на методите на даден подпис да бъде изложен е точно един.

Между другото, замяната на делегатите с интерфейси по никакъв начин няма да попречи на създаването на метод за комбиниране с общо предназначение. Наистина ковариацията на интерфейса може да направи такъв метод да работи по-добре от съществуващия Delegate.Combine в много отношения. Прилагането на метод, аналогичен на Delegate.Remove, би било в най-добрия случай тромаво и досадно, но не мога да се сетя за друга ситуация освен управлението на абонамент за събития, която да изисква използването на Delegate.Remove, а абонаментът за събития може да се управлява най-добре с други подходи така или иначе.

person supercat    schedule 03.01.2012