Quali sono le differenze tra delegati ed eventi?


Risposte:


283

Una dichiarazione di evento aggiunge un livello di astrazione e protezione sull'istanza delegata . Questa protezione impedisce ai client del delegato di reimpostare il delegato e il suo elenco di invocazioni e consente solo di aggiungere o rimuovere target dall'elenco di invocazioni.


44
Se ovviamente, questo livello di protezione impedisce anche ai "client" (codice al di fuori della classe / struttura di definizione) di invocare il delegato e di ottenere in qualsiasi modo l'oggetto delegato "dietro" l'evento.
Jeppe Stig Nielsen il

7
Non del tutto vero. È possibile dichiarare un evento senza un'istanza delegata back-end. In c #, è possibile implementare un evento in modo esplicito e utilizzare una diversa struttura di dati back-end di propria scelta.
Miguel Gamboa,

3
@mmcdole puoi fornire un esempio per spiegare il suo?
vivek nuna,

103

Per capire le differenze puoi guardare questi 2 esempi

Esempio con delegati (in questo caso, un'azione - che è un tipo di delegato che non restituisce un valore)

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

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

Per usare il delegato, dovresti fare qualcosa del genere:

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

Questo codice funziona bene ma potresti avere dei punti deboli.

Ad esempio, se scrivo questo:

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

con l'ultima riga di codice, ho ignorato i comportamenti precedenti solo con uno mancante +(ho usato =invece di +=)

Un altro punto debole è che ogni classe che usa la tua Animalclasse può sollevare RaiseEventsemplicemente chiamandola animal.RaiseEvent().

Per evitare questi punti deboli puoi usare eventsin c #.

La tua classe di animali cambierà in questo modo:

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

per chiamare eventi

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

differenze:

  1. Non stai utilizzando una proprietà pubblica ma un campo pubblico (utilizzando gli eventi, il compilatore protegge i tuoi campi da accessi indesiderati)
  2. Gli eventi non possono essere assegnati direttamente. In questo caso, non genererà l'errore precedente che ho mostrato con l'override del comportamento.
  3. Nessuno al di fuori della tua classe può sollevare l'evento.
  4. Gli eventi possono essere inclusi in una dichiarazione di interfaccia, mentre un campo no

Appunti:

EventHandler è dichiarato come il seguente delegato:

public delegate void EventHandler (object sender, EventArgs e)

accetta un mittente (di tipo Oggetto) e argomenti evento. Il mittente è nullo se proviene da metodi statici.

Questo esempio, che usa EventHandler<ArgsSpecial>, può anche essere scritto usando EventHandlerinvece.

Fare riferimento qui per la documentazione su EventHandler


7
Tutto è andato benissimo fino a quando mi sono imbattuto in "Nessuno al di fuori della tua classe è in grado di organizzare l'evento". Cosa significa? Nessuno può chiamare RaiseEventpurché un metodo chiamante abbia accesso a un'istanza animalnel codice che utilizza l'evento?
dance2die,

11
@Sung Events può essere generato solo all'interno della classe, forse non sono stato chiaro spiegarlo. Con gli eventi puoi chiamare la funzione che genera l'evento (incapsulamento), ma può essere aumentato solo dall'interno della classe che lo definisce. Fammi sapere se non sono chiaro.
favoloso

1
"Gli eventi non possono essere assegnati direttamente." A meno che non ti capisca male, questo non è vero. Ecco un esempio: gist.github.com/Chiel92/36bb3a2d2ac7dd511b96
Chiel ten Brinke,

2
@faby, vuoi dire che anche se l'evento è stato dichiarato pubblico, non posso ancora farlo animal.Run(this, new ArgsSpecial("Run faster");?
Pap

1
@ChieltenBrinke Naturalmente l'evento può essere assegnato ai membri della classe ... ma non diversamente.
Jim Balter,

94

Oltre alle proprietà sintattiche e operative, c'è anche una differenza semantica.

I delegati sono, concettualmente, modelli di funzione; cioè, esprimono un contratto a cui una funzione deve aderire per essere considerata del "tipo" del delegato.

Gli eventi rappresentano ... beh, gli eventi. Hanno lo scopo di avvisare qualcuno quando succede qualcosa e sì, aderiscono a una definizione delegata ma non sono la stessa cosa.

Anche se fossero esattamente la stessa cosa (sintatticamente e nel codice IL), rimarrebbe comunque la differenza semantica. In generale preferisco avere due nomi diversi per due concetti diversi, anche se sono implementati nello stesso modo (il che non significa che mi piace avere lo stesso codice due volte).


8
Ottima descrizione dei delegati.
Sampson,

1
Quindi possiamo dire che un evento è un tipo "speciale" di un delegato?
Pap

Non capisco, puoi usare un delegato per "avvisare qualcuno quando succede qualcosa". Forse non lo faresti, ma puoi e quindi non è una proprietà intrinseca dell'evento.
steve

@Jorge Córdoba esempio di delegato ed delegato agli eventi è proprietario di un giornale ed eventi (Iscriviti o annulla iscrizione) e alcune persone acquistano il giornale e alcune persone non acquistano il giornale significa che il proprietario del giornale non può forzare tutti a comprare il giornale il mio punto giusto o sbagliato?
Rahul_Patil,

37

Ecco un altro buon link a cui fare riferimento. http://csharpindepth.com/Articles/Chapter2/Events.aspx

In breve, il take away dell'articolo - Gli eventi sono incapsulamento sui delegati.

Citazione dall'articolo:

Supponiamo che gli eventi non esistessero come concetto in C # /. NET. In che modo un'altra classe si iscriverebbe a un evento? Tre opzioni:

  1. Una variabile di delegato pubblico

  2. Una variabile delegata supportata da una proprietà

  3. Una variabile delegata con i metodi AddXXXHandler e RemoveXXXHandler

L'opzione 1 è chiaramente orribile, per tutti i normali motivi per cui aborriamo le variabili pubbliche.

L'opzione 2 è leggermente migliore, ma consente agli abbonati di sostituirsi efficacemente: sarebbe fin troppo facile scrivere someInstance.MyEvent = eventHandler; che sostituirà qualsiasi gestore di eventi esistente anziché aggiungerne uno nuovo. Inoltre, è ancora necessario scrivere le proprietà.

L'opzione 3 è fondamentalmente ciò che gli eventi ti danno, ma con una convenzione garantita (generata dal compilatore e supportata da flag aggiuntivi nell'IL) e un'implementazione "gratuita" se sei soddisfatto della semantica che ti danno gli eventi simili a campi. Sottoscrivere e annullare la sottoscrizione agli eventi è incapsulato senza consentire l'accesso arbitrario all'elenco dei gestori di eventi e le lingue possono semplificare le cose fornendo la sintassi sia per la dichiarazione che per la sottoscrizione.


Spiegazione piacevole e concisa. Grazie
Pap

Questa è una preoccupazione teorica più che altro, ma FWIW mi è sempre sembrato che l'argomento "Opzione 1 è male perché non ci piacciono le variabili pubbliche" potrebbe usare un po 'più di chiarimenti. Se lo sta dicendo perché a causa della "cattiva pratica OOP", tecnicamente una public Delegatevariabile esporrà "dati", ma per quanto ne so OOP non ha mai menzionato concetti come un Delegate(non è né un "oggetto" né un "messaggio") e .NET tratta comunque a malapena i delegati come i dati.
jrh

Anche se mi piacerebbe anche dare consigli più pratici, se ti trovi in ​​una situazione in cui desideri assicurarti che esista un solo gestore, creare i tuoi AddXXXHandlermetodi con una private Delegatevariabile può essere una buona opzione. In questo caso è possibile verificare se un gestore è già impostato e reagire in modo appropriato. Questa potrebbe anche essere una buona configurazione se hai bisogno dell'oggetto che tiene in mano Delegateper poter cancellare tutti i gestori ( eventnon ti dà alcun modo per farlo).
jrh

7

NOTA: se si ha accesso a C # 5.0 Unleashed , leggere le "Limitazioni sull'uso normale dei delegati" nel capitolo 18 "Eventi" per comprendere meglio le differenze tra i due.


Mi aiuta sempre ad avere un esempio semplice e concreto. Quindi eccone uno per la comunità. In primo luogo, mostro come usare i delegati da soli per fare ciò che gli eventi fanno per noi. Quindi mostro come funzionerebbe la stessa soluzione con un'istanza di EventHandler. E poi spiego perché non vogliamo fare quello che spiego nel primo esempio. Questo post è stato ispirato da un articolo di John Skeet.

Esempio 1: utilizzo del delegato pubblico

Supponiamo che io abbia un'app WinForms con una singola casella a discesa. Il menu a discesa è associato a un List<Person>. Dove Persona ha proprietà di ID, Nome, NickName, HairColor. Nel modulo principale è un controllo utente personalizzato che mostra le proprietà di quella persona. Quando qualcuno seleziona una persona nel menu a discesa, le etichette nell'aggiornamento del controllo utente aggiornano per mostrare le proprietà della persona selezionata.

inserisci qui la descrizione dell'immagine

Ecco come funziona. Abbiamo tre file che ci aiutano a mettere insieme questo:

  • Mediator.cs - la classe statica contiene i delegati
  • Form1.cs - modulo principale
  • DetailView.cs: il controllo utente mostra tutti i dettagli

Ecco il codice pertinente per ciascuna delle classi:

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

Ecco il nostro controllo utente:

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;

    }
}

Finalmente abbiamo il seguente codice nel nostro Form1.cs. Qui stiamo chiamando OnPersonChanged, che chiama qualsiasi codice sottoscritto al delegato.

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. Quindi è così che faresti funzionare tutto questo senza usare eventi e solo usando delegati . Abbiamo appena messo un delegato pubblico in una classe: puoi renderlo statico o un singleton o qualsiasi altra cosa. Grande.

MA, MA, MA, non vogliamo fare ciò che ho appena descritto sopra. Perché i campi pubblici sono dannosi per molte, molte ragioni. Quindi quali sono le nostre opzioni? Come descrive John Skeet, ecco le nostre opzioni:

  1. Una variabile di delegato pubblico (questo è quello che abbiamo appena fatto sopra. Non farlo. Ti ho appena detto perché è male)
  2. Metti il ​​delegato in una proprietà con get / set (il problema qui è che gli abbonati potrebbero sostituirsi a vicenda - così potremmo iscrivere un gruppo di metodi al delegato e quindi potremmo accidentalmente dire PersonChangedDel = null , cancellando tutti gli altri abbonamenti. l'altro problema che rimane qui è che, poiché gli utenti hanno accesso al delegato, possono invocare gli obiettivi nell'elenco di invocazione: non vogliamo che gli utenti esterni abbiano accesso a quando sollevare i nostri eventi.
  3. Una variabile delegata con i metodi AddXXXHandler e RemoveXXXHandler

Questa terza opzione è essenzialmente ciò che un evento ci offre. Quando dichiariamo un EventHandler, ci dà accesso a un delegato - non pubblicamente, non come proprietà, ma come questa cosa chiamiamo un evento che ha appena aggiunto / rimosso gli accessor.

Vediamo come appare lo stesso programma, ma ora utilizzo un evento anziché il delegato pubblico (ho anche cambiato il nostro mediatore in un singleton):

Esempio 2: con EventHandler anziché un delegato pubblico

Mediatore:

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

Nota che se F12 su EventHandler, ti mostrerà che la definizione è solo un delegato generico con l'oggetto "mittente" extra:

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

Il controllo utente:

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;

    }
}

Infine, ecco il codice Form1.cs:

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

Poiché EventHandler vuole ed EventArgs come parametro, ho creato questa classe con una sola proprietà al suo interno:

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

Speriamo che questo ti mostri un po 'sul perché abbiamo eventi e su come sono diversi - ma funzionalmente uguali - dei delegati.


Anche se apprezzo tutto il buon lavoro svolto in questo post e mi è piaciuto leggere la maggior parte di esso, sento ancora che un problema non è stato risolto 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. Nell'ultima versione di Mediator, puoi comunque chiamare il OnPersonChangeogni volta che hai un riferimento al singleton. Forse dovresti menzionare che l' Mediatorapproccio non impedisce quel comportamento particolare ed è più vicino a un bus di eventi.
Ivaylo Slavov

6

È inoltre possibile utilizzare eventi nelle dichiarazioni di interfaccia, non così per i delegati.


2
L'interfaccia @surfen può contenere eventi, ma non delegati.
Alexandr Nikitin,

1
Cosa intendi esattamente? Puoi avere Action a { get; set; }dentro una definizione di interfaccia.
Chiel ten Brinke,

6

Che grande incomprensione tra eventi e delegati !!! Un delegato specifica un TIPO (come un class, o uninterface fa), mentre un evento è solo un tipo di MEMBRO (come campi, proprietà, ecc.). E, proprio come qualsiasi altro tipo di membro, anche un evento ha un tipo. Tuttavia, nel caso di un evento, il tipo di evento deve essere specificato da un delegato. Ad esempio, NON PUOI dichiarare un evento di un tipo definito da un'interfaccia.

Concludendo, possiamo fare la seguente Osservazione: il tipo di un evento DEVE essere definito da un delegato . Questa è la relazione principale tra un evento e un delegato ed è descritta nella sezione II.18 Definizione degli eventi delle partizioni da I a VI dell'ECMA-335 (CLI) :

Nell'uso tipico, TypeSpec (se presente) identifica un delegato la cui firma corrisponde agli argomenti passati al metodo di attivazione dell'evento.

Tuttavia, questo fatto NON implica che un evento utilizzi un campo delegato di supporto . In verità, un evento può utilizzare un campo di supporto di qualsiasi diverso tipo di struttura di dati di propria scelta. Se si implementa esplicitamente un evento in C #, si è liberi di scegliere il modo in cui archiviare i gestori di eventi (si noti che i gestori di eventi sono istanze del tipo di evento , che a sua volta è obbligatoriamente un tipo delegato --- dall'Osservazione precedente ). Tuttavia, è possibile archiviare i gestori di eventi (che sono istanze delegate) in una struttura di dati come a Listo a Dictionaryo in qualsiasi altro, o persino in un campo delegato di supporto. Ma non dimenticare che NON è obbligatorio utilizzare un campo delegato.


4

Un evento in .net è una combinazione designata di un metodo Add e di un metodo Remove, che prevedono entrambi un determinato tipo di delegato. Sia C # che vb.net possono generare automaticamente codice per i metodi di aggiunta e rimozione che definiranno un delegato per contenere le iscrizioni agli eventi e aggiungere / rimuovere i delegati passati a / da quel delegato di sottoscrizione. VB.net genererà anche automaticamente il codice (con l'istruzione RaiseEvent) per invocare l'elenco delle iscrizioni se e solo se non è vuoto; per qualche motivo, C # non genera quest'ultimo.

Si noti che mentre è comune gestire le iscrizioni agli eventi utilizzando un delegato multicast, questo non è l'unico modo per farlo. Da una prospettiva pubblica, un aspirante abbonato deve sapere come far sapere a un oggetto che desidera ricevere eventi, ma non ha bisogno di sapere quale meccanismo utilizzerà l'editore per sollevare gli eventi. Si noti inoltre che, sebbene chiunque abbia definito la struttura dei dati degli eventi in .net apparentemente abbia pensato che ci dovrebbe essere un mezzo pubblico per aumentarli, né C # né vb.net fanno uso di quella funzione.


3

Per definire l'evento in modo semplice:

L'evento è un RIFERIMENTO a un delegato con due restrizioni

  1. Non può essere invocato direttamente
  2. Non è possibile assegnare direttamente i valori (ad es. EventObj = delegateMethod)

Sopra due sono i punti deboli per i delegati ed è affrontato in caso di necessità. Esempio di codice completo per mostrare la differenza nel violinista è qui https://dotnetfiddle.net/5iR3fB .

Attiva / disattiva il commento tra Evento e Delegato e il codice client che invoca / assegna valori da delegare per comprendere la differenza

Ecco il codice inline.

 /*
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

Il delegato è un puntatore a funzione sicura. L'evento è un'implementazione del modello di progettazione editore-abbonato che utilizza delegato.


0

Se controlli Intermediate Language, saprai che il compilatore .net converte delegato in una classe sigillata in IL con alcune funzioni integrate, come invoke, startInvoke, endInvoke e classe delegate ereditata da un'altra classe, forse chiamata "SystemMulticast". Immagino che Event sia una classe figlio di delegato con alcune proprietà aggiuntive.

La differenza tra istanza di evento e delegato è che non è possibile eseguire eventi al di fuori della dichiarazione. Se si dichiara un evento in classe A, è possibile eseguire questo evento solo in classe A. Se si dichiara un delegato in classe A, è possibile utilizzare questo delegato ovunque. Penso che questa sia la principale differenza tra loro

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.