Events in .Net

Iets waar ik me al sinds ik het tegenkwam over verbaasd heb is de standaardmanier waarop in C#/.Net events afgehandeld worden. Dat is nog niet zo lang geleden (zo gaat dat als Junior Programmeur(tm)) maar nu ik inmiddels gedetacheerd ben en op een al lopend project ben ingezet merk ik het nog eens extra. Volgens mij kan het veel beter.

Hoe zat het ook alweer? Als ik een object obj heb dat een event simpleEvent kan afvuren, dan kan ik daarop reageren door een methode met de juiste signatuur aan dat event toe te voegen:

...
obj.simpleEvent += new
   EventHandler(reactToSimpleEvent);
...
public void reactToSimpleEvent (object
    sender, EventArgs e)
{
     // code
}

Als nu het simpleEvent afgevuurd wordt, wordt de code in reactToSimpleEvent uitgevoerd. Hierbij wordt obj dan geacht (maar dit wordt niet afgedwongen) om zichelf als sender mee te geven.

Alle voorgedefinieerde events die ik ben tegengekomen zijn op deze leest geschoeid: de uit te voeren code is steeds een methode met return type void, en argumenten sender van type object en nog een tweede argument, van type EventArgs of een subtype daarvan. In dat tweede argument worden dan de bij het event horende gegevens doorgegeven.

Ik vraag me nu twee dingen af: ten eerste, waarom heeft men EventArgs uitgevonden? MSDN zegt over EventArgs:

This class contains no event data; it is used by events that do not pass state information to an event handler when an event is raised. If the event handler requires state information, the application must derive a class from this class to hold the data.

En inderdaad, als we kijken naar de klassebeschrijving dan zien we dat de klasse alleen een lege constructor definieert, en een static field EventArgs.Empty, met als beschrijving: “Represents an event with no event data.”

Dus… wacht’es even… we geven data mee om aan te geven dat we geen data hebben om mee te geven…?!

Hebben we daar een hele nieuwe klasse voor nodig? What happened to good old null? En trouwens, wat voegt EventArgs toe aan Object als’ie toch geen enkele functionaliteit heeft? Juist het feit dat er zo’n dummy object wordt meegestuurd, betekende dat toen ik de code in dat project wou factoriseren, ik eerst moest nagaan of niemand het event aanriep met een ad hoc subklasse object.

Nou zou je kunnen zeggen dat die mogelijkheid nou juist wel nuttig zou kunnen zijn – maar daar ben ik het niet mee eens: sowieso kan, als je dan toch de functie-argumenten misbruikt, die data net zo goed in het eerste argument (object sender) worden verpakt – dat het ding sender is genoemd en dat je daar als event-gooier alleen maar this in propt is ook maar een conventie. En verder: het feit dat het argument EventArgs als type heeft, geeft – zoals het er nu voorstaat – expliciet aan dat je verwacht dat er geen meegegeven data zal zijn. Als je dat toch doet, dan breekt dat die expliciete verwachting. Ik denk dat het breken van verwachtingen juist een belangrijke oorzaak is van bugs. Als je data wilt meegeven aan je events, dan zal je die ook van de juiste MyEventHandler-declaratie moeten voorzien.

Het tweede wat ik me van .Net-events afvroeg is het volgende. Aangezien je toch onder controle hebt wat de signatuur van het event is, waarom maakt .Net daar dan geen gebruik van? Nu hebben alle eventhandlers precies twee argumenten, waarvan de 2e alle data van het event bevat – en dat betekent dat er voor elk type event een aparte subklasse van zowel EventArgs als van EventHandler is. Als we onze eigen events op dezelfde menier in elkaar zetten, zullen we dus ook steeds per event twee van zulke klassen moeten schrijven. Sinds 2.0 is er een generieke versie EventHandler<TEventArgs> met TEventArgs : EventArgs, maar dat lost dus maar de (kleinste) helft van het probleem op: de argumentklasse TEventArgs moeten we nog steeds zelf maken.

Het enige wat die subklassen van EventArgs doen, is meerdere argumenten voor de eventhandler samenpakken in één dik groot functionaliteitloos object. Waarom niet gewoon een eigen MyEventHandler die al die argumenten gewoon los specificeert…? Dus niet dit:

  // definieer argumentklasse
class MyEventArgs : EventArgs 
{
  public string beschrijving;
  public int ID;
  public DateTime tijdstip;
  public Point plaats;
    // ...en nog andere data...
}


class MyEventThrower 
{
  public delegate void MyEventHandler(
      object sender, MyEventArgs args
  );

  public event MyEventHandler myEvent;
  
  ...
  if (myEvent != null) 
  {    
      // pak argumenten in
      MyEventArgs args = new MyEventargs(
          beschr, id, tijd, pl, enz);

      // gooi event
      myEvent(this, args);
  }
  ...
}

Maar gewoon direct zo:

  // geen argumentklasse nodig

class MyEventThrower 
{

  public delegate void MyEventHandler (
      object sender,
      string beschrijving, int ID,
      DateTime tijdstip, Point plaats
        // ...andere data ook 'inline'...
  );

  public event MyEventHandler MyEvent;

  ...
  if (MyEvent != null) 
  {
        // gooi event direct
      MyEvent(this, besc, id, tijd, pl, enz);
  }
  ...
}

De afhandeling van events zonder data komt er dan gewoon uit te zien als public delegate void EventHandler(object sender)zonder nutteloze EventArgs. Door het op deze manier te doen scheelt het per eventtype tenminste een klasse om te schrijven, en je hoeft niet, elke keer voordat je een event wilt gooien, je data eerst in te pakken in zo’n zelfontworpen MyEventArgs-object.

Zelfs in de 14e eeuw wist de monnik Willem van Ockham het al: entia non sunt multiplicanda praeter necessitatem, oftewel entities should not be multiplied beyond necessity. Laten we dat in de 21e eeuw dus vooral ook niet doen.