Ein denkwürdiges Ereignis

Veröffentlicht: 09. Dez 2001 | Aktualisiert: 18. Jun 2004

Von Eric Gunnerson

Rückblick: Letzten Monat sprachen wir über Leistung und Boxing. Eine Frage eines Lesers lautete, warum die Perl-Version so viel schneller als die C#-Version sei. Eine Antwort darauf wäre, dass Perl seine Aufgabe sehr gut erfüllt. Ein Leser wies darauf hin, dass ich vergessen hätte, den Regex anzuweisen, den regulären Ausdruck zu kompilieren und nicht bei jeder Übereinstimmung zu interpretieren. In der Beta-Version 1 geschieht dies, indem "c" als zweiter Parameter an den Regex-Konstruktor weitergegeben wird (es gibt eine Auflistung, dies auch in Beta 2 zu tun). Dies halbiert den Overhead nahezu und reduziert die Ausführungsdauer der schnellsten Version auf etwas weniger als 7 Sekunden.

Auf dieser Seite

Delegates und Ereignisse
Delegates
Weitergabestatus
Multicasting
Ereignisse
Websites

Delegates und Ereignisse

Gegenstand der Kolumne in diesem Monat sind Delegates und Ereignisse, genau genommen nur Delegates, weil Ereignisse nächsten Monat behandelt werden. Ich beginne mit einer Erläuterung, was Delegates sind, was sie leisten und wie sie eingesetzt werden. Zu den Ereignissen kommen wir dann nächsten Monat.

Delegates

Wenn wir eine Funktion aufrufen, geben wir in den meisten Fällen an, dass sie direkt aufgerufen wird. Wenn also die MyClass-Klasse über eine Funktion namens Process verfügt, rufen wir sie normalerweise wie folgt auf:

MyClass myClass = new MyClass(); 
myClass.Process();

Dies ist in den meisten Fällen auch erfolgreich. Manchmal möchten wir jedoch eine Funktion gar nicht direkt aufrufen, sondern sie an andere Benutzer weitergeben können, die sie dann aufrufen. Dies ist besonders hilfreich im Fall eines ereignisgesteuerten Systems, z.B. einer grafischen Benutzeroberfläche, wenn ein Code ausgeführt werden soll, sobald ein Benutzer auf eine Schaltfläche klickt, oder wenn ich Daten protokollieren möchte, aber die Protokollierungsweise nicht angeben kann.

Beachten Sie die folgende Codeanweisung:

public class MyClass 
{ 
   public void Process() 
   { 
      Console.WriteLine("Process() begin"); 
      // Weitere Eingaben hier. 
      Console.WriteLine("Process() end"); 
   } 
}

In dieser Klasse protokollieren wir, wann die Funktion beginnt und wann sie endet. Unglücklicherweise geht unsere Protokollierung aber fest verdrahtet nur an die Konsole, was wir u.U. gar nicht möchten. Was wir wirklich wollen, ist eine Möglichkeit der funktionsexternen Steuerung, wo die Daten protokolliert werden, ohne dass der Funktionscode dadurch komplizierter wird.

Ein Delegate eignet sich hierzu hervorragend. Durch ihn können wir angeben, wie die aufzurufende Funktion aussieht, ohne angeben zu müssen, welche Funktion aufgerufen werden soll. Die Deklaration für einen Delegate sieht genau so aus wie die Deklaration für eine Funktion, nur dass wir die Signatur der Funktionen, auf die der Delegate verweist, deklarieren müssen.

In unserem Beispiel deklarieren wir einen Delegate, der einen einzigen Zeichenfolgeparameter benötigt und über keinen Rückgabetyp verfügt. Die Klasse wird wie folgt geändert:

public class MyClass 
{ 
   public delegate void LogHandler(string message); 
   public void Process(LogHandler logHandler) 
   { 
      if (logHandler != null) 
         logHandler("Process() begin"); 
      // Weitere Eingaben hier. 
      if (logHandler != null) 
         logHandler ("Process() end"); 
   } 
}

Die Verwendung eines Delegates funktioniert genau so wie das direkte Aufrufen einer Funktion, obwohl wir vor dem Aufrufen der Funktion noch prüfen müssen, ob der Delegate Null ist (d.h., nicht auf eine Funktion verweist).

Zum Aufrufen der Process()-Funktion müssen wir eine Protokollierungsfunktion deklarieren, die dem Delegate entspricht. Anschließend müssen wir eine Instanz des Delegates erstellen, die auf die Funktion verweist. Dieser Delegate wird dann an die Process()-Funktion weitergegeben.

class Test 
{ 
   static void Logger(string s) 
   { 
      Console.WriteLine(s); 
   } 
   public static void Main() 
   { 
      MyClass myClass = new MyClass(); 
      MyClass.LogHandler lh = new MyClass.LogHandler(Logger); 
      myClass.Process(lh); 
   } 
}

Wir möchten die Logger()-Funktion aus der Process()-Funktion aufrufen. Sie ist so deklariert, dass sie dem Delegate entspricht. In Main() erstellen wir eine Instanz des Delegates und geben den Delegatekonstruktor an die Funktion weiter, auf die er verweisen soll. Abschließend geben wir den Delegate an die Process()-Funktion weiter, die dann die Logger()-Funktion aufrufen kann.

Als Kenner von C++ werden Sie wahrscheinlich denken, dass ein Delegate einem Funktionszeiger ähnelt, und Sie haben nicht ganz unrecht. Ein Delegate ist aber nicht nur ein Funktionszeiger, sondern liefert auch Funktionalität.

Weitergabestatus

In dem einfachen Beispiel oben schreibt die Logger()-Funktion die Zeichenfolge lediglich aus. Eine andere Funktion möchte vielleicht die Informationen in einer Datei protokollieren, muss aber dazu wissen, in welche Datei die Informationen geschrieben werden sollen.

In Win32 können Sie bei der Weitergabe eines Funktionszeigers auch gleichzeitig den Status übergeben. In C# jedoch ist dies nicht erforderlich, da Delegates auf statische Funktionen oder auf Member-Funktionen verweisen können. Das folgende Beispiel zeigt, wie auf eine Member-Funktion verwiesen wird:

class FileLogger 
{ 
   FileStream fileStream; 
   StreamWriter streamWriter; 
   public FileLogger(string filename) 
   { 
      fileStream = new FileStream(filename, FileMode.Create); 
      streamWriter = new StreamWriter(fileStream); 
   } 
   public void Logger(string s) 
   { 
      streamWriter.WriteLine(s); 
   } 
   public void Close() 
   { 
      streamWriter.Close(); 
      fileStream.Close(); 
   } 
} 
class Test 
{ 
   public static void Main() 
   { 
      FileLogger fl = new FileLogger("process.log"); 
      MyClass myClass = new MyClass(); 
      MyClass.LogHandler lh = new MyClass.LogHandler(fl.Logger); 
      myClass.Process(lh); 
      fl.Close(); 
   } 
}

Die FileLogger-Klasse schließt die Datei einfach mit ein. Main() wird derart geändert, dass der Delegate auf die Logger()-Funktion in der fl-Instanz eines FileLogger verweist. Wenn Sie diesen Delegate aus der Process()-Funktion starten, wird die Member-Funktion aufgerufen und die Zeichenfolge in der entsprechenden Datei protokolliert.

Das Gute an der Sache ist, dass wir die Process()-Funktion nicht ändern müssen. Der Code zum gesamten Delegate ist identisch, gleichgültig, ob er auf eine statische Funktion oder eine Member-Funktion verweist.

Multicasting

Auf Member-Funktionen zeigen zu können, ist sehr nützlich, aber Delegates können noch mehr. In C# sind Delegates mehrfach besetzbar (multicast), d.h., dass sie auf mehr als eine Funktion gleichzeitig zeigen können (sie basieren auf dem System.MulticastDelegate-Typ). Ein Multicastdelegate unterhält eine Liste von Funktionen, die aufgerufen werden, wenn der Delegate gestartet wird. Diese können der Protokollierungsfunktion aus dem ersten Beispiel hinzugefügt werden. Beide nennen sich Delegates. Wir verwenden die Delegate.Combine()-Funktion, um zwei Delegates zu kombinieren. Der entsprechende Code sieht folgendermaßen aus:

      MyClass.LogHandler lh = (MyClass.LogHandler)  
         Delegate.Combine(new Delegate[]  
            {new MyClass.LogHandler(Logger), 
            new MyClass.LogHandler(fl.Logger)});

Das scheint kompliziert. C# liefert eine einfachere Syntax und zwingt dem Benutzer nicht die komplizierte Syntax auf. Anstatt die Delegate.Combine()-Funktion aufzurufen, verwenden Sie lediglich den Operator +=, um die beiden Delegates zu verbinden:

      MyClass.LogHandler lh = null; 
      lh += new MyClass.LogHandler(Logger); 
      lh += new MyClass.LogHandler(fl.Logger);

Dies ist eine elegantere Lösung. Sie entfernen einen Delegate aus einem Multicastdelegate, indem Sie die Delegate.Remove()-Funktion aufrufen oder den Operator -= verwenden (ich wüsste, welche Option ich nähme).

Wenn Sie einen Multicastdelegate aufrufen, werden die Delegates in der Aufrufliste synchron in der Reihenfolge ihres Auftretens gestartet. Sollte während dieses Vorgangs ein Fehler auftreten, wird die Ausführung unterbrochen.

Wenn Sie die Aufrufreihenfolge noch stärker steuern möchten (z.B. für einen garantierten Aufruf), rufen Sie die Aufrufliste bei dem Delegate ab, und starten Sie die Funktionen selbst. Dies sieht folgendermaßen aus:

foreach (LogHandler logHandler in lh.GetInvocationList()) 
{ 
   try 
   { 
      logHandler(message); 
   } 
   catch (Exception e) 
   { 
      // etwas mit der Ausnahme machen. 
   } 
}

Der Code packt jeden Aufruf in einen Try- oder Catch-Handler. Wird eine Ausnahme in einen Handler gelegt, verhindert dies nicht, dass andere Handler aufgerufen werden.

Ereignisse

Nach einigen Bemerkungen zu Delegates gehen wir nun zu den Ereignissen über. Eine nahe liegende Frage lautet: "Warum brauchen wir Ereignisse, wenn wir doch Delegates haben?"

Dies kann am besten beantwortet werden, wenn wir ein Ereignis in einem Benutzeroberflächenobjekt betrachten. Eine Schaltfläche z.B. könnte einen öffentlichen Click-Delegate besitzen. Wir könnten an diesen Delegate eine Funktion hängen. Die Schaltfläche riefe dann den Delegate auf, sobald jemand auf die Schaltfläche klickt. Wir könnten folgenden Code schreiben:

   Button.Click = new Button.ClickHandler(ClickFunction);

Dies bedeutet, dass ClickFunction() aufgerufen wird, wenn auf die Schaltfläche geklickt wird.

Frage: Gibt es ein Problem bei dem oben stehenden Code? Was haben wir vergessen?

Wir haben vergessen, den Operator += zu verwenden und haben stattdessen den Delegate direkt zugewiesen. Das heißt, jeder weitere Delegate, der an

Button.Click

angehängt wurde, ist jetzt abgehängt.

Button.Click

muss öffentlich sein, damit andere Objekte darauf zugreifen können. Das oben dargestellte Szenario kann nicht abgewendet werden. Auf gleiche Weise kann ein Benutzer folgenden Code schreiben, um einen Delegate zu entfernen:

   Button.Click = null;

Damit würden alle Delegates entfernt werden.

Dies ist ungünstig, da in vielen Fällen nur ein Delegate angehängt ist und das Problem somit nicht als Bug erkennbar wird. Werden später weitere Delegates angehängt, funktioniert die Sache nicht mehr.

Ereignisse legen einen "Schutzmantel" um das Delegatemodell. Beispiel eines Objekts, das ein Ereignis unterstützt:

public class MyObject 
{ 
   public delegate void ClickHandler(object sender, EventArgs e); 
   public event ClickHandler Click; 
   protected void OnClick() 
   { 
      if (Click != null) 
         Click(this, null); 
   } 
}

Der ClickHandler-Delegate legt die Signatur des Ereignisses fest und verwendet dazu das Vorgabemuster für Ereignisdelegates. Der Name endet auf "handler" und hat zwei Parameter. Der erste Parameter ist das Objekt, das das Objekt gesendet hat; der zweite Parameter dient zur Weitergabe der Informationen in Verbindung mit dem Ereignis. In diesem Fall sind keine Informationen weiterzugeben. EventArgs wird direkt verwendet, aber eine aus EventArgs abgeleitete Klasse (z.B. MouseEventArgs) wird verwendet, falls Informationen weiterzugeben sind.

Die Deklaration des Click-Ereignisses macht Folgendes: Zunächst deklariert sie die Elementvariable Click, die aus der Klasse heraus verwendet wird. Dann deklariert sie ein Ereignis namens Click, das - je nach den üblichen Zugriffsregeln - außerhalb der Klasse verwendet werden kann (in diesem Fall ist das Ereignis öffentlich).

Eine Funktion wie OnClick() ist normalerweise eingeschlossen, so dass der Typ bzw. die abgeleiteten Typen das Ereignis auslösen können. Sie werden bemerken, dass der zur Ereignisauslösung verwendete Code der gleiche Code wie für Delegates ist, denn Click ist ein Delegate.

Wie beim Delegate erfolgt das An- und Abhängen an bzw. von einem Ereignis mit += und -=, aber - anders als beim Delegate - sind dies die einzigen Vorgänge, die bei einem Ereignis ausgeführt werden können. Dadurch wird ausgeschlossen, dass die beiden oben genannten Fehler eintreten können.

Die Verwendung eines Ereignisses ist unkompliziert.

class Test 
{ 
   static void ClickFunction(object sender, EventArgs args) 
   { 
      // Ereignis hier verarbeiten. 
   } 
   public static void Main() 
   { 
      MyObject myObject = new MyObject(); 
      myObject.Click += new MyObject.ClickHandler(ClickFunction);       
   } 
}

Wir erstellen eine statische Funktion oder eine Member-Funktion, die mit der Signatur des Delegates übereinstimmt. Dann fügen wir dem Ereignis die neue Instanz des Delegates mit += hinzu.

Hiermit beenden wir den Artikel für diesen Monat. Im nächsten Monat sprechen wir über die Art und Weise, wie Ereignisse intern funktionieren, sowie über fortgeschrittene Implementierungsvorgänge (Tipp: Ereignisse unterscheiden sich manchmal von Delegates durch zusätzlichen Schutz). Wenn Sie gespannt sind und es nicht mehr abwarten können: Der Beispielcode enthält den Code vom nächsten Monat.

Websites

Unglücklicherweise sind alle diesen Monat an meinen Laptop gesendeten Sites verloren gegangen. Falls Sie mir einen URL gesendet haben, ihn aber nicht auf der Site entdecken konnten, senden Sie ihn bitte nochmals an ericgu@microsoft.com.

Download

Beispielcode für diesen Artikel