Каковы различия между делегатами и событиями? - PullRequest
286 голосов
/ 27 августа 2008

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

Ответы [ 11 ]

259 голосов
/ 27 августа 2008

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

92 голосов
/ 12 июля 2014

Чтобы понять различия, вы можете взглянуть на эти 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 в c #.

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

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)

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

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

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

92 голосов
/ 27 августа 2008

В дополнение к синтаксическим и эксплуатационным свойствам, есть также семантическая разница.

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

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

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

34 голосов
/ 12 апреля 2013

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

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

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

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

  1. Открытая переменная-делегат

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

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

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

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

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

7 голосов
/ 13 апреля 2015

ПРИМЕЧАНИЕ. Если у вас есть доступ к C # 5.0 Unleashed , прочитайте раздел «Ограничения простого использования делегатов» в главе 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`.
}

Ok. Так вот, как бы вы работали без использования событий и только с использованием делегатов . Мы просто помещаем открытый делегат в класс - вы можете сделать его статическим или одноэлементным, или как угодно. Отлично.

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

  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; }
}

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

6 голосов
/ 18 февраля 2015

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

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

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

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

6 голосов
/ 25 августа 2010

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

4 голосов
/ 27 августа 2011

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

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

3 голосов
/ 13 сентября 2016

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

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

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

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

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

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

 /*
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();
    }


}
0 голосов
/ 14 января 2019

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

...