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

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

10 ответов


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


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

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

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

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


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

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

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. вы не используете public property but a public field (используя события, компилятор защищает ваши поля от нежелательного доступа)
  2. события не могут быть назначены напрямую. В этом случае это не приведет к предыдущей ошибке, которую я показал с переопределением поведения.
  3. никто вне вашего класса не может поднять событие.
  4. события могут быть включены в объявление интерфейса, тогда как поле не может

Примечания:

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

public delegate void EventHandler (object sender, EventArgs e)

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

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

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


Это старый пост, но если кто - то наткнется на него, как я-вот еще одна хорошая ссылка для ссылки.. http://csharpindepth.com/Articles/Chapter2/Events.aspx

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

"предположим, что события не существовали как понятие в C#/.Сеть. Как бы другой класс подписывается на событие?

три варианта:

  1. общественные delegatevariable

  2. делегировать переменную, поддерживаемую свойством

  3. делегировать переменную с помощью методов AddXXXHandler и RemoveXXXHandler

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

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

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


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


Примечание: Если у вас есть доступ к C# 5.0 Unleashed, прочитайте " ограничения на простое использование делегатов "в Главе 18 под названием" события", чтобы лучше понять различия между ними.


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

Пример 1: Использование public delegate

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

enter image description here

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

  • посредник.cs -- static класс содержит делегатов
  • форма form1.cs -- основная форма
  • DetailView.cs -- user control показывает все подробности

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

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.цезий. Здесь мы вызываем 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. переменная public delegate (это то, что мы только что сделали выше. не делай этого. я только что сказал вам выше, почему это плохо)
  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 определяющие события of ECMA-335 (CLI) разделы I-VI:

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

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

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

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


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

событие ссылка делегату с двумя ограничениями

  1. невозможно вызвать напрямую
  2. не могут быть присвоены значения напрямую (e.g eventObj = delegateMethod)

выше двух являются слабыми местами для делегатов, и он рассматривается в событии. Полный пример кода, чтобы показать разницу в fiddler здесь 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 позволяет назначить метод делегату, где тип параметра метода является базовым классом класса, который есть указывается в качестве параметра делегата.