événement Action <> vs événement EventHandler <>


144

Y a-t-il une différence entre déclarer event Action<>et event EventHandler<>.

En supposant que peu importe quel objet a réellement déclenché un événement.

par exemple:

public event Action<bool, int, Blah> DiagnosticsEvent;

contre

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

class DiagnosticsArgs : EventArgs
{
    public DiagnosticsArgs(bool b, int i, Blah bl)
    {...}
    ...
}

l'utilisation serait presque la même dans les deux cas:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

Il y a plusieurs choses que je n'aime pas dans le event EventHandler<>pattern:

  • Déclaration de type supplémentaire dérivée d'EventArgs
  • Passage obligatoire de la source de l'objet - souvent personne ne s'en soucie

Plus de code signifie plus de code à maintenir sans aucun avantage clair.

En conséquence, je préfère event Action<>

Cependant, seulement s'il y a trop d'arguments de type dans Action <>, une classe supplémentaire est requise.


2
plusOne (je viens de battre le système) pour "personne ne s'en soucie"
hyankov

@plusOne: J'ai vraiment besoin de connaître l'expéditeur! Dites que quelque chose se passe et vous voulez savoir qui l'a fait. C'est là que vous avez besoin de la «source d'objet» (aka l'expéditeur).
Kamran Bigdely

l'expéditeur peut être une propriété dans la charge utile de l'événement
Thanasis Ioannidis

Réponses:


67

La principale différence sera que si vous utilisez Action<>votre événement, il ne suivra pas le modèle de conception de pratiquement tout autre événement du système, ce que je considérerais comme un inconvénient.

L'un des avantages du modèle de conception dominant (mis à part le pouvoir de la similitude) est que vous pouvez étendre l' EventArgsobjet avec de nouvelles propriétés sans modifier la signature de l'événement. Cela serait toujours possible si vous Action<SomeClassWithProperties>utilisiez, mais je ne vois pas vraiment l'intérêt de ne pas utiliser l'approche régulière dans ce cas.


L'utilisation pourrait-elle Action<>entraîner des fuites de mémoire? Un inconvénient du EventHandlermodèle de conception est les fuites de mémoire. Il convient également de noter qu'il peut y avoir plusieurs gestionnaires d'événements mais une seule action
Luke T O'Brien

4
@ LukeTO'Brien: Les événements sont essentiellement des délégués, donc les mêmes possibilités de fuite de mémoire existent avec Action<T>. En outre, un Action<T> peut faire référence à plusieurs méthodes. Voici un résumé qui démontre que: gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
Fredrik Mörk

89

Sur la base de certaines des réponses précédentes, je vais diviser ma réponse en trois domaines.

Premièrement, les limites physiques de l'utilisation de Action<T1, T2, T2... >vs en utilisant une classe dérivée de EventArgs. Il y en a trois: Premièrement, si vous modifiez le nombre ou les types de paramètres, chaque méthode qui s'abonne devra être modifiée pour se conformer au nouveau modèle. S'il s'agit d'un événement public que les assemblées tierces utiliseront, et qu'il y a une possibilité que les arguments d'événement changent, ce serait une raison d'utiliser une classe personnalisée dérivée d'arguments d'événements pour des raisons de cohérence (rappelez-vous, vous POUVEZ toujours use an Action<MyCustomClass>) Deuxièmement, l'utilisation Action<T1, T2, T2... >vous empêchera de renvoyer un retour à la méthode appelante à moins que vous n'ayez une sorte d'objet (avec une propriété Handled par exemple) qui est passé avec l'action. Troisièmement, vous n'obtenez pas de paramètres nommés, donc si vous passez boolun an int, deuxstring's, et a DateTime, vous n'avez aucune idée de la signification de ces valeurs. En remarque, vous pouvez toujours avoir une méthode "Activer cet événement en toute sécurité tout en continuant à utiliser Action<T1, T2, T2... >".

Deuxièmement, les implications de cohérence. Si vous avez déjà un grand système avec lequel vous travaillez, il est presque toujours préférable de suivre la façon dont le reste du système est conçu à moins que vous n'ayez une très bonne raison de ne pas trop. Si vous avez des événements publics qui doivent être gérés, la possibilité de substituer des classes dérivées peut être importante. Garde cela à l'esprit.

Troisièmement, dans la vraie vie, je trouve personnellement que j'ai tendance à créer beaucoup d'événements ponctuels pour des choses comme les changements de propriété avec lesquels j'ai besoin d'interagir (en particulier lorsque je fais MVVM avec des modèles de vue qui interagissent les uns avec les autres) ou lorsque l'événement a un seul paramètre. La plupart du temps, ces événements prennent la forme de public event Action<[classtype], bool> [PropertyName]Changed;ou public event Action SomethingHappened;. Dans ces cas, il y a deux avantages. Tout d'abord, j'obtiens un type pour la classe émettrice. Si MyClassdéclare et est la seule classe qui déclenche l'événement, j'obtiens une instance explicite de MyClasspour travailler avec dans le gestionnaire d'événements. Deuxièmement, pour les événements simples tels que les événements de changement de propriété, la signification des paramètres est évidente et indiquée dans le nom du gestionnaire d'événements et je n'ai pas à créer une myriade de classes pour ces types d'événements.


Article de blog génial. Vaut vraiment le détour si vous lisez ce fil!
Vexir

1
Réponse détaillée et bien pensée qui explique le raisonnement derrière la conclusion
MikeT

18

Dans la plupart des cas, je dirais de suivre le modèle. Je l' ai dévié de lui, mais très rarement, et pour des raisons de spécifiques. Dans le cas d'espèce, le plus gros problème que j'aurais est que j'utiliserais probablement toujours un Action<SomeObjectType>, me permettant d'ajouter des propriétés supplémentaires plus tard, et d'utiliser la propriété bidirectionnelle occasionnelle (pensez Handled, ou d'autres événements de rétroaction où le l'abonné doit définir une propriété sur l'objet événement). Et une fois que vous avez commencé dans cette ligne, vous pourriez aussi bien l'utiliser EventHandler<T>pour certains T.


14

L'avantage d'une approche plus verbeuse vient lorsque votre code se trouve dans un projet de 300 000 lignes.

En utilisant l'action, comme vous l'avez fait, il n'y a aucun moyen de me dire ce que sont bool, int et Blah. Si votre action a passé un objet qui a défini les paramètres, ok.

En utilisant un EventHandler qui voulait un EventArgs et si vous complétiez votre exemple DiagnosticsArgs avec des getters pour les propriétés qui commentaient leur objectif, votre application serait plus compréhensible. Veuillez également commenter ou nommer complètement les arguments dans le constructeur DiagnosticsArgs.


6

Si vous suivez le modèle d'événement standard, vous pouvez ajouter une méthode d'extension pour rendre la vérification du déclenchement d'événement plus sûre / plus facile. (c'est-à-dire que le code suivant ajoute une méthode d'extension appelée SafeFire () qui effectue la vérification nulle, ainsi que (évidemment) la copie de l'événement dans une variable distincte pour être à l'abri de la condition de course nulle habituelle qui peut affecter les événements.)

(Bien que je sois dans une sorte de double esprit si vous devriez utiliser des méthodes d'extension sur des objets nuls ...)

public static class EventFirer
{
    public static void SafeFire<TEventArgs>(this EventHandler<TEventArgs> theEvent, object obj, TEventArgs theEventArgs)
        where TEventArgs : EventArgs
    {
        if (theEvent != null)
            theEvent(obj, theEventArgs);
    }
}

class MyEventArgs : EventArgs
{
    // Blah, blah, blah...
}

class UseSafeEventFirer
{
    event EventHandler<MyEventArgs> MyEvent;

    void DemoSafeFire()
    {
        MyEvent.SafeFire(this, new MyEventArgs());
    }

    static void Main(string[] args)
    {
        var x = new UseSafeEventFirer();

        Console.WriteLine("Null:");
        x.DemoSafeFire();

        Console.WriteLine();

        x.MyEvent += delegate { Console.WriteLine("Hello, World!"); };
        Console.WriteLine("Not null:");
        x.DemoSafeFire();
    }
}

4
... ne pouvez-vous pas faire la même chose avec Action <T>? SafeFire <T> (cette action <T> theEvent, T theEventArgs) devrait fonctionner pour ... et pas besoin d'utiliser "where"
Beachwalker

6

Je me rends compte que cette question a plus de 10 ans, mais il me semble que non seulement la réponse la plus évidente n’a pas été abordée, mais que la question n’a peut-être pas vraiment compris clairement ce qui se passe sous les couvertures. En outre, il y a d'autres questions sur la liaison tardive et ce que cela signifie en ce qui concerne les délégués et les lambdas (nous en parlerons plus tard).

Commencez par vous adresser à l'éléphant / gorille de 800 lb dans la pièce, quand choisir eventvs Action<T>/ Func<T>:

  • Utilisez un lambda pour exécuter une instruction ou une méthode. À utiliser eventlorsque vous voulez plus d'un modèle pub / sous avec plusieurs instructions / lambdas / fonctions qui s'exécuteront (c'est une différence majeure dès le départ).
  • Utilisez un lambda lorsque vous souhaitez compiler des instructions / fonctions en arborescences d'expression. Utilisez des délégués / événements lorsque vous souhaitez participer à une liaison tardive plus traditionnelle, telle que celle utilisée dans la réflexion et l'interopérabilité COM.

À titre d'exemple d'événement, connectons un ensemble d'événements simple et `` standard '' à l'aide d'une petite application console comme suit:

public delegate void FireEvent(int num);

public delegate void FireNiceEvent(object sender, SomeStandardArgs args);

public class SomeStandardArgs : EventArgs
{
    public SomeStandardArgs(string id)
    {
        ID = id;
    }

    public string ID { get; set; }
}

class Program
{
    public static event FireEvent OnFireEvent;

    public static event FireNiceEvent OnFireNiceEvent;


    static void Main(string[] args)
    {
        OnFireEvent += SomeSimpleEvent1;
        OnFireEvent += SomeSimpleEvent2;

        OnFireNiceEvent += SomeStandardEvent1;
        OnFireNiceEvent += SomeStandardEvent2;


        Console.WriteLine("Firing events.....");
        OnFireEvent?.Invoke(3);
        OnFireNiceEvent?.Invoke(null, new SomeStandardArgs("Fred"));

        //Console.WriteLine($"{HeightSensorTypes.Keyence_IL030}:{(int)HeightSensorTypes.Keyence_IL030}");
        Console.ReadLine();
    }

    private static void SomeSimpleEvent1(int num)
    {
        Console.WriteLine($"{nameof(SomeSimpleEvent1)}:{num}");
    }
    private static void SomeSimpleEvent2(int num)
    {
        Console.WriteLine($"{nameof(SomeSimpleEvent2)}:{num}");
    }

    private static void SomeStandardEvent1(object sender, SomeStandardArgs args)
    {

        Console.WriteLine($"{nameof(SomeStandardEvent1)}:{args.ID}");
    }
    private static void SomeStandardEvent2(object sender, SomeStandardArgs args)
    {
        Console.WriteLine($"{nameof(SomeStandardEvent2)}:{args.ID}");
    }
}

La sortie ressemblera à ceci:

entrez la description de l'image ici

Si vous faisiez la même chose avec Action<int>ou Action<object, SomeStandardArgs>, vous ne verriez que SomeSimpleEvent2et SomeStandardEvent2.

Alors qu'est-ce qui se passe à l'intérieur event?

Si nous développons FireNiceEvent, le compilateur génère en fait ce qui suit (j'ai omis certains détails concernant la synchronisation des threads qui ne sont pas pertinents pour cette discussion):

   private EventHandler<SomeStandardArgs> _OnFireNiceEvent;

    public void add_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
    {
        Delegate.Combine(_OnFireNiceEvent, handler);
    }

    public void remove_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
    {
        Delegate.Remove(_OnFireNiceEvent, handler);
    }

    public event EventHandler<SomeStandardArgs> OnFireNiceEvent
    {
        add
        {
            add_OnFireNiceEvent(value)
        }
        remove
        {
            remove_OnFireNiceEvent(value)

        }
    }

Le compilateur génère une variable de délégué privé qui n'est pas visible par l'espace de noms de classe dans lequel elle est générée. Ce délégué est ce qui est utilisé pour la gestion des abonnements et la participation à la liaison tardive, et l'interface publique est les opérateurs familiers +=et que -=nous avons tous appris à connaître et à aimer :)

Vous pouvez personnaliser le code des gestionnaires d'ajout / suppression en modifiant la portée du FireNiceEventdélégué sur protected. Cela permet désormais aux développeurs d'ajouter des hooks personnalisés aux hooks, tels que la journalisation ou des hooks de sécurité. Cela donne vraiment des fonctionnalités très puissantes qui permettent désormais une accessibilité personnalisée à l'abonnement en fonction des rôles des utilisateurs, etc. Pouvez-vous faire cela avec des lambdas? (En fait, vous pouvez en compiler des arborescences d'expressions personnalisées, mais cela dépasse le cadre de cette réponse).

Pour aborder quelques points à partir de certaines des réponses ici:

  • Il n'y a vraiment aucune différence dans la «fragilité» entre la modification de la liste d'arguments dans Action<T>et la modification des propriétés d'une classe dérivée de EventArgs. Les deux nécessiteront non seulement un changement de compilation, mais ils changeront tous les deux une interface publique et nécessiteront un contrôle de version. Aucune différence.

  • En ce qui concerne la norme de l'industrie, cela dépend de l'endroit où elle est utilisée et pourquoi. Action<T>et tel est souvent utilisé dans IoC et DI, et eventest souvent utilisé dans le routage de messages tels que les frameworks de type GUI et MQ. Notez que je l'ai dit souvent , pas toujours .

  • Les délégués ont des durées de vie différentes de celles des lambdas. Il faut aussi être conscient de la capture ... non seulement avec la fermeture, mais aussi avec la notion de «regardez ce que le chat a entraîné». Cela affecte l'empreinte mémoire / la durée de vie ainsi que la gestion des fuites.

Une dernière chose, quelque chose que j'ai mentionné plus tôt ... la notion de liaison tardive. Vous verrez souvent cela lors de l'utilisation d'un framework comme LINQ, concernant le moment où un lambda devient `` live ''. C'est très différent de la liaison tardive d'un délégué, qui peut se produire plus d'une fois (c'est-à-dire que le lambda est toujours là, mais la liaison se produit à la demande aussi souvent que nécessaire), par opposition à un lambda, qui une fois qu'il se produit, c'est fait - la magie a disparu et la ou les méthodes / propriété (s) seront toujours liées. Quelque chose à garder à l'esprit.


4

En regardant les modèles d'événements .NET standard, nous trouvons

La signature standard d'un délégué d'événement .NET est:

void OnEventRaised(object sender, EventArgs args);

[...]

La liste d'arguments contient deux arguments: l'expéditeur et les arguments d'événement. Le type d'expéditeur au moment de la compilation est System.Object, même si vous connaissez probablement un type plus dérivé qui serait toujours correct. Par convention, utilisez object .

Ci-dessous sur la même page, nous trouvons un exemple de la définition d'événement typique qui est quelque chose comme

public event EventHandler<EventArgs> EventName;

Avions-nous défini

class MyClass
{
  public event Action<MyClass, EventArgs> EventName;
}

le gestionnaire aurait pu être

void OnEventRaised(MyClass sender, EventArgs args);

sendera le type correct ( plus dérivé ).


Désolé de ne pas avoir remarqué que la différence réside dans la signature du gestionnaire, qui bénéficierait d'une saisie plus précise sender .
user1832484
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.