Які відмінності між делегатами та подіями?


317

Які відмінності між делегатами та подіями? Не мають обох посилань на функції, які можна виконати?



2
це пояснює, наприклад, подивіться юнигеек.com/delegates-events-unity
Рахул Лаліт

Відповіді:


283

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


44
Звичайно, цей захисний шар також заважає "клієнтам" (коду за межами визначаючого класу / структури) викликати делегата та будь-яким чином отримувати об'єкт делегата "позаду" події.
Джеппе Стіг Нільсен

7
Не зовсім правда. Ви можете оголосити подію без екземпляра делегата бекенда. У c # ви можете явно реалізувати подію та використовувати іншу структуру даних, що надаються за вашим вибором.
Мігель Гамбоа

3
@mmcdole можете надати приклад, щоб пояснити його?
vivek nuna

103

Щоб зрозуміти відмінності, ви можете переглянути ці 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().

Щоб уникнути цих слабких місць, ви можете використовувати eventsc #.

Ваш клас 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)

він приймає відправника (типу Object) та аргументи події. Відправник недійсний, якщо він походить із статичних методів.

Цей приклад, який використовує EventHandler<ArgsSpecial>, також можна записати, використовуючи EventHandlerнатомість.

Зверніться тут для документації про EventHandler


7
Все виглядало чудово, поки я не зіткнувся з "Ніхто поза вашим класом не може підняти подію". Що це означає? Не може хтось зателефонувати RaiseEvent, якщо метод виклику має доступ до екземпляра animalкоду, який використовує подію?
dance2die

11
@Sung Events можна підняти лише зсередини класу, можливо, мені це було не зовсім зрозуміло. За допомогою подій ви можете викликати функцію, яка викликає подію (інкапсуляція), але її можна підняти лише зсередини класу, що визначає її. Дайте мені знати, якщо я не зрозуміла.
faby

1
"Події не можна призначити безпосередньо." Якщо я не зрозумів вас неправильно, це неправда. Ось приклад: gist.github.com/Chiel92/36bb3a2d2ac7dd511b96
Chiel ten Brinke

2
@faby, Ви маєте на увазі, що, хоча подія оголошена публічною, я все одно не можу зробити це animal.Run(this, new ArgsSpecial("Run faster");?
Пап

1
@ChieltenBrinke Звичайно, подію можна призначити членам класу ... але не інакше.
Джим Балтер

94

Окрім синтаксичних та експлуатаційних властивостей, є ще й семантична різниця.

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

Події представляють ... ну, події. Вони покликані попередити когось, коли щось трапиться, і так, вони дотримуються делегатського визначення, але вони не те саме.

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


8
Відмінний опис Делегатів.
Сампсон

1
Тож чи можна сказати, що подія є "особливим" типом делегата?
Пап

Я не розумію вашу думку. Ви можете скористатися делегатом, щоб "попередити когось, коли щось трапиться". Можливо, ви б цього не зробили, але ви можете, отже, це не властива подія.
steve

@ Джордж Кордоба прикладом делегата та делегата подій є власником газети та подіями (передплатити або скасувати підписку), а деякі люди купують газету, а деякі не купують газету означає, що власник газети не може змусити кожного купувати газету мою думку правильно чи неправильно?
Rahul_Patil

37

Ось ще одне хороше посилання на яке можна посилатися. http://csharpindepth.com/Articles/Chapter2/Events.aspx

Коротко кажучи, вилучення із статті - Події - це інкапсуляція над делегатами.

Цитата зі статті:

Припустимо, події не існували як концепція в C # /. NET. Як інший клас підписався на подію? Три варіанти:

  1. Публічна змінна делегата

  2. Змінна делегата, яку підтримує властивість

  3. Змінна делегата методами AddXXXHandler та RemoveXXXHandler

Варіант 1 явно жахливий, тому що ми з усіх причин нормально прикриваємося загальнодоступними змінними.

Варіант 2 трохи кращий, але дозволяє абонентам ефективно перекривати один одного - було б надто просто написати someInstance.MyEvent = eventHandler; що замінить будь-які наявні обробники подій, а не додавання нового. Крім того, вам ще потрібно записати властивості.

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


Приємне і стисле пояснення. Thanx
Pap

Це скоріше теоретичне занепокоєння, ніж будь-що, але FWIW я завжди відчував, що "Варіант 1 поганий, тому що нам не подобаються публічні змінні" аргумент може використати трохи більше роз'яснення. Якщо він говорить , що , тому що це «погано OOP практик», технічноpublic Delegate змінний буде викриття «даних», але в міру моєї ООП знання ніколи не згадував які - або понять зовсім подібно Delegate(це ні «об'єкт» , ні «повідомлення») , і .NET насправді ледве не обробляє делегатів, як дані.
jrh

Хоча я також хотів би дати більше практичних порад, якщо ви опинилися в ситуації, коли ви хочете переконатися, що існує лише один обробник, то виготовлення власних AddXXXHandlerметодів зі private Delegateзмінною може бути хорошим варіантом. У цьому випадку ви можете перевірити, чи обробник вже встановлений, і реагувати належним чином. Це також може бути хорошим налаштуванням, якщо вам потрібен об'єкт, у якому знаходиться, Delegateщоб мати змогу очистити всі обробники ( eventне дає вам ніякого способу зробити це).
jrh

7

ПРИМІТКА. Якщо у вас є доступ до C # 5.0 Unleashed , прочитайте "Обмеження щодо простого використання делегатів" у главі 18 під заголовком "Події", щоб краще зрозуміти відмінності між ними.


Мені завжди допомагає простий, конкретний приклад. Отже ось один для громади. Спочатку я показую, як ви можете самостійно використовувати делегатів, щоб робити те, що події роблять для нас. Потім я показую, як те саме рішення буде працювати з екземпляром EventHandler. А потім я пояснюю, чому ми НЕ хочемо робити те, що я пояснюю в першому прикладі. Цей пост був натхненний статтею Джона Скіта.

Приклад 1: Використання публічного делегата

Припустимо, у мене є програма WinForms з одним спадною панеллю. Випадаюче меню прив’язане до List<Person>. Де Особа має властивості Id, Імені, Псевдоніма, HairColor. У головній формі - користувацьке управління користувачем, яке показує властивості цієї людини. Коли хтось вибирає людину у спадному меню на оновленнях керування користувача, щоб відобразити властивості вибраної людини.

введіть тут опис зображення

Ось як це працює. У нас є три файли, які допомагають нам скласти це разом:

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

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


Хоча я ціную всю хорошу роботу в цій публікації і мені сподобалося, читаючи її більшість, я все ще відчуваю, що одне питання не вирішене - The other problem that remains here is that since the users have access to the delegate, they can invoke the targets in the invocation list -- we don't want external users having access to when to raise our events. В останній версії програми Mediatorви все одно можете телефонувати OnPersonChangeкожен раз, коли маєте посилання на одиночку. Можливо, вам слід зазначити, що такий Mediatorпідхід не заважає такої поведінки, і ближче до шини подій.
Івайло Славов

6

Ви також можете використовувати події в інтерфейсних деклараціях, не так для делегатів.


2
Інтерфейс @surfen може містити події, але не делегатів.
Олександр Нікітін

1
Що саме ти маєш на увазі? Ви можете мати Action a { get; set; }всередині інтерфейсу визначення.
Chiel ten Brinke

6

Яке велике непорозуміння між подіями та делегатами !!! Делегат вказує TYPE (наприклад, a classабо an interface), тоді як подія є лише своєрідною ЧЛЕНОЮ (наприклад, полями, властивостями тощо). І, як і будь-який інший тип учасника, подія також має тип. Однак у випадку події тип події повинен бути визначений делегатом. Наприклад, ви не можете оголосити подію типу, визначеного інтерфейсом.

Завершуючи, ми можемо зробити таке Спостереження: тип події ОБОВ'ЯЗКОВО визначається делегатом . Це основне співвідношення між подією і делегатом і описано в розділі II.18 визначення подій з ECMA-335 (CLI) Перегородки I до VI :

За типового використання TypeSpec (за наявності) ідентифікує делегата , підпис якого відповідає аргументам, переданим методу пожежі події.

Однак цей факт НЕ означає, що подія використовує резервне поле делегата . По правді, подія може використовувати резервне поле будь-якого іншого типу структури даних на ваш вибір. Якщо ви явно реалізуєте подію в C #, ви можете обирати спосіб зберігання обробників подій (зауважте, що обробники подій є екземплярами типу події , що, в свою чергу, є обов'язковим типом делегата --- від попереднього спостереження ). Але ви можете зберігати ці обробники подій (які є делегуючими екземплярами) у такій структурі даних, як a Listабо a Dictionaryчи будь-який інший, або навіть у резервному полі делегата. Але не забувайте, що НЕ обов'язково використовувати поле делегата.


4

Подія в .net - це призначена комбінація методу Add і методу Remove, який очікує певного типу делегата. І C #, і vb.net можуть автоматично генерувати код для методів додавання та видалення, який визначатиме делегата для утримання підписок на події, а також додає / видаляє передане в делегаті до / з цього делегата підписки. VB.net також автоматично генерує код (із заявою RaiseEvent), щоб викликати список підписок, якщо і лише якщо він не порожній; чомусь C # не генерує останнього.

Зауважте, що хоча звичайно керувати підписками на події за допомогою делегата багатоадресної передачі, це не єдиний засіб для цього. З публічної точки зору, передбачуваний підписник подій повинен знати, як повідомити об’єкту, що він хоче отримувати події, але йому не потрібно знати, який механізм видавець використовуватиме для події подій. Зауважте також, що хоч хто визначав структуру даних про події в .net, мабуть, вважав, що має бути публічний засіб їх підняття, ні C #, ні vb.net не використовують цю функцію.


3

Щоб визначитись із подією просто:

Подія - СПРАВКА делегату з двома обмеженнями

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


}

2

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


0

Якщо ви поставите прапорець проміжну мову, ви дізнаєтесь, що компілятор .net перетворює делегата в закритий клас в IL з деякими вбудованими функціями, такими як виклик, beginInvoke, endInvoke та клас делегата, успадкований від іншого класу, може називатися "SystemMulticast". Я думаю, що Event - це дочірній клас Delegate з деякими додатковими властивостями.

Різниця між примірником події та делегатом полягає в тому, що ви не можете запускати подію поза декларацією. Якщо ви оголошуєте подію в класі А, ви можете запустити цю подію лише в класі А. Якщо ви оголосите делегата в класі А, ви можете використовувати цього делегата де завгодно. Я думаю, що це головна відмінність між ними

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.