Делегат против интерфейсов - есть еще какие-то пояснения?


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 # эти события будут представлены в виде списков делегатов, скрытых ключевым словом «event» в c #, по одному для каждого отдельного события. Это означает, что вы можете легко подписаться на то, что вы хотите, не обременяя свой класс открытыми методами «Ключ» и т. Д.

+1 за баллы 3-5.

Дополнительно:

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

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

В Java это должно быть реализовано с использованием интерфейса. Часто нет естественного места для реализации такого интерфейса. Если класс содержит список, который он хочет каким-то образом преобразовать, то не очень естественно, что он реализует интерфейс «ListTransformer», тем более что может быть два разных списка, которые должны быть преобразованы по-разному.

Конечно, вы можете использовать анонимные классы, которые являются похожей концепцией (в Java).


Попробуйте отредактировать свой пост, чтобы действительно ответить на его вопросы
Ям Маркович,

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

Естественно. Бывает и со мной, и оно само по себе имеет свою ценность. Вот почему я только предложил это, но не жаловался на это. :)
Ям Маркович

3

1) Шаблоны событий, ну что ж, классика - это шаблон Observer, вот хорошая ссылка Microsoft

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

Ссылка выше говорит о делегатах и ​​о том, почему они хороши для композиции, что может помочь с вашим запросом.


3

Прежде всего, хороший вопрос. Я восхищаюсь вашим вниманием к полезности, а не слепо принимая «лучшие практики». +1 за это.

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

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

Прежде всего, я предполагаю, что вы уже знаете, что такое интерфейс. Что касается делегата, достаточно сказать, что это структура, содержащая типизированный указатель на метод вместе с необязательным указателем на объект, представляющий thisаргумент для этого метода. В случае статических методов последний указатель является нулевым.
Есть также многоадресные делегаты, которые похожи на делегаты, но им может быть назначено несколько таких структур (то есть один вызов Invoke для многоадресного делегата вызывает все методы в его назначенном списке вызовов).

Что они подразумевают под образцом событийного дизайна?

Они подразумевают использование событий в 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
    }
}

Теперь метод filter позволяет легко передавать только небольшой фрагмент логики, который не знает о внутренней организации коллекции, в то время как коллекция не зависит от логики, данной ему. Отлично. За исключением одной проблемы:
Коллекция действительно зависит от логики. Коллекция по своей сути предполагает, что переданная функция предназначена для проверки значения в соответствии с условием и возврата результата теста. Обратите внимание, что не все функции, которые принимают одно значение в качестве аргумента и возвращают логическое значение, на самом деле являются простыми предикатами. Например, метод удаления нашей коллекции является такой функцией.
Предположим, мы позвонили 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.