Делегат проти інтерфейсів - ще доступні роз'яснення?


23

Прочитавши статтю - Коли використовувати делегати замість інтерфейсів (Посібник з програмування C #) , мені потрібна допомога, щоб зрозуміти наведені нижче пункти, які, як мені здається, не такі зрозумілі (для мене). Будь-які приклади чи детальні пояснення для них доступні?

Використовуйте делегата, коли:

  • Використовується модель дизайну подій.
  • Бажано інкапсулювати статичний метод.
  • Бажана легка композиція.
  • Класу може знадобитися більше, ніж одна реалізація методу.

Використовуйте інтерфейс, коли:

  • Існує група споріднених методів, які можна назвати.
  • Класу потрібна лише одна реалізація методу.

Мої запитання:

  1. Що вони означають під шаблоном дизайну подій?
  2. Як склад може виявитися легким, якщо використовується делегат?
  3. якщо є група споріднених методів, яку можна викликати, то використовуйте інтерфейс - Яку користь вона має?
  4. якщо для класу потрібна лише одна реалізація методу, використовуйте інтерфейс - як це виправдано з точки зору переваг?

Відповіді:


11

Що вони означають під шаблоном дизайну подій?

Вони, швидше за все, посилаються на реалізацію шаблону спостерігача, який є основною мовною конструкцією в C #, викритим як " події ". Прослуховування подій можливо, підключивши до них делегата. Як зазначив Ям Маркович, EventHandlerце звичайний базовий тип делегата для подій, але будь-який тип делегата може бути використаний.

Як склад може виявитися легким, якщо використовується делегат?

Це, мабуть, просто стосується пропозиції гнучкості делегатів. Ви можете легко «скласти» певну поведінку. За допомогою лямбдів синтаксис робити це також дуже стисло. Розглянемо наступний приклад.

class Bunny
{
    Func<bool> _canHop;

    public Bunny( Func<bool> canHop )
    {
        _canHop = canHop;
    }

    public void Hop()
    {
        if ( _canHop() )  Console.WriteLine( "Hop!" );
    }
}

Bunny captiveBunny = new Bunny( () => IsBunnyReleased );
Bunny lazyBunny = new Bunny( () => !IsLazyDay );
Bunny captiveLazyBunny = new Bunny( () => IsBunnyReleased && !IsLazyDay );

Якщо ви робите щось подібне з інтерфейсами, вам потрібно використовувати або шаблон стратегії, або використовувати (абстрактний) базовий Bunnyклас, з якого ви поширюєте більш конкретні кролики.

якщо існує група споріднених методів, яку можна викликати, то використовуйте інтерфейс - Яку користь вона має?

Знову я використаю зайчиків, щоб продемонструвати, як було б легше.

interface IAnimal
{
    void Jump();
    void Eat();
    void Poo();
}

class Bunny : IAnimal { ... }
class Chick : IAnimal { ... }

// Using the interface.
IAnimal bunny = new Bunny();
bunny.Jump();  bunny.Eat();  bunny.Poo();
IAnimal chick = new Chick();
chick.Jump();  chick.Eat();  chick.Poo();

// Without the interface.
Action bunnyJump = () => bunny.Jump();
Action bunnyEat = () => bunny.Eat();
Action bunnyPoo = () => bunny.Poo();
bunnyJump(); bunnyEat(); bunnyPoo();
Action chickJump = () => chick.Jump();
Action chickEat = () => chick.Eat();
...

якщо для класу потрібна лише одна реалізація методу, використовуйте інтерфейс - як це виправдано з точки зору переваг?

Для цього розглянемо перший приклад із зайчиком ще раз. Якщо коли-небудь потрібна лише одна реалізація - не потрібна спеціальна композиція, - ви можете розкрити цю поведінку як інтерфейс. Вам ніколи не доведеться будувати лямбда, ви можете просто використовувати інтерфейс.

Висновок

Делегати пропонують набагато більшу гнучкість, тоді як інтерфейси допомагають вам встановлювати міцні контракти. Тому я вважаю останній згаданий пункт: "Класу може знадобитися більше, ніж одна реалізація методу". , безумовно, найбільш релевантний.

Додатковою причиною використання делегатів є те, що ви хочете викрити лише частину класу, з якої ви не можете налаштувати вихідний файл.

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


12

Делегат - це щось на зразок інтерфейсу для єдиного підпису методу, який не повинен бути явно реалізований, як звичайний інтерфейс. Ви можете сконструювати його на ходу.

Інтерфейс - це лише мовна структура, що представляє деякий контракт - "Цим я гарантую, що я надаю наступні методи та властивості".

Крім того, я не повністю згоден, що делегати в першу чергу корисні, коли вони використовуються як рішення для моделі спостерігача / підписника. Це, однак, є елегантним рішенням "проблеми багатомовності у Java".

Для ваших питань:

1 і 2)

Якщо ви хочете створити систему подій у Java, ви зазвичай використовуєте інтерфейс для розповсюдження події, наприклад:

interface KeyboardListener
{
    void KeyDown(int key);
    void KeyUp(int key)
    void KeyPress(int key);
    .... and so on
}

Це означає, що вашому класу доведеться явно реалізувати всі ці методи та надавати заглушки для всіх, навіть якщо ви просто хочете реалізувати KeyPress(int key).

У C # ці події будуть представлені у вигляді списків делегата, прихованих ключовим словом "подія" у c #, по одному для кожної поодинокої події. Це означає, що ви можете легко підписатися на те, що ви хочете, не обтяжуючи свій клас загальнодоступними методами "Ключ" тощо.

+1 за балами 3-5.

Додатково:

Делегати дуже корисні, коли ви хочете поставити, наприклад, функцію "map", яка бере список і проектує кожен елемент у новий список, з однаковою кількістю елементів, але певним чином відрізняється. По суті, IEnumerable.Select (...).

IEnumerable.Select приймає a Func<TSource, TDest>, який є делегатом, що обертає функцію, яка приймає елемент TSource та перетворює цей елемент у елемент TDest.

У Java це потрібно було б реалізувати за допомогою інтерфейсу. Часто немає природного місця для реалізації такого інтерфейсу. Якщо клас містить список, який він хоче певним чином перетворити, не зовсім природно, що він реалізує інтерфейс "ListTransformer", тим більше, що може бути два різних списки, які слід перетворити різними способами.

Звичайно, ви можете використовувати анонімні класи, які є схожим поняттям (в java).


Подумайте про редагування своєї публікації, щоб насправді відповісти на його запитання
Ям Маркович,

@YamMarcovic Ви маєте рацію, я трохи проскочив у вільній формі, а не безпосередньо відповідав на його запитання. Тим не менш, я думаю, це пояснює трохи про залучену механіку. Крім того, його питання були трохи менш добре сформовані, коли я відповідав на питання. : p
Макс

Природно. Так само трапляється і в мене, і вона сама по собі має свою цінність. Тому я лише запропонував це, але не скаржився на це. :)
Ям Маркович

3

1) Шаблони подій, добре, що класика - це модель Observer, ось хороша посилання Майкрософт Microsoft talk Observer

Стаття, з якою ви посилаєтесь, не дуже добре написана іммо і робить речі складнішими, ніж потрібно. Статичний бізнес - це здоровий глузд, ви не можете визначити інтерфейс зі статичними членами, тому, якщо ви хочете поліморфної поведінки статичного методу, ви використовуєте делегата.

Посилання вгорі розповідає про делегатів і про те, чому вони корисні для складання, щоб це могло допомогти вашому запиту.


3

Перш за все, гарне запитання. Я захоплююсь вашою увагою до корисності, а не сліпо приймаю "найкращі практики". +1 для цього.

Я читав цей посібник раніше. Вам потрібно щось запам’ятати - це лише посібник, головним чином для новоприбулих на C #, які вміють програмувати, але не так добре знайомі зі способом C #. Це не стільки сторінка правил, скільки сторінка, яка описує, як зазвичай це робиться. А оскільки вони вже роблять це так скрізь, може бути хорошою ідеєю залишатися послідовними.

Я перейду до справи, відповідаючи на ваші запитання.

Перш за все, я припускаю, що ви вже знаєте, що таке інтерфейс. Щодо делегата, то достатньо сказати, що це структура, що містить набраний покажчик на метод, а також необов'язковий вказівник на об'єкт, що представляє thisаргумент для цього методу. У разі статичних методів останній покажчик є нульовим.
Також є делегати для багатоадресної передачі, які подібно до делегатів, але можуть мати декілька цих структур, призначених їм (мається на увазі один виклик для виклику на делегат багатоадресної передачі виклику всіх методів у призначеному списку викликів).

Що вони означають під шаблоном дизайну подій?

Вони мають на увазі використання подій у C # (який має спеціальні ключові слова для витонченої реалізації цієї надзвичайно корисної схеми). Події в C # проводяться делегатами з декількох груп.

Коли ви визначаєте подію, наприклад, у цьому прикладі:

class MyClass {
  // Note: EventHandler is just a multicast delegate,
  // that returns void and accepts (object sender, EventArgs e)!
  public event EventHandler MyEvent;

  public void DoSomethingThatTriggersMyEvent() {
    // ... some code
    var handler = MyEvent;
    if (handler != null)
      handler(this, EventArgs.Empty);
    // ... some other code
  }
}

Компілятор фактично перетворює це в наступний код:

class MyClass {
  private EventHandler MyEvent = null;

  public void add_MyEvent(EventHandler value) {
    MyEvent += value;
  }

  public void remove_MyEvent(EventHandler value) {
    MyEvent -= value;
  }

  public void DoSomethingThatTriggersMyEvent() {
    // ... some code
    var handler = MyEvent;
    if (handler != null)
      handler(this, EventArgs.Empty);
    // ... some other code
  }
}

Потім ви підписуєтесь на подію, роблячи це

MyClass instance = new MyClass();
instance.MyEvent += SomeMethodInMyClass;

Який складається до

MyClass instance = new MyClass();
instance.add_MyEvent(new EventHandler(SomeMethodInMyClass));

Отже, це відбувається в C # (або .NET взагалі).

Як склад може виявитися легким, якщо використовується делегат?

Це легко продемонструвати:

Припустимо, у вас є клас, який залежить від набору дій, які слід йому передати. Ви можете інкапсулювати ці дії в інтерфейс:

interface RequiredMethods {
  void DoX();
  int DoY();
};

І кожен, хто хотів передати дії своєму класу, спочатку повинен реалізувати цей інтерфейс. Або ви могли б полегшити їхнє життя залежно від наступного класу:

sealed class RequiredMethods {
  public Action DoX;
  public Func<int> DoY();
}

Таким чином, абонентам потрібно лише створити екземпляр RequiredMethods і прив’язати методи до делегатів під час виконання. Це є , як правило , легше.

Такий спосіб робити надзвичайно вигідно при правильних обставинах. Подумайте над цим - навіщо залежати від інтерфейсу, коли все, що вам дійсно важливо, - це передача вам передачі?

Переваги використання інтерфейсів, коли є група споріднених методів

Корисно використовувати інтерфейси, оскільки інтерфейси зазвичай вимагають явних реалізацій часу компіляції. Це означає, що ви створюєте новий клас.
І якщо у вас є група споріднених методів в одному пакеті, вигідно, щоб цей пакет був повторно використаний іншими частинами коду. Тож якщо вони можуть просто інстанціювати клас замість того, щоб створити набір делегатів, це простіше.

Переваги використання інтерфейсів, якщо для класу потрібна лише одна реалізація

Як зазначалося раніше, інтерфейси реалізуються в час компіляції - це означає, що вони ефективніші, ніж виклик делегата (що таке рівень непрямості як такий).

"Одна реалізація" може означати реалізацію, яка існує єдиним чітко визначеним місцем.
Інакше реалізація може надходити з будь-якої точки програми, що, як правило, відповідає підпису методу. Це дозволяє досягти більшої гнучкості, оскільки методам потрібно лише відповідати очікуваному підпису, а не належати до класу, який явно реалізує певний інтерфейс. Але ця гнучкість може заробити і фактично порушує принцип заміни Ліскова , оскільки в більшості випадків ви хочете чіткості, оскільки це мінімізує шанс нещасних випадків. Так само, як статичне введення тексту.

Термін може також позначатись тут делегатами з кількома розсилками. Методи, оголошені інтерфейсами, можуть бути реалізовані лише один раз у класі реалізації. Але делегати можуть накопичувати кілька методів, які будуть називатися послідовно.

Отже, загалом, схоже, що довідник недостатньо інформативний, а просто функціонує як те, що він є - керівництвом, а не правилом. Деякі поради можуть насправді звучати трохи суперечливо. Ви самі вирішуєте, коли правильно застосовувати те, що. Посібник, здається, лише дає нам загальний шлях.

Я сподіваюся, що на ваші запитання відповіли на ваше задоволення. І знову кудо за питання.


2

Якщо моя пам'ять .NET все ще зберігається, делегат - це в основному функція ptr або функтор. Він додає шар непрямості до виклику функції, так що функції можуть бути замінені без викликового коду, що потрібно змінювати. Це те саме, що робить інтерфейс, за винятком того, що інтерфейс пакує кілька функцій разом, і реалізатор повинен реалізувати їх разом.

Шаблон подій, загалом кажучи, такий, де щось реагує на події з інших країн (наприклад, повідомлення Windows). Сукупність подій, як правило, відкрито закінчується, вони можуть наступати в будь-якому порядку і не обов'язково пов'язані один з одним. Для цього делегати добре працюють в тому, що кожна подія може викликати одну функцію, не потребуючи посилання на серію реалізуючих об'єктів, яка також може містити безліч невідповідних функцій. Окрім цього (саме тут мій .NET-пам’ять розпливчасте), я думаю, що до події можуть бути додані кілька делегатів.

Хоча композиція, хоча я не дуже знайомий з цим терміном, в основному полягає в тому, щоб створити один об'єкт, щоб мати декілька підрозділів або об'єднаних дітей, яким передається робота. Делегати дозволяють дітям змішуватись і узгоджуватись більш спеціально, коли інтерфейс може бути надмірним або спричиняти занадто багато з'єднань та жорсткості та крихкості.

Перевага інтерфейсу для пов'язаних методів полягає в тому, що методи можуть поділяти стан об'єкта, що реалізується. Функції делегування не можуть настільки чітко ділити або навіть містити стан.

Якщо класу потрібна одна реалізація, інтерфейс є більш підходящим, оскільки в будь-якому класі, що реалізує всю колекцію, можна виконати лише одну реалізацію, і ви отримаєте переваги класу, що реалізує (стан, інкапсуляція тощо). Якщо реалізація може змінитися через стан виконання, делегати працюють краще, оскільки їх можна замінити на інші реалізації, не впливаючи на інші методи. Наприклад, якщо є три делегати, кожен з яких має дві можливі реалізації, вам знадобиться вісім різних класів, що реалізують триметодний інтерфейс для обліку всіх можливих комбінацій станів.


1

Шаблон дизайну "події" (більш відомий як шаблон спостерігача) дозволяє приєднати до делегата кілька методів одного і того ж підпису. Ви не можете реально зробити це за допомогою інтерфейсу.

Я зовсім не впевнений, що композиція делегату легше, ніж інтерфейс. Це дуже дивне твердження. Цікаво, чи він має на увазі, тому що ви можете приєднати анонімні методи до делегата.


1

Найбільше уточнення, яке я можу запропонувати:

  1. Делегат визначає функцію підпису функції - які параметри відповідна функція буде приймати і яку вона повертатиме.
  2. Інтерфейс стосується цілого набору функцій, подій, властивостей та полів.

Так:

  1. Коли ви хочете узагальнити деякі функції з однаковою підписом - використовуйте делегат.
  2. Коли ви хочете узагальнити деяку поведінку чи якість класу - використовуйте інтерфейс.

1

Ваше запитання про події вже добре висвітлено. І також правда, що інтерфейс може визначати кілька методів (але насправді не потрібно), тоді як тип функції лише коли-небудь ставить обмеження для окремої функції.

Однак реальна різниця:

  • Значення функцій узгоджуються з типами функцій за допомогою структурного підтипу (тобто неявна сумісність з потрібною структурою). Це означає, що якщо значення функції має підпис, сумісний з типом функції, це дійсне значення для типу.
  • екземпляри підбираються до інтерфейсів номінальним підтипом (тобто явним використанням імені). Це означає, що якщо екземпляр є членом класу, який явно реалізує даний інтерфейс, екземпляр є дійсним значенням для інтерфейсу. Однак якщо в об'єкті є лише всі члени, затребувані інтерфейсами, і всі члени мають сумісні підписи, але інтерфейс явно не реалізує, це не є дійсним значенням для інтерфейсу.

Звичайно, але що це означає?

Візьмемо цей приклад (код знаходиться в haXe, оскільки мій C # не дуже хороший):

class Collection<T> {
    /* a lot of code we don't care about now */
    public function filter(predicate:T->Bool):Collection<T> { 
         //build a new collection with all elements e, such that predicate(e) == true
    }
    public function remove(e:T):Bool {
         //removes an element from this collection, if contained and returns true, false otherwise
    }
}

Тепер метод-фільтр дозволяє легко зручно проходити лише невеликий фрагмент логіки, який не знає про внутрішню організацію колекції, тоді як колекція не залежить від логіки, заданої їй. Чудово. За винятком однієї проблеми:
Колекція дійсно залежить від логіки. Колекція за своєю суттю робить припущення, що передана функція призначена для перевірки значення на умову та повернення успіху тесту. Зауважте, що не всі функції, які беруть одне значення як аргумент і повертають булеві, насправді є простими предикатами. Наприклад, метод видалення нашої колекції - така функція.
Припустимо, ми зателефонували c.filter(c.remove). Результатом стала б колекція з усіма елементами cчасуcсама стає порожньою. Це прикро, тому що, природно, ми могли б очікувати, що cвона буде інваріантною.

Приклад дуже побудований. Але ключова проблема полягає в тому, що виклик коду c.filterз деяким значенням функції в якості аргументу не може знати, чи підходить цей аргумент (тобто в кінцевому рахунку збереже інваріант). Код, який створив значення функції, може чи не може знати, що він буде інтерпретуватися як предикат.

Тепер давайте змінимо:

interface Predicate<T> {
    function test(value:T):Bool;
}
class Collection<T> {
    /* a lot of code we don't care about now */
    public function filter(predicate:Predicate<T>):Collection<T> { 
         //build a new collection with all elements e, such that predicate.test(e) == true
    }
    public function remove(e:T):Bool {
         //removes an element from this collection, if contained and returns true, false otherwise
    }
}

Що змінилося? Що змінилося, так це те, що будь-яка цінність тепер надається filter, явно підписала договір бути предикатом. Звичайно, злісні або надзвичайно дурні програмісти створюють реалізацію інтерфейсу, який не є побічним ефектом і, таким чином, не є предикатами.
Але більше не може статися те, що хтось пов’язує логічну одиницю даних / коду, яку помилково трактують як предикат через свою зовнішню структуру.

Отже, перефразовуючи сказане вище лише кількома словами:

  • інтерфейси означають використання номінального введення тексту і тим самим явні зв’язки
  • делегати мають на увазі використання структурного типу та тим самим неявних зв’язків

Перевага явних відносин полягає в тому, що ви можете бути певними в них. Недоліком є ​​те, що вони вимагають накладних витрат чіткості. І навпаки, недоліком неявних зв’язків (у нашому випадку підпис однієї функції, що відповідає бажаному підпису), полягає в тому, що ви дійсно не можете бути на 100% впевнені, що можете використовувати речі таким чином. Перевага полягає в тому, що ви можете встановити відносини без усіх накладних витрат. Можна просто швидко зібрати речі разом, оскільки їх структура дозволяє. Ось що означає легка композиція.
Це трохи схоже на LEGO: ви можете просто підключити фігуру LEGO до зірок на піратському кораблі LEGO, просто тому, що це дозволяє зовнішня структура. Тепер ви можете відчути, що це страшенно неправильно, або це може бути саме те, що ви хочете. Ніхто не збирається вас зупинити.


1
Варто зазначити, що "явно реалізує інтерфейс" (під "номінальним підтипом") не те саме, що явна чи неявна реалізація членів інтерфейсу. У C #, класи завжди повинні чітко оголошувати інтерфейси, які вони реалізують, як ви говорите. Але члени інтерфейсу можуть бути реалізовані або явно (наприклад, int IList.Count { get { ... } }), або неявно ( public int Count { get { ... } }). Ця відмінність не стосується цієї дискусії, але вона заслуговує на згадку, щоб не бентежити читачів.
фог

@phoog: Так, дякую за поправку. Я навіть не знав, що перша можлива. Але так, спосіб того, як мова насправді здійснює реалізацію інтерфейсу, дуже різниться. Наприклад, у Objective-C він видасть вам попередження лише у тому випадку, якщо воно не буде реалізовано.
back2dos

1
  1. Шаблон дизайну подій передбачає структуру, яка включає механізм публікації та підписки. Події публікує джерело, кожен абонент отримує власну копію опублікованого товару та несе відповідальність за власні дії. Це дуже зв'язаний механізм, оскільки видавець навіть не повинен знати, що підписники є. Не кажучи вже про те, що передплатники не є обов'язковими (це може бути ніхто)
  2. композиція "легша", якщо використовується делегат порівняно з інтерфейсом. Інтерфейси визначають екземпляр класу як об'єкт "свого роду обробника", коли, як виглядає, екземпляр конструктивної композиції "має обробник", тому зовнішній вигляд екземпляра менш обмежений використанням делегата і стає більш гнучким.
  3. З коментаря нижче я виявив, що мені потрібно покращити свою публікацію, дивіться це для довідки: http://bytes.com/topic/c-sharp/answers/252309-interface-vs-delegate

1
3. Чому делегатам потрібно більше кастингу, і чому більш жорстке з'єднання є вигодою? 4. Вам не потрібно використовувати події для використання делегатів, а з делегатами це просто як проведення методу - ви знаєте, що це тип повернення та типи аргументів. Крім того, чому метод, оголошений в інтерфейсі, полегшує обробку помилок, ніж метод, приєднаний до делегата?
Ям Маркович

У випадку лише специфікації делегата ви праві. Однак у випадку обробки подій (принаймні, до цього моє посилання стосується) вам завжди потрібно успадкувати форму якогось eventArgs, щоб діяти як контейнер для користувацьких типів, тож замість того, щоб безпечно перевіряти значення, ви завжди доведеться розгортати тип перед тим, як обробляти значення.
Карло Куйп

Що стосується причини, чому я думаю, що метод, визначений в інтерфейсі, полегшить обробку помилок, - це те, що компілятор може перевірити типи винятків, викинуті з цього методу. Я знаю, що це не переконливі докази, але, можливо, це слід вказати як перевагу?
Карло Куйп

1
По-перше, ми говорили про делегати проти інтерфейсів, а не про події проти інтерфейсів. По-друге, зробити базу події самою делегатом EventHandler - це лише умова, і аж ніяк не обов'язкова. Але знову ж таки, якщо говорити про події тут, то це не так. Що стосується вашого другого коментаря - винятки не перевіряються в C #. Ви плутаєтесь з Java (яка, до речі, навіть не має делегатів).
Ям Маркович

Знайдено тему з цього питання, що пояснює важливі відмінності: bytes.com/topic/c-sharp/answers/252309-interface-vs-delegate
Карло Куйп
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.