Codingly

Tout ce que vous n’avez pas besoin de savoir au sujet du mot clé event

Posted in Articles by Romain Verdier on août 17, 2009

Vous ne vous êtes jamais demandé à quoi servait, en C#, le mot clé event ? Moi oui. Et je vais même jusqu’à poser la question d’une certaine façon aux gens qui m’entourent, sauf peut-être dans la ligne 2 du métro :

—  A quoi ça sert, le mot clé event ?
—  Bah, à déclarer des events.
—  Oui, mais ça marche pas sans le mot clé event ? En écrivant juste, par exemple :
  public EventHandler Click;
—  Heu, si, je crois.
—  Donc, à quoi ça sert, le mot clé event ?
—  T’es relou, dégage.

Notez qu’à l’oral, il n’y a pas la coloration syntaxique. Je suppose que c’est ce qui conduit assez régulièrement mes interlocuteurs à être désagréables. (J’aime pas les gens désagréables !)

Au risque donc que vous commenciez à m’appeler Monsieur Mauclet, j’ai bien envie de regrouper dans ce post les principales différences qui existent entre :

public EventHandler Click;

Et :

public event EventHandler Click;

Et de combattre au passage certaines confusions, parce que je me sens l’âme d’un chevalier en ce moment.

Tout d’abord — et c’est la différence fondamentale dont découle la plupart des autres — un event (syntaxiquement assimilable à un champ de type delegate modifié par le mot clé event) n’est pas un champ. Il s’agit plutôt d’une sorte de propriété un peu spéciale revêtant une sémantique particulière : un event est explicitement, heh, un évènement.

Concrètement, écrire :

public event EventHandler Click;

Est équivalent à :

private EventHandler click;
public event EventHandler Click
{
	add { this.click += value; }
	remove { this.click -= value; }
}

Le code ci-dessus est valide en C#. Par défaut, un évènement possède des « accesseurs », add et remove, qui permettent respectivement d’ajouter et de supprimer des handlers à un champ privé, de type delegate. Notez qu’il s’agit ici de l’implémentation générée par le compilateur lorsque vous utilisez la déclaration classique d’un event, mais il est tout à fait possible de changer cette implémentation : vous êtes en effet libre de mettre ce que vous voulez dans le add et le remove. A vrai dire, un event n’est que ce couple add/remove.

Exemple !utile :

private EventHandler click;
private int handlerCount;
public event EventHandler Click
{
	add 
	{ 
		this.click += value; 
		this.handlerCount++;
	}
	remove
	{ 
		this.click -= value; 
		this.handlerCount--;
	}
}

Il est intéressant de faire l’analogie entre la façon usuelle de déclarer un event et l’écriture d’une propriété automatique. Dans les deux cas, le compilateur fait le grunt work, et se charge donc de générer le backing field encapsulé par l’event ou la propriété, ainsi que la logique associée (affectation/lecture de la valeur ou ajout/suppression du handler), parce que la plupart du temps, c’est effectivement ce que l’on désire. Dans les deux cas, au risque de me répéter, il est possible de reprendre la main et de faire ce que l’on veut en proposant une implémentation différente des accesseurs (get/set pour les propriétés, add/remove pour les events).

En résumé, un event n’est pas un champ, un delegate, ou un truc de facebook ; non, un event en C# est un moyen d’exposer de façon normalisée, deux méthodes permettant d’enregistrer/désenregistrer des handlers. C’est tout.

Je crois qu’il faut bien faire la différence entre ce qu’est un event par définition, et ce que le compilateur génère par défaut pour nous aider. On peut très bien déclarer un event sans avoir de backing field, comme c’est aussi le cas pour les propriétés. Un-event-c’est-deux-méthodes.

Bon.

Après ce petit rappel, on devrait se sentir un peu plus à l’aise pour commencer l’énumération inutile de tout ce qui distingue plus ou moins évidemment un event d’un Champ Public De Type Delegate (CPDTD).

Protection

Lorsque vous exposez un CPDTD, n’importe qui est capable de le référencer et de le manipuler en tant que tel. Ce n’est pas le cas lorsque vous déclarez un event, puisque seules deux méthodes d’ajout/suppression de handler sont exposées. Du coup, l’invocation d’un event est restreinte : seul le type qui porte l’évènement peut déclencher ce dernier, les autres types devant se contenter de fournir leurs handlers en utilisant modestement les accesseurs.

Attention, même les types dérivés perdent le privilège d’invocation; on devra donc la plupart du temps prévoir une méthode protected pour contourner la limitation.

Interfaces

Puisqu’un event-c’est-deux-méthodes, il peut apparaitre dans une interface, alors qu’un CPDTD, non. C’est très important, et très logique. Il est compréhensible en effet qu’on puisse s’engager à « exposer » deux méthodes dans un contrat, mais pas un champ.

Polymorphisme

Puisqu’un event-c’est-deux-méthodes, toujours, on peut parler de polymorphisme, et appliquer aux events les modifiers qu’il est habituellement possible d’appliquer aux méthodes. Implicitement, ils concerneront les deux accesseurs add et remove. Cela signifie qu’un event peut être abstract, virtual ou même sealed. De même, on peut overrider ou redéfinir un event à l’aide des mots clés override et new.

Pas le droit lol

Le fait qu’un event ait toujours des accesseurs (add/remove) implique que les fonctions correspondantes existent déjà. Ainsi, il est impossible d’écrire :

public event EventHandler Click;

public void add_Click(EventHandler handler) {} // Erreur : signature existante
public void remove_Click(EventHandler handler) {} // Erreur : signature existante

Opérateurs

Lorsqu’utilisés avec les delegates, les opérateurs += et -= ne sont pas interprétés comme la combinaison condensée d’une addition/soustraction + une affectation. le type Delegate ne surcharge d’ailleurs ni l’opérateur +, ni l’opérateur -. En fait, le compilateur remplace += et -= par les appels respectifs aux méthodes statiques Delegate.Combine et Delegate.Remove, qui retournent de nouvelles instances de délégués. (Un délégué est immutable)

Par contre, lorqu’on utilise += et -= pour ajouter/supprimer un handler à un event, le compilateur travaille différemment et se charge d’appeller directement les accesseurs de l’évènement.

Si Click est un CPDTD :

button.Click += OnClick;
// Compilé en :
// button.Clik = (EventHandler) Delegate.Combine(button.Clik, new EventHandler(OnClick));

Si Click est un event :

button.Click += OnClick;
// Compilé en :
// button.add_Click(new EventHandler(OnClick));

Warnings

Puisqu’il est uniquement possible de déclencher un évènement à l’intérieur de la classe qui contient sa déclaration, le compilateur est capable de lever un warning lorsqu’il ne détecte aucune invocation de l’évènement.

Evaluation

En C#, une affectation est une expression évaluable. On peut écrire, par exemple :

int i = 12;
int j = i = 6; // (i = 6) est évalué, de type int, donc le code est valide et j vaudra 6.

De même, lorsque Click est un CPDTD, il est possible décrire :

EventHandler handler = button.Click += OnClick;

Car l’expression button.Click += OnClick est de type EventHandler.

Mais dans le cas où Click est un event, button.Click += OnClick est de type void, du coup le code précédent ne compile pas.

Reflection

Si on veut accéder à un CPDTD par la réflexion, on utilise la méthode GetField ou GetFields. Mais comme les events sont des animaux bien différents, il existe les méthodes GetEvent et GetEvents, qui retournent des instances d’EventInfo. J’ai insisté là dessus précedemment : la notion d’event n’est liée à celle de backing field que parce qu’il s’agit d’une association constatée dans 99% des implémentations, mais c’est tout. Il est impossible d’optenir les métadonnées relatives au backing field d’un event à partir de son EventInfo, tout comme il est impossible d’obtenir les métadonnées relatives au backing field d’une propriété à partir de son PropertyInfo.

Enfin si, c’est possible, mais chut.

Interop

Si vous kiffez COM, sachez que les CPDTD ne vous seront d’aucune utilité au cas où vous chercheriez à interopérer avec des évènements COM. Il faudra forcément utiliser des events. Et ne me dites pas que c’est évident, où je vous tue.

Doudounotte

Microsoft propose une guideline relative aux events :

…the delegate type used for an event should take two parameters, an « object source » parameter indicating the source of the event, and an « e » parameter that encapsulates any additional information about the event. The type of the « e » parameter should derive from the EventArgs class. For events that do not use any additional information, the .NET Framework has already defined an appropriate delegate type: EventHandler.

Ce n’est qu’un encouragement, hein, vous êtes libre d’avoir des events de type Action<Func<Dictionary<string, List<uint>>, Guid>, bool> si vous le souhaitez. Néanmoins, l’idée est louable, puisque si le conseil est suivi, la contravariance permettrait à la méthode suivante de devenir un handler universel :

public void OnEvent(object sender, EventArgs e)
{
}

Je suppose que c’est l’ambition très secrète de toutes les méthodes du monde. En tout cas, si j’étais une méthode, l’idée me séduirait.

Synchronisation

Pour les events, lorsqu’on laisse le compilateur générer le backing field et les accesseurs, ces derniers seront automatiquement synchronisés. Le compilateur associera en effet l’attribut MethodImpl avec l’option MethodImplOptions.Synchronized aux méthodes add et remove, ce qui est fonctionnellement équivalent à prendre un lock sur l’instance ou le type, selon si l’event est ou non statique.

Par contre, si vous préférez fournir votre propre implémentation pour les accesseurs, ce sera à vous de prévoir la synchronisation en utilisant un lock manuellement, si nécessaire.

Merci Patrice !

Menteur

Si un event n’est que deux méthodes, comment se fait-il alors, gros malin, que l’on puisse écrire et compiler :

public event EventHandler Click;

public void ICanHazInvokashun()
{
    Click(this, EventArgs.Empty);
}

En effet, un évènement est bien invocable. Ca voudrait dire que le compilateur est capable de tricher, et d’appeler sournoisement la méthode Invoke sur le champ privé caché ? Et bien oui. Le pseudo IL du corps de ICanHazInvokashun ressemble à :

ldarg.0 
ldfld class System.EventHandler Button::Click
ldarg.0 
ldsfld class System.EventArgs System.EventArgs::Empty
callvirt instance void System.EventHandler::Invoke(object, class System.EventArgs)
ret

A la ligne 2 on constate que c’est bien un champ (ldfld = load field) qui est poussé sur la pile et sur lequel on appelle la méthode Invoke (ligne 5). A quoi d’autre pouvions-nous nous attendre ?

Si on décide de fournir une implémentation différente pour add et remove, le compilateur ne devrait plus être en mesure de permettre l’invocation directe de l’évènement avec la même syntaxe :

public event EventHandler Click
{
    add { /* rien */ } remove { /* pas mieux */ }
}

public void ICanHazInvokashun()
{
    Click(this, EventArgs.Empty); // Haha j't'ai foolé, sale compiler.
}

Effectivement, il n’est plus possible de compiler la ligne 8, et on obtient le message d’erreur généralement réservé aux utilisateurs du type qui expose l’évènement :

The event ‘Button.Click’ can only appear on the left hand side of += or -=

Voilà. Certes, c’était un post un peu facile, et tout ça n’est pas très excitant. Mais après tout, qu’est-ce qui peut bien être excitant en C# ? On parle d’un langage de programmation ! Alors à moins d’être un fanboy, etc.

Tagged with: ,

16 Réponses

Subscribe to comments with RSS.

  1. Rubix said, on août 17, 2009 at 8:32

    N’aurait-il point été plus court, ainsi que moins lassant, de juste répondre à « Donc, à quoi ça sert, le mot clé event ? » par « Bah a pouvoir ajouter plusieurs callbacks indépendamment, comme dans l’pattern Observer », et de zapper les détails techniques que personne n’utilise ?

    • Romain Verdier said, on août 17, 2009 at 9:02

      — A quoi ça sert, le mot clé event ?
      — Bah a pouvoir ajouter plusieurs callbacks indépendamment, comme dans l’pattern Observer.
      — Oui, mais on peut pas faire ça sans le mot clé event ? En écrivant juste, par exemple :
        public EventHandler Click;

  2. DarkHerumor said, on août 17, 2009 at 8:36

    Très bon article, je pense que ce genre de rappel / précision, est souvent minimisé. Mais on se rend réellement compte des implications de chacune des définitions ici.
    En tout cas j’avoue avoir éclaircis une zone d’ombre dans mon esprit :)

  3. andreone said, on août 17, 2009 at 8:42

    Moi non plus je ne finit pas excité, mais ça fait une bonne présentation détaillée du sujet et en bon noob c#, j’apprécie.

    Sinon, c’est quoi une Doudounotte?

  4. grozeille said, on août 18, 2009 at 6:27

    Ce genre de poste se laisse bien lire :) Le temps me parait moins long dans le métro…
    J’adore !

  5. Evilz said, on août 18, 2009 at 9:37

    Voilà ! C’est le retour du grand Romain, un bon post provocateur mais qui nous rapelle les bases et un peu plus.

    @Rubix & andreone : Comme le dis Romain à la fin, je ne pense pas que ce genre de post peut « excité » à moins d’être un fanboy, et ce n’est pas son but. Pour ça y a des sites spécialisés ;)

  6. Patrice Lamarche said, on août 18, 2009 at 3:30

    C’est bizarre, j’étais persuadé du contraire au niveau de ton chapitre Synchronisation. Tu es sûr qu’il ne faut pas ne pas oublier de mettre des locks lorsque l’on définit les accesseurs add/remove ??

    • Romain Verdier said, on août 18, 2009 at 3:59

      T’as parfaitement raison, je n’ai pas été très clair.

      Lorsqu’on utilise un « field-like event », en laissant le compilateur générer le backing field et les accesseurs, ces derniers seront synchronisés — l’attribut sus-cité est ajouté automatiquement.

      Par contre, lorsqu’on décide d’écrire sa propre implémentation pour le add et le remove, on fait effectivement ce qu’on veut. Si on a besoin que les accesseurs soient synchronisés, alors il faudra prendre un lock explicitement car le compilateur ne s’en chargera pas par défaut.

      Merci pour ta relecture, je vais modifier l’article en conséquence :)

  7. keitaro said, on août 19, 2009 at 2:58

    C’est un bon article pour ceux qui découvrent C# (et pour ceux qui ne sont pas payés à la ligne de code).

    Sinon ça reste du C# (il fallait bien la placer)

  8. F said, on août 27, 2009 at 10:50

    ZZZZZZZzzzzzzzzzzzzzzzzzzzzzzzzZZZZZZZZZZzzzzzzzzzzzzzz …

    En espérant ne pas être désagréable, tocard…;)

  9. Pistache said, on décembre 11, 2009 at 3:46

    J’ai trouvé ça très très drôle, Romain. Bravo !

  10. Jmix90 said, on mars 15, 2010 at 2:21

    Très drôle !

  11. lep said, on juin 25, 2011 at 2:38

    Excellent j’ai bien ri et appris des choses :)

  12. Saien said, on décembre 3, 2013 at 9:55

    Excellent article, 4 ans après et il sert toujours :D

  13. Formation Web said, on avril 12, 2014 at 5:15

    Super citation « Ben alors, ça sert à quoi le terme Event? Réponse: t’es relou, dégage. » je ne m’attendais pas à pouffer de rire en essayant de m’instruire sur C#.
    Merci!

  14. […] trouver des articles sur le net comme la doc MSDN, ou sur des blogs comme celui de Romain Verdier (codingly.com) dont j’aime bien le […]


Votre commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l’aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l’aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s

%d blogueurs aiment cette page :