Vantaggi dell'utilizzo di delegati in stile .NET rispetto a quelli personalizzati

2

Conosco eventi e delegati in C #. Li trovo incredibilmente utili per i sottosistemi basati sugli eventi. Una cosa che non capisco, tuttavia, è il motivo per cui tutta la documentazione .NET per gli eventi utilizza un modello molto specifico per i delegati:

public class Foo
{
    public class CustomEventArgs : System.EventArgs
    {
        ...
    }

    public delegate void CustomEventHandler(object sender, EventArgs e);
    public event CustomEventHandler CustomEvent;

    public void OnCustomEvent(int x, float y, bool z)
    {
        if (CustomEvent != null)
            CustomEvent(this, new CustomEventArgs(x, y, z));
    }
}

Trovo questo metodo di dichiarazione e utilizzo dei delegati incredibilmente insicuro e goffo rispetto all'utilizzo di delegati personalizzati. Ad esempio:

public class Foo
{
    public delegate void CustomEventHandler(Foo foo, int x, float y, bool z);
    public event CustomEventHandler CustomEvent;

    public void OnCustomEvent(int x, int y, int z)
    {
        if (CustomEvent != null)
            CustomEvent(this, x, y, z);
    }
}

Oltre all'ovvio vantaggio di "È il modello più familiare ai programmatori .NET", non riesco a vedere nessun altro vantaggio pratico nell'utilizzare i delegati con le firme void CustomEventHandler(object sender, CustomEventArgs e) anziché con i delegati personalizzati. Principalmente, hai i seguenti vantaggi con i delegati personalizzati:

  1. Puoi garantire che il mittente sia di un tipo specifico.
  2. Non hai bisogno di una classe completamente nuova solo per passare i dati dell'evento, che porta al codice di messaggistica e al nome dell'inquinamento

Sarei interessato a sapere se ci sono altri vantaggi nell'usare il modello .NET per eventi e delegati che potrei mancare.

Modifica

Principalmente come risposta a @MainMa, volevo dare un esempio più concreto. L'esempio è una semplice rappresentazione di un personaggio con un concetto di salute e morte. Il personaggio lancia eventi quando è ferito o muore. Considera lo scenario seguente, utilizzando i delegati personalizzati:

public class Character
{
    public delegate void DeathEventHandler(Character character);
    public delegate void HurtEventHandler(Character character, float damage);

    public event DeathEventHandler DeathEvent;
    public event HurtEventHandler HurtEvent;

    public bool IsDead { ... }

    public void ApplyDamage(float damage)
    {
        ...
        OnHurt(damage);
        ...
        if (IsDead)
            OnDeath();
    }

    public void OnHurt(float damage)
    {
        if (HurtEvent != null)
            HurtEvent(this, damage);
    }

    public void OnDeath()
    {
        if (DeathEvent != null)
            DeathEvent(this);
    }
}

Per me, questo è semplice. Separa le preoccupazioni e impone l'incapsulamento. Un conduttore che è preoccupato solo per la morte di un personaggio non ha bisogno di sapere quanti danni sono stati applicati al personaggio prima della morte. Così avanti e così via.

Ora confronta questo modello .NET.

public class Character
{
    public class CharacterHurtEventArgs : EventArgs
    {
        public float Damage { ... }
        public bool Dead { ... }
    }

    public event EventHandler<CharacterHurtEventArgs> HurtEvent;
    public event EventHandler<EventArgs> DeathEvent; // No args ...?

    // ... The rest is the same story as above more or less ...
}

Usando questo, ho appena aggiunto una classe extra. Il conduttore ha ancora bisogno di delegati diversi per questi eventi, e non vi è alcuna garanzia che l'oggetto morto sia un personaggio. Qualunque cosa potrebbe lanciarlo.

Ovviamente, seguendo il suggerimento di @ MainMa, potremmo modificarlo in:

public class Character
{
    public class HealthConditionEventArgs : EventArgs
    {
        public float Damage { ... }
        public bool Dead { ... }
        public float Hitpoints { ... }
        ...
    }

    public event EventHandler<HealthConditionEventArgs> HealthConditionChangedEvent;

    public void ApplyDamage(float damage)
    {
        ...
        HealthConditionChangedEventArgs e = new HealthConditionChangedEventArgs();
        ...
        OnHealthConditionChanged(e);
        ...
    }

    public void OnHealthConditionChanged(HealthConditionEventArgs e)
    {
        if (HealthConditionChangedEvent != null)
            HealthConditionChangedEvent(e);
    }
}

Questo è leggermente più ordinato, ma elimina la separazione delle preoccupazioni. Ora se il conduttore si preoccupa solo della morte, dovrà ricevere tutti gli eventi feriti. Naturalmente, potremmo fare in modo che eventi separati contengano lo stesso argomento degli eventi, ma faresti comunque circolare l'intera condizione di salute dei personaggi come parte dell'evento. Di nuovo, se ti importa solo della morte, non è necessario che il conduttore sappia più del fatto che il personaggio è morto.

A mio parere, il primo esempio, tuttavia, risolve tutti questi problemi di incapsulamento. Delegati discreti per eventi discreti, con solo le informazioni necessarie passate in ogni evento. Neanche nuove classi.

    
posta Zeenobit 06.02.2015 - 18:55
fonte

3 risposte

6

Immaginiamo un esempio di un livello di presentazione. Il livello ha interazioni con il mouse: ogni volta che l'utente muove un mouse, un evento che indica quale mouse si è mosso (immagina che l'utente possa avere più mouse) e come il mouse possa essere spostato.

Con il tuo approccio, la bozza del codice sarebbe:

public delegate void MouseMovedHandler(
    PresentationLayer l, Guid mouseId, int fromX, int fromY, int toX, int toY);

Ovviamente sarà rapidamente refactored in:

public delegate void MouseMovedHandler(
    PresentationLayer l, Guid mouseId, Coord from, Coord to);

Ma poi, scoprirai che vuoi sapere, ad esempio, la distanza tra from e to . Quindi creerai una classe Line , definita da due punti, e il tuo delegato diventerà:

public delegate void MouseMovedHandler(PresentationLayer l, Guid mouseId, Line motion)

Better. Ora, cosa succede se vogliamo essere in grado di fare la differenza tra una linea semplice e un movimento di un mouse specifico identificato? Bene, finiremo per creare la classe:

class MouseMotion : Line
{
    public Mouse Mouse { get; }
    public Line Motion { get; }
    ...
}

public delegate void MouseMovedHandler(PresentationLayer l, MouseMotion motion)

Ora, c'è una posizione nella tua app in cui devi tracciare ogni possibile input: movimento del mouse, clic del pulsante del mouse, tratti della tastiera. Ne hai bisogno perché l'applicazione avvia l'elaborazione in background quando l'utente non sta facendo nulla, ma dovrebbe fermarlo e liberare le risorse nell'interfaccia utente se l'utente sta utilizzando la tua app.

Questo significa che ora hai bisogno di qualcosa di comune tra MouseMotion , MouseButtonChange e KeyboardStroke . Qualcosa come IEvent . O non sarebbe più semplice usare EventArgs ?

Quindi finisci con:

class MouseMotionEventArgs: EventArgs { ... }
public delegate void MouseMovedHandler(PresentationLayer l, MouseMotionEventArgs motion)

Tranne che è possibile evitare il delegato, come spiegato all'inizio della risposta. Ora puoi restare con il delegato al fine di avere il tuo% tipizzato strongmentePresentationLayer che probabilmente non ti servirà il più delle volte oppure sostituirlo con un% co_de scadente utilizzando object .

La versione precedente della risposta , per completezza:

C'è un motivo per cui crei un EventHandler<MouseMotionEventArgs> invece di utilizzare CustomEventHandler ?

Con il generico EventHandler<CustomEventArgs> , il codice inizia ad assomigliare a questo:

public class Foo
{
    public event EventHandler<3DPointChangedEventArgs> CustomEvent;

    public void OnCustomEvent(3DPoint point)
    {
        if (CustomEvent != null)
        {
            CustomEvent(this, (3DPointChangedEventArgs)point);
        }
    }
}

Ora confrontalo con il codice che hai suggerito:

Parlando specificamente dell'inquinamento del nome, avere classi 3DPoint non è poi così male. Questi nomi sono in genere molto chiari ed esprimono l'intenzione dell'autore e non possono essere confusi con altri oggetti di business.

Rispondendo all'ultima modifica, ci sono tre punti da considerare:

  1. Non è necessariamente utile separare la morte dalla salute. La rappresentazione di base della salute potrebbe essere un numero; per esempio, 0.0 può rappresentare la morte e 100.0 può corrispondere alla salute completa.

    Ovviamente, un numero dovrebbe essere sostituito dalla classe *EventArgs che può quindi contenere regole aziendali che possono essere modificate nel tempo. Ad esempio, se si tratta di un gioco, ulteriori caratteristiche potrebbero includere ulteriori intervalli di salute, che consentono di avere, ad esempio, il 120% della salute.

    Questo potrebbe creare un problema di prestazioni: un evento in meno significa che se qualcosa sta ascoltando gli eventi di morte, verrà ora notificato su tutti gli eventi di cambiamento di salute. Non ne sarei preoccupato tanto presto, e aspetterò fino a quando non diventerà un vero collo di bottiglia. Dopotutto, cosa succede se la performance diventa ancora migliore, ora che non abbiamo una condizione in Health ?

  2. Se l'autore decide di mantenere entrambi gli eventi, l'evento death può utilizzare ApplyDamage() , rendendo possibile l'utilizzo di EventArgs . Questo rende un delegato di meno da scrivere.

  3. Chi ascolta l'evento? Immaginiamo che il mondo lo ascolti per rimuovere gli oggetti pochi minuti dopo la loro morte (il che ha senso, poiché gli oggetti potrebbero non essere consapevoli del mondo, e quindi non saranno in grado di rimuoverli da esso).

    Questo significa anche che non ci interessa davvero se l'oggetto morto è un EventArgs.Empty o un Character o un Trebuchet o un WarShip . Ciò significa che è OK perdere i tipi durante la trasmissione delle informazioni attraverso l'evento: la rimozione sarà la stessa per un paladino o una pecora.

Ecco il risultato finale che applica i punti 1 e 3:

public class Health
{
    private double value;

    public Health(double value) { ... }

    public void ApplyDamage() { ... }
}

public class Character
{
    public event EventHandler<HealthEventArgs> HealthChanged;

    public Health Health { get { return this.health; } }

    public void ApplyDamage(double damage)
    {
        ...
        this.health.ApplyDamage(damage);
        ...
    }

    public void OnHealthChange(float damage)
    {
        if (HurtEvent != null)
        {
            HurtEvent(this, damage);
        }
    }
}

La gestione degli eventi può quindi essere eseguita in questo modo, rendendo l'astrazione del tipo dell'oggetto mondo rimosso:

public class World
{
    public void MarkAsDead(object worldObject)
    {
        // Wait for five minutes.
        this.elements.Remove(worldObject);
    }
}
    
risposta data 06.02.2015 - 19:13
fonte
0

Questa è la convenzione adottata da Winforms e anche alcuni altri gestori di eventi nell'ecosistema .NET. Fornisce due cose: l'oggetto che ha causato l'evento e alcuni argomenti specifici dell'evento.

Uno dei motivi per cui è fatto in questo modo è perché gli eventi possono essere multicast; puoi avere un gestore di eventi che gestisce più eventi o un evento che viene trasmesso a diversi gestori. Per fornire flessibilità, l'oggetto nuda viene inviato al gestore eventi, nel caso in cui non sappiamo in realtà quale sia l'oggetto che genera l'evento. Questa convenzione è stata creata prima dell'introduzione dei generici.

Si noti che il framework .NET fornisce una versione generica dell'evento Gestore delegato da .NET 2.0, che fornisce sicurezza del tipo in fase di compilazione.

    
risposta data 06.02.2015 - 19:20
fonte
0

Non posso ancora commentare, ma questi generici sono anche validi:

private event Action SomeEvent1;
private event Action<int> SomeEvent2;
private event Func<int, int, int> SomeEvent3;

Fornisce un modello ambiguo non descrittivo se generato automaticamente:

int CLASSNAME_SomeEvent3(int arg1, int arg2) //arg what? argh!!
{
}
    
risposta data 07.02.2015 - 03:01
fonte

Leggi altre domande sui tag