Delegates vs. Events

February 28, 2008 11:22 by Andre Loker

Manche Fragen tauchen mit schöner Regelmäßigkeit in Foren etc. auf. Eine davon lautet:

Was ist der Unterschied zwischen Ereignissen (Events) und Delegaten (Delegates)?

Beispiel:

   1: delegate void MyDelegate( string message );
   2:  
   3: class MyClass {
   4:     public MyDelegate MyDelegate;
   5:     public event MyDelegate MyEvent;
   6: }

MyClass.MyDelegate ist ein Delegate, MyClass.MyEvent entsprechend ein Event. Der Unterschied zwischen den beiden besteht hauptsächlich in den Zugriffsrechten außerhalb der Klasse MyClass.

Wir können drei Operationen festhalten, die für delegates bzw. events wichtig sind:

  • Hinzufügen/Entfernen von Event-Handlern (mit den += und -= Operatoren)
  • Direktes Zuweisen eines Wertes (mit dem = Operator)
  • Auslösen des Events/aufrufen der Event-Handler (mit dem ()-Operator)

Der Unterschied zwischen Event und Delegate lässt sich mit folgender Tabelle leicht veranschaulichen:

Operation Event Delegate
Innerhalb MyClass    
  Hinzufügen/Entfernen mit += und -= erlaubt erlaubt
  Zuweisen mit = erlaubt erlaubt
  Auslösen mit () erlaubt erlaubt
Außerhalb MyClass    
  Hinzufügen/Entfernen mit += und -= erlaubt erlaubt
  Zuweisen mit = nicht erlaubt erlaubt
  Auslösen mit () nicht  erlaubt erlaubt

 

Mit "innerhalb" MyClass sind Methoden gemeint, die MyClass selbst definiert. Von MyClass abgeleitete Klassen werden in dieser Tabelle wie "außerhalb MyClass" behandelt.

Code-Beispiel:

   1: using System;
   2:  
   3: delegate void MyDelegate( string message );
   4:  
   5: class MyClass {
   6:     public MyDelegate MyDelegate;
   7:     public event MyDelegate MyEvent;
   8: }
   9:  
  10: class Program {
  11:     static void Main() {
  12:         MyClass mc = new MyClass();
  13:  
  14:         // DELEGATES
  15:         // Subscribe
  16:         mc.MyDelegate += HandleTheEvent; // OK
  17:         // Unsubscribe
  18:         mc.MyDelegate -= HandleTheEvent; // OK
  19:         // Clear all subscribers
  20:         mc.MyDelegate = null; // OK
  21:         // Invoke subscribers
  22:         mc.MyDelegate( "Hello, world!" ); // OK
  23:  
  24:         // EVENTS
  25:         // Subscribe
  26:         mc.MyEvent += HandleTheEvent; // OK
  27:         // Unsubscribe
  28:         mc.MyEvent -= HandleTheEvent; // OK
  29:         // Clear all subscribers
  30:         mc.MyEvent = null; // NOT ALLOWED, Cpmpiler error
  31:         // Invoke subscribers
  32:         mc.MyEvent( "Hello, world!" ); // NOT ALLOWED, Compiler error
  33:     }
  34:  
  35:     private static void HandleTheEvent( string message ) {
  36:         Console.WriteLine( message );
  37:     }
  38: }

Zusammengefasst:

Der wesentliche Unterschied bei der Handhabung von Delegates und Events besteht darin, dass außerhalb der Klasse, die das Event bzw. den Delegaten definiert, das direkte Zuweisen und das Auslösen eines Events nicht erlaubt ist, während beides bei einem Delegaten gesstattet ist.

Oft wird nun gefragt, was man nehmen sollte: Events oder Delegates. Wenn man Ereignisse anbieten möchte, wird man verständlicherweise zu Events greifen. Ein öffentlicher Delegat bricht die Kapselung der Klasse viel zu sehr auf. Ein Event hingegen bietet eine wohldefinierte begrenzte Schnittstelle zur Außenwelt an. Zudem erlauben Events mehr Feinkontrolle, wie der nächste Abschnitt zeigt.

Details

Es gibt - gerade in technischer Hinsicht - natürlich einen größeren Unterschied zwischen Events und Delegaten. Dazu muss man wissen, wie ein Event für die CLR aussieht. Bei der Definition des Events

   1: public event MyDelegate MyEvent;

erzeugt der C# Compiler in etwa folgenden Code:

   1: private MyDelegate _MyEvent;
   2:  
   3: public void add_MyEvent(MyDelegate value)
   4: {
   5:     this.MyEvent = (MyDelegate) Delegate.Combine(this.MyEvent, value);
   6: }
   7:  
   8: public void remove_MyEvent(MyDelegate value)
   9: {
  10:     this.MyEvent = (MyDelegate) Delegate.Remove(this.MyEvent, value);
  11: }

Hinweis: In Wirklichkeit haben das Feld und das Event den selben Namen (MyEvent). Zur Veranschaulichung verwende ich aber _MyEvent für das Feld und MyEvent für das Event.

In Wahrheit erzeugt die Definition des Events also ein privates Feld vom Typ des Delegaten und zwei öffentliche Wrapper-Methoden, um sich bei dem Delegaten an- bzw. abzumelden. Die += und -= Operator für das Event werden vom Compiler zu Aufrufen der add_MyEvent und remove_MyEvent Methoden kompiliert. Das erklärt, warum dies die einzigen beiden Operationen sind, die außerhalb der MyClass-Klasse erlaubt sind. Da _MyEvent private ist, erklärt sich auch, warum selbst von MyClass abgeleitete Klassen lediglich += und -= verwenden dürfen.

Auch das Auslösen des Events ist zunächst syntaktischer Zucker:

   1: MyEvent( "foo" );

wird zu

   1: _MyEvent( "foo" );

In manchen Fällen möchte man das Standardverhalten des C#-Compilers (erstellen eines privaten Feldes des Delegaten-Types und Erzeugen der einfachen add und remove-Methoden) überschreiben. In der .NET Klassenbibliothek wird das z.B. bei den Windows-Forms oft gemacht, da ein Control viele Events hat, die bei weitem aber nicht alle verwendet werden. Würde man hier das Standardverhalten verwenden, würde für jedes Event ein Feld angelegt, das entsprechend Platz pro Instanz belegt.

Um das Verhalten zu überschreiben, kann man in C# für ein Event explizit die add- und remove-Methoden angeben. Hier ein komplexeres Beispiel, wie man mehrere Events zentral handhaben kann, indem man mit Hilfe eines Dictionary-Feldes die Buchführung für Events selber übernimmt:

   1: class MyClass {
   2:     // keys to identify events in the dictionary
   3:     private static readonly object Event1Key = new object();
   4:     private static readonly object Event2Key = new object();
   5:     private static readonly object Event3Key = new object();
   6:     private static readonly object Event4Key = new object();
   7:     // stores all active events
   8:     private readonly Dictionary<object, Delegate> events = new Dictionary<object, Delegate>();
   9:  
  10:     public event EventHandler Event1 {
  11:         add { AddHandler( Event1Key, value ); }
  12:         remove { RemoveHandler( Event1Key, value ); }
  13:     }
  14:  
  15:     public event EventHandler Event2 {
  16:         add { AddHandler( Event2Key, value ); }
  17:         remove { RemoveHandler( Event2Key, value ); }
  18:     }
  19:  
  20:     public event EventHandler Event3 {
  21:         add { AddHandler( Event3Key, value ); }
  22:         remove { RemoveHandler( Event3Key, value ); }
  23:     }
  24:  
  25:     public event EventHandler Event4 {
  26:         add { AddHandler( Event4Key, value ); }
  27:         remove { RemoveHandler( Event4Key, value ); }
  28:     }
  29:  
  30:     // add a handler using the dictionary
  31:     private void AddHandler( object key, Delegate value ) {
  32:         Delegate old;
  33:         if ( events.TryGetValue( key, out old ) ) {
  34:             // Handler exists, combine old with new
  35:             events[ key ] = Delegate.Combine( old, value );
  36:         } else {
  37:             // Handler does not exist
  38:             events[ key ] = value;
  39:         }
  40:     }
  41:  
  42:     // remove a handler using the dictionary
  43:     private void RemoveHandler( object key, Delegate value ) {
  44:         Delegate old;
  45:         if ( events.TryGetValue( key, out old ) ) {
  46:             // Handler exists, remove it
  47:             events[ key ] = Delegate.Remove( old, value );
  48:         }
  49:     }
  50:  
  51:     // raises a specific event
  52:     protected void Raise( object key, object sender, EventArgs e ) {
  53:         Delegate handler;
  54:         if ( events.TryGetValue( key, out handler ) ) {
  55:             // assume we use a delegate format similar to EventHandler, 
  56:             // i.e.: it expects (object sender, EventArgs e) as arguments
  57:             handler.DynamicInvoke( sender, e );
  58:         }
  59:     }
  60:  
  61:     // Some method that raises an event
  62:     public void SomeMethod() {
  63:         // ...
  64:         // Raise Event1
  65:         Raise( Event1Key, this, EventArgs.Empty );
  66:         // Raise Event3
  67:         Raise( Event3Key, this, EventArgs.Empty );
  68:     }
  69: }

Wichtig hierbei:

  • Es macht Sinn, das Hinzufügen/Entfernen der Handler zum/vom Dictionary in eigene Methoden auszulagern (AddHandler/RemoveHandler)
  • Man kann Events, die eigene add/remove-Methoden implementieren nicht mehr über EventName() auslösen. Statt dessen muss hier der entsprechende Delegat im Verzeichnis gefunden und aufgerufen werden. Da das Verzeichnis hier sehr allgemeingültig ist (die Delegaten werden als Delegate-Referenzen gespeichert), verwende ich hier Delegate.DynamicInvoke. Wäre der Typ des Delegaten klar definiert (z.b. EventHandler), könnte auch der normale Aufruf mittels () verwendet werden. Die Variante mit DynamicInvoke hat den Vorteil, dass unterschiedlichste Eventhandler-Delegaten verwendet werden, solange sie alle vergleichbare Parameter haben. Das erklärt übrigens auch, warum Microsoft das Format HandlerDelegate(object sender, EventArgs e) forciert.
  • Die Verwendung von Dictionary ist sicherlich nicht die platzsparendste Variante, dazu hat die Klasse einfach zu viel verwaltungstechnischen Overhead. Das .NET Framework bietet für diese Zwecke eine sehr schlanke Klasse an: System.ComponentModel.EventHandlerList (Beispiel). Die Variante mit Dictionary soll nur veranschaulichen, wie das Konzept grundsätzlich funktioniert.

Die Verwendung der Events ändert sich dadurch aber nicht:

   1: class Program {
   2:     private static void Main() {
   3:         MyClass mc = new MyClass();
   4:         // use as always
   5:         mc.Event1 += MyClassHandler;
   6:         mc.Event2 += MyClassHandler;
   7:         mc.Event3 += MyClassHandler;
   8:         mc.Event4 += MyClassHandler;
   9:         mc.SomeMethod();
  10:     }
  11:  
  12:     private static void MyClassHandler( object sender, EventArgs e ) {
  13:         Console.WriteLine( "Event raised" ); 
  14:     }
  15: }