Каковы различия между делегатами и событиями?

StackOverflow https://stackoverflow.com/questions/29155

  •  09-06-2019
  •  | 
  •  

Вопрос

В чем разница между делегатами и событиями?Разве оба не содержат ссылки на функции, которые могут быть выполнены?

Это было полезно?

Решение

Ан Событие декларация добавляет уровень абстракции и защиты делегат пример.Эта защита не позволяет клиентам делегата сбросить делегат и его список вызовов и позволяет только добавлять или удалять целевые объекты из списка вызовов.

Другие советы

Помимо синтаксических и эксплуатационных свойств, существует еще и семантическая разница.

Концептуально делегаты — это шаблоны функций;то есть они выражают контракт, которого должна придерживаться функция, чтобы считаться «типом» делегата.

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

Даже если бы они были абсолютно одинаковыми (синтаксически и в IL-коде), все равно останется семантическая разница.В общем, я предпочитаю иметь два разных имени для двух разных концепций, даже если они реализованы одинаково (что не означает, что мне нравится иметь один и тот же код дважды).

Чтобы понять различия, вы можете посмотреть на эти 2 примера.

Пример с делегатами (в данном случае Action — это своего рода делегат, который не возвращает значение)

public class Animal
{
    public Action Run {get; set;}

    public void RaiseEvent()
    {
        if (Run != null)
        {
            Run();
        }
    }
}

Чтобы использовать делегата, вам следует сделать что-то вроде этого:

Animal animal= new Animal();
animal.Run += () => Console.WriteLine("I'm running");
animal.Run += () => Console.WriteLine("I'm still running") ;
animal.RaiseEvent();

Этот код работает хорошо, но у вас могут быть некоторые слабые места.

Например, если я напишу это:

animal.Run += () => Console.WriteLine("I'm running");
animal.Run += () => Console.WriteLine("I'm still running");
animal.Run = () => Console.WriteLine("I'm sleeping") ;

в последней строке кода я переопределил предыдущее поведение, только одна пропущена + (Я использовал = вместо +=)

Еще одним слабым местом является то, что каждый класс, использующий ваш Animal класс может поднять RaiseEvent просто звоню animal.RaiseEvent().

Чтобы избежать этих слабых мест, вы можете использовать events в С#.

Ваш класс Animal изменится следующим образом:

public class ArgsSpecial : EventArgs
{
    public ArgsSpecial (string val)
    {
        Operation=val;
    }

    public string Operation {get; set;}
} 

public class Animal
{
    // Empty delegate. In this way you are sure that value is always != null 
    // because no one outside of the class can change it.
    public event EventHandler<ArgsSpecial> Run = delegate{} 

    public void RaiseEvent()
    {  
         Run(this, new ArgsSpecial("Run faster"));
    }
}

вызывать события

 Animal animal= new Animal();
 animal.Run += (sender, e) => Console.WriteLine("I'm running. My value is {0}", e.Operation);
 animal.RaiseEvent();

Отличия:

  1. Вы используете не общедоступное свойство, а общедоступное поле (с помощью событий компилятор защищает ваши поля от нежелательного доступа)
  2. События не могут быть назначены напрямую.В этом случае это не приведет к предыдущей ошибке, которую я показал при переопределении поведения.
  3. Никто за пределами вашего класса не может инициировать это событие.
  4. События могут быть включены в объявление интерфейса, тогда как поле не может быть включено в объявление интерфейса.

Примечания:

EventHandler объявлен как следующий делегат:

public delegate void EventHandler (object sender, EventArgs e)

он принимает отправителя (типа объекта) и аргументы события.Отправитель имеет значение null, если он исходит от статических методов.

Этот пример, в котором используется EventHandler<ArgsSpecial>, также можно записать с помощью EventHandler вместо.

Ссылаться здесь для документации о EventHandler

Вот еще одна хорошая ссылка для ссылки.http://csharpeepeepep.com/Articles/Chapter2/Events.aspx

Вкратце, вывод из статьи: события — это инкапсуляция делегатов.

Цитата из статьи:

Предположим, что события не существуют как концепция в C#/.NET.Как другой класс может подписаться на событие?Три варианта:

  1. Публичная переменная делегата

  2. Переменная делегата, поддерживаемая свойством

  3. Переменная-делегат с методами AddXXXHandler и RemoveXXXHandler.

Вариант 1 явно ужасен, по всем обычным причинам, по которым мы ненавидим общедоступные переменные.

Вариант 2 немного лучше, но позволяет подписчикам эффективно переопределять друг друга — было бы слишком просто написать someInstance.MyEvent = eventHandler;который заменит любые существующие обработчики событий, а не будет добавлять новый.Кроме того, вам еще нужно написать свойства.

Вариант 3 — это, по сути, то, что дают вам события, но с гарантированным соглашением (сгенерированным компилятором и подкрепленным дополнительными флагами в IL) и «бесплатной» реализацией, если вас устраивает семантика, которую дают вам события, подобные полям.Подписка на события и отписка от них инкапсулируются без разрешения произвольного доступа к списку обработчиков событий, а языки могут упростить задачу, предоставляя синтаксис как для объявления, так и для подписки.

ПРИМЕЧАНИЕ:Если у вас есть доступ к C# 5.0 выпущен на свободу, прочитайте «Ограничения на простое использование делегатов» в главе 18 под названием «События», чтобы лучше понять различия между ними.


Мне всегда помогает простой и конкретный пример.Итак, вот один для сообщества.Сначала я покажу, как можно использовать только делегаты, чтобы делать то, что для нас делают события.Затем я покажу, как то же решение будет работать с экземпляром EventHandler.А затем я объясняю, почему мы НЕ хотим делать то, что я объясняю в первом примере.Этот пост был вдохновлен статья Джон Скит.

Пример 1:Использование публичного делегата

Предположим, у меня есть приложение WinForms с одним раскрывающимся списком.Раскрывающийся список привязан к List<Person>.Где Person имеет свойства Id, Name, NickName, HairColor.В главной форме находится настраиваемый пользовательский элемент управления, который показывает свойства этого человека.Когда кто-то выбирает человека в раскрывающемся списке, метки в пользовательском элементе управления обновляются и отображают свойства выбранного человека.

enter image description here

Вот как это работает.У нас есть три файла, которые помогут нам собрать это воедино:

  • Mediator.cs — статический класс содержит делегатов
  • Form1.cs -- основная форма
  • DetailView.cs — пользовательский элемент управления показывает все детали.

Вот соответствующий код для каждого из классов:

class Mediator
{
    public delegate void PersonChangedDelegate(Person p); //delegate type definition
    public static PersonChangedDelegate PersonChangedDel; //delegate instance. Detail view will "subscribe" to this.
    public static void OnPersonChanged(Person p) //Form1 will call this when the drop-down changes.
    {
        if (PersonChangedDel != null)
        {
            PersonChangedDel(p);
        }
    }
}

Вот наш пользовательский элемент управления:

public partial class DetailView : UserControl
{
    public DetailView()
    {
        InitializeComponent();
        Mediator.PersonChangedDel += DetailView_PersonChanged;
    }

    void DetailView_PersonChanged(Person p)
    {
        BindData(p);
    }

    public void BindData(Person p)
    {
        lblPersonHairColor.Text = p.HairColor;
        lblPersonId.Text = p.IdPerson.ToString();
        lblPersonName.Text = p.Name;
        lblPersonNickName.Text = p.NickName;

    }
}

Наконец, у нас есть следующий код в нашем Form1.cs.Здесь мы вызываем OnPersonChanged, который вызывает любой код, подписанный на делегата.

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
    Mediator.OnPersonChanged((Person)comboBox1.SelectedItem); //Call the mediator's OnPersonChanged method. This will in turn call all the methods assigned (i.e. subscribed to) to the delegate -- in this case `DetailView_PersonChanged`.
}

Хорошо.Вот как вы это заработаете без использования событий и просто используя делегатов.Мы просто помещаем публичный делегат в класс — вы можете сделать его статическим, одноэлементным или каким угодно.Большой.

НО, НО, НО, мы не хотим делать то, что я только что описал выше.Потому что общественные поля — это плохо по многим, многим причинам.Итак, какие у нас есть варианты?Как описывает Джон Скит, вот наши варианты:

  1. Публичная переменная-делегат (это то, что мы только что сделали выше.не делай этого.я выше сказал, почему это плохо)
  2. Поместите делегат в свойство с помощью get/set (проблема здесь в том, что подписчики могут переопределить друг друга - поэтому мы могли бы подписаться на делегат несколько методов, а затем мы могли бы случайно сказать PersonChangedDel = null, уничтожив все остальные подписки.Другая проблема, которая здесь остается, заключается в том, что, поскольку пользователи имеют доступ к делегату, они могут вызывать цели в списке вызовов — мы не хотим, чтобы внешние пользователи имели доступ к тому, когда вызывать наши события.
  3. Переменная-делегат с методами AddXXXHandler и RemoveXXXHandler.

Этот третий вариант, по сути, и есть то, что дает нам событие.Когда мы объявляем EventHandler, он дает нам доступ к делегату — не публично, не как свойство, а как то, что мы называем событием, которое просто добавляет/удаляет средства доступа.

Давайте посмотрим, как выглядит та же программа, но теперь вместо публичного делегата используется событие (я также изменил наш Медиатор на одноэлементный):

Пример 2:С EventHandler вместо публичного делегата

Посредник:

class Mediator
{

    private static readonly Mediator _Instance = new Mediator();

    private Mediator() { }

    public static Mediator GetInstance()
    {
        return _Instance;
    }

    public event EventHandler<PersonChangedEventArgs> PersonChanged; //this is just a property we expose to add items to the delegate.

    public void OnPersonChanged(object sender, Person p)
    {
        var personChangedDelegate = PersonChanged as EventHandler<PersonChangedEventArgs>;
        if (personChangedDelegate != null)
        {
            personChangedDelegate(sender, new PersonChangedEventArgs() { Person = p });
        }
    }
}

Обратите внимание: если вы нажмете F12 на EventHandler, вы увидите, что определение представляет собой просто универсальный делегат с дополнительным объектом «отправитель»:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

Пользовательский контроль:

public partial class DetailView : UserControl
{
    public DetailView()
    {
        InitializeComponent();
        Mediator.GetInstance().PersonChanged += DetailView_PersonChanged;
    }

    void DetailView_PersonChanged(object sender, PersonChangedEventArgs e)
    {
        BindData(e.Person);
    }

    public void BindData(Person p)
    {
        lblPersonHairColor.Text = p.HairColor;
        lblPersonId.Text = p.IdPerson.ToString();
        lblPersonName.Text = p.Name;
        lblPersonNickName.Text = p.NickName;

    }
}

Наконец, вот код Form1.cs:

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
        Mediator.GetInstance().OnPersonChanged(this, (Person)comboBox1.SelectedItem);
}

Поскольку EventHandler требует, чтобы EventArgs был параметром, я создал этот класс только с одним свойством:

class PersonChangedEventArgs
{
    public Person Person { get; set; }
}

Надеюсь, это немного покажет вам, почему у нас есть события и чем они отличаются (но функционально такие же) от делегатов.

Вы также можете использовать события в объявлениях интерфейса, но не для делегатов.

Какое великое недопонимание между событиями и делегатами!!!Делегат указывает ТИП (например, class, или interface делает), тогда как событие — это просто своего рода ЧЛЕН (например, поля, свойства и т. д.).И, как и у любого другого элемента, у события также есть тип.Однако в случае события тип события должен быть указан делегатом.Например, вы НЕ МОЖЕТЕ объявить событие типа, определенного интерфейсом.

Подводя итоги, мы можем сделать следующее Наблюдение:тип события ДОЛЖЕН быть определен делегатом.Это основная связь между событием и делегатом, описанная в разделе II.18 Определение событий из ECMA-335 (CLI) Разделы с I по VI:

При обычном использовании TypeSpec (если присутствует) идентифицирует делегата чья подпись соответствует аргументам, передаваемым в метод Fire события.

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

Событие в .net — это назначенная комбинация методов Add и Remove, оба из которых ожидают делегата определенного типа.И C#, и vb.net могут автоматически генерировать код для методов добавления и удаления, который будет определять делегат для хранения подписок на события, а также добавлять или удалять переданные делегаты в/из этого делегата подписки.VB.net также автоматически сгенерирует код (с помощью оператора RaiseEvent) для вызова списка подписки тогда и только тогда, когда он не пуст;по какой-то причине C# не генерирует последнее.

Обратите внимание: хотя управление подписками на события обычно осуществляется с помощью делегата многоадресной рассылки, это не единственный способ сделать это.С общедоступной точки зрения потенциальный подписчик событий должен знать, как сообщить объекту, что он хочет получать события, но ему не обязательно знать, какой механизм издатель будет использовать для вызова событий.Также обратите внимание, что, хотя тот, кто определил структуру данных событий в .net, очевидно, считал, что должны быть общедоступные средства их создания, ни C#, ни vb.net не используют эту функцию.

Чтобы определить событие простым способом:

Событие представляет собой ССЫЛКА делегату с двумя ограничениями

  1. Невозможно вызвать напрямую
  2. Невозможно присвоить значения напрямую (например, eventObj = DelegateMethod).

Вышеуказанные два являются слабыми местами для делегатов, и они будут устранены в ходе мероприятия.Полный пример кода, показывающий разницу в скрипаче, находится здесь. https://dotnetfiddle.net/5iR3fB .

Переключите комментарий между событием и делегатом и клиентским кодом, который вызывает/назначает значения делегату, чтобы понять разницу.

Вот встроенный код.

 /*
This is working program in Visual Studio.  It is not running in fiddler because of infinite loop in code.
This code demonstrates the difference between event and delegate
        Event is an delegate reference with two restrictions for increased protection

            1. Cannot be invoked directly
            2. Cannot assign value to delegate reference directly

Toggle between Event vs Delegate in the code by commenting/un commenting the relevant lines
*/

public class RoomTemperatureController
{
    private int _roomTemperature = 25;//Default/Starting room Temperature
    private bool _isAirConditionTurnedOn = false;//Default AC is Off
    private bool _isHeatTurnedOn = false;//Default Heat is Off
    private bool _tempSimulator = false;
    public  delegate void OnRoomTemperatureChange(int roomTemperature); //OnRoomTemperatureChange is a type of Delegate (Check next line for proof)
    // public  OnRoomTemperatureChange WhenRoomTemperatureChange;// { get; set; }//Exposing the delegate to outside world, cannot directly expose the delegate (line above), 
    public  event OnRoomTemperatureChange WhenRoomTemperatureChange;// { get; set; }//Exposing the delegate to outside world, cannot directly expose the delegate (line above), 

    public RoomTemperatureController()
    {
        WhenRoomTemperatureChange += InternalRoomTemperatuerHandler;
    }
    private void InternalRoomTemperatuerHandler(int roomTemp)
    {
        System.Console.WriteLine("Internal Room Temperature Handler - Mandatory to handle/ Should not be removed by external consumer of ths class: Note, if it is delegate this can be removed, if event cannot be removed");
    }

    //User cannot directly asign values to delegate (e.g. roomTempControllerObj.OnRoomTemperatureChange = delegateMethod (System will throw error)
    public bool TurnRoomTeperatureSimulator
    {
        set
        {
            _tempSimulator = value;
            if (value)
            {
                SimulateRoomTemperature(); //Turn on Simulator              
            }
        }
        get { return _tempSimulator; }
    }
    public void TurnAirCondition(bool val)
    {
        _isAirConditionTurnedOn = val;
        _isHeatTurnedOn = !val;//Binary switch If Heat is ON - AC will turned off automatically (binary)
        System.Console.WriteLine("Aircondition :" + _isAirConditionTurnedOn);
        System.Console.WriteLine("Heat :" + _isHeatTurnedOn);

    }
    public void TurnHeat(bool val)
    {
        _isHeatTurnedOn = val;
        _isAirConditionTurnedOn = !val;//Binary switch If Heat is ON - AC will turned off automatically (binary)
        System.Console.WriteLine("Aircondition :" + _isAirConditionTurnedOn);
        System.Console.WriteLine("Heat :" + _isHeatTurnedOn);

    }

    public async void SimulateRoomTemperature()
    {
        while (_tempSimulator)
        {
            if (_isAirConditionTurnedOn)
                _roomTemperature--;//Decrease Room Temperature if AC is turned On
            if (_isHeatTurnedOn)
                _roomTemperature++;//Decrease Room Temperature if AC is turned On
            System.Console.WriteLine("Temperature :" + _roomTemperature);
            if (WhenRoomTemperatureChange != null)
                WhenRoomTemperatureChange(_roomTemperature);
            System.Threading.Thread.Sleep(500);//Every second Temperature changes based on AC/Heat Status
        }
    }

}

public class MySweetHome
{
    RoomTemperatureController roomController = null;
    public MySweetHome()
    {
        roomController = new RoomTemperatureController();
        roomController.WhenRoomTemperatureChange += TurnHeatOrACBasedOnTemp;
        //roomController.WhenRoomTemperatureChange = null; //Setting NULL to delegate reference is possible where as for Event it is not possible.
        //roomController.WhenRoomTemperatureChange.DynamicInvoke();//Dynamic Invoke is possible for Delgate and not possible with Event
        roomController.SimulateRoomTemperature();
        System.Threading.Thread.Sleep(5000);
        roomController.TurnAirCondition (true);
        roomController.TurnRoomTeperatureSimulator = true;

    }
    public void TurnHeatOrACBasedOnTemp(int temp)
    {
        if (temp >= 30)
            roomController.TurnAirCondition(true);
        if (temp <= 15)
            roomController.TurnHeat(true);

    }
    public static void Main(string []args)
    {
        MySweetHome home = new MySweetHome();
    }


}

Covariance и Contravariance обеспечить дополнительную гибкость объектам делегата.С другой стороны, событие не имеет таких понятий.

  • Covariance Позволяет присвоить метод делегату, где возвращаемый тип метода - это класс, который получен из класса, который определяет тип возврата делегата.
  • Contravariance Позволяет назначить метод делегату, где тип параметра метода является базовым классом класса, который указан как параметр делегата.

Делегат — это типобезопасный указатель на функцию.Событие — это реализация шаблона проектирования «издатель-подписчик» с использованием делегата.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top