So setzen Sie Delegates in .NET ein

Veröffentlicht: 18. Nov 2001 | Aktualisiert : 18. Jun 2004

Von Jeffrey Richter

Mit den Delegates des .NET-Frameworks lassen sich typsichere Callback-Mechanismen auf objektorientierte Weise implementieren. Die Flexibilität wird durch die Verkettung von Delegates noch erweitert.

Auf dieser Seite

Aufruf statischer Methoden via Delegates Aufruf statischer Methoden via Delegates
Aufruf von Instanzmethoden Aufruf von Instanzmethoden
Entmystifizierte Delegates Entmystifizierte Delegates
Verkettung von Delegates Verkettung von Delegates
Die Geschichte der Delegates Die Geschichte der Delegates
Delegatesketten Delegatesketten
Die Delegatesketten-Unterstützung in C# Die Delegatesketten-Unterstützung in C#
Aufruf einer Delegateskette Aufruf einer Delegateskette
Fazit Fazit

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

sys

Der Rückruf von Funktionen gehört zweifellos zu den nützlichsten Mechanismen, die je für die Programmierung erdacht wurden. Die Funktion qsort aus der C-Laufzeitbibliothek erwartet zum Beispiel einen Zeiger auf eine Vergleichsfunktion, mit deren Hilfe die Datenelemente in einem Array sortiert werden. Das Programm ruft qsort auf und qsort ruft mit Hilfe des übergebenen Zeigers die passenden Vergleichsfunktionen auf. In Windows werden Callbacks für Fensterfunktionen, Abfangfunktionen (Hooks), asynchrone Prozeduraufrufe und derlei Dinge mehr gebraucht.

Auch im .NET Framework werden für eine ganze Reihe von Vorgängen Rückrufmethoden eingesetzt. Sie können Callbacks einrichten, wenn Sie Hinweisnachrichten über das Laden und Entladen von Baugruppen (Assemblies) brauchen, über nicht bearbeitete Ausnahmen, über Zustandsänderungen in Datenbanken, in Fenstern oder im Dateisystem, über die Auswahl von Menüpunkten, über den Abschluss von asynchron durchgeführten Arbeiten und so weiter.

In C/C++ ist die Adresse einer Funktion einfach eine Speicheradresse, die keine zusätzlichen Informationen liefert, wie zum Beispiel die Zahl der Funktionsparameter, die Parametertypen, den Ergebnistyp der Funktion oder die benutzte Aufrufkonvention. Kurz gesagt, der übliche Rückruf mit Hilfe eines einfachen Funktionszeigers ist nicht typsicher.

Das .NET Framework bietet einen speziellen, typsicheren Mechanismus für Rückrufe an, der auf den Namen "Delegates" hört. Ich möchte meine Diskussion der Delegates mit der Frage beginnen, wie man sie benutzt. Der Code im Listing L1 zeigt, wie man Delegates deklariert, anlegt und einsetzt.

L1 DelegateDemo.cs

using System; 
    // In der Beta2: System.Windows.Forms 
using System.WinForms;  
using System.IO; 
class Set { 
   private Object[] items; 
   public Set(Int32 numItems) { 
      items = new Object[numItems]; 
      for (Int32 i = 0; i < numItems; i++) 
         items[i] = i; 
   } 
   // Definiere einen Feedback-Typ für Rückmeldungen 
   // (Beachte: dieser Typ liegt in der Set-Klasse) 
   public delegate void Feedback( 
      Object value, Int32 item, Int32 numItems); 
   public void ProcessItems(Feedback feedback) { 
      for (Int32 item = 0; item < items.Length; item++) { 
         // Rückruffunktion angegeben? Dann rufe sie auf. 
         if (feedback != null) { 
            feedback(items[item], item + 1, items.Length); 
         } 
      } 
   } 
} 
class App { 
   [STAThreadAttribute] 
   static void Main() { 
      StaticCallbacks(); 
      InstanceCallbacks(); 
   } 
   static void StaticCallbacks() { 
      // Lege eine Menge mit 5 Elementen an. 
      Set setOfItems = new Set(5); 
      // Bearbeite die Elemente ohne Rückmeldung. 
      setOfItems.ProcessItems(null); 
      Console.WriteLine(); 
      // Bearbeite die Elemente mit Rückmeldung auf der 
      // Konsole. 
      setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToConsole)); 
      Console.WriteLine(); 
      // Bearbeite die Elemente mit Rückmeldung in einem 
      // Meldungsfenster. 
      setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToMsgBox)); 
      Console.WriteLine(); 
      // Bearbeite die Elemente mit Rückmeldung in der 
      // Konsole UND in einem Meldungsfenster. 
      Set.Feedback fb = null; 
      fb += new Set.Feedback(App.FeedbackToConsole); 
      fb += new Set.Feedback(App.FeedbackToMsgBox); 
      setOfItems.ProcessItems(fb); 
      Console.WriteLine(); 
   } 
   static void FeedbackToConsole( 
      Object value, Int32 item, Int32 numItems) { 
      Console.WriteLine("Processing item {0} of {1}: {2}.",  
         item, numItems, value); 
   } 
   static void FeedbackToMsgBox( 
      Object value, Int32 item, Int32 numItems) { 
      MessageBox.Show(String.Format("Processing item {0} of {1}: {2}.", 
        item, numItems, value)); 
   } 
   static void InstanceCallbacks() { 
      // Lege eine Menge mit 5 Elementen an. 
      Set setOfItems = new Set(5); 
      // Bearbeite die Elemente mit Rückmeldung in einer 
      // Datei. 
      App appobj = new App(); 
      setOfItems.ProcessItems(new Set.Feedback(appobj.FeedbackToFile)); 
      Console.WriteLine(); 
   } 
   void FeedbackToFile( 
      Object value, Int32 item, Int32 numItems) { 
      StreamWriter sw = new StreamWriter("Status", true); 
      sw.WriteLine("Processing item {0} of {1}: {2}.",  
         item, numItems, value); 
      sw.Close(); 
   } 
}

Besonders interessant ist die Set-Klasse am Anfang von Listing L1. In diesem Beispiel enthält die Klasse stellvertretend einige Elemente, die einzeln zu bearbeiten sind. Bei der Erstellung eines Set-Objekts übergeben Sie die Zahl der Elemente, die zur Menge gehören, an den Konstruktor. Der Konstruktor legt dann ein passendes Array an und initialisiert es mit der entsprechenden Anzahl von Integerwerten.

Außerdem definiert die Set-Klasse einen öffentlichen Delegate. Der Delegate definiert die Signatur der dazugehörigen Rückrufmethode. Im Beispiel definiert der Delegate Feedback eine Methode, die drei Parameter hat (ein Object, ein Int32 und ein weiteres Int32) und keinen Ergebniswert (void). In gewisser Weise ähnelt ein Delegate einem typedef von C/C++, mit dem die Adresse einer Funktion definiert wird (anders gesagt, einem Funktionszeiger).
Darüber hinaus definiert die Set-Klasse eine öffentliche Methode namens ProcessItems. Diese Methode hat einen Parameter namens feedback, bei dem es sich um eine Referenz auf ein Feedback-Delegateobjekt handelt. ProcessItems zählt die einzelnen Elemente des Arrays auf und ruft für jedes einzelne Element die Rückruffunktion in feedback auf. Beim Aufruf erhält diese Methode den Wert des zu bearbeitenden Elements, die Nummer des Elements und die Gesamtzahl der Elemente, die im fraglichen Array zu finden sind. Nun steht es der Rückrufmethode frei, das betreffende Element so zu bearbeiten, wie sie es für richtig hält.

Aufruf statischer Methoden via Delegates

Die Methode StaticCallbacks demonstriert die verschiedenen Arten, in denen man Delegates einsetzen kann. Die Methode beginnt mit der Erstellung eines Set-Objekts für fünf Elemente. Dann ruft sie ProcessItems mit null als Feedback-Argument auf. Das ist das erste Beispiel dafür, wie man Delegates benutzen kann. ProcessItems ist eine Methode, die für die Bearbeitung jedes Elements aus dem Set sorgt. Da der Feedback-Parameter in diesem Fall null ist, erfolgt die Bearbeitung ohne den Aufruf irgendwelcher Rückrufmethoden.

Für das zweite Beispiel wird ein neues Set.Feedback-Delegateobjekt angelegt. Das Delegateobjekt ist eine Art Verpackung oder Hülle für eine Methode und erlaubt den indirekten Aufruf der Methode mit Hilfe dieser Hülle. An den Konstruktor des Typs Feedback wird der Name einer Methode übergeben, in diesem Fall App.FeedbacktoConsole. Das ist der Name der Methode, die speziell verpackt werden soll. Die Referenz, die der new-Operator liefert, wird an ProcessItems weitergegeben. Anschließend ruft ProcessItems für jedes einzelne Element aus der Menge die App-Methode FeedbackToConsole auf. FeedbackToConsole schreibt einfach einen kurzen Text auf die Konsole, aus dem hervorgeht, welches Element bearbeitet wird und welchen Wert es hat.
Das dritte Beispiel ist im Wesentlichen mit dem zweiten identisch. Der einzige Unterschied besteht darin, dass das Feedback-Delegateobjekt die Methode App.FeedbackToMsgBox einhüllt. FeedbackToMsgBox stellt einen kurzen Text zusammen, aus dem hervorgeht, welches Element bearbeitet wird und welchen Wert das Element hat. Dieser Text wird dann in einem Meldungsfenster angezeigt.

Das vierte und letzte Beispiel zeigt, wie sich Delegates verketten lassen (Bleibt man beim Vergleich mit dem klassischen Funktionszeiger, so stellt diese Verkettung eine Liste mit Funktionszeigern dar.). In diesem Beispiel wird eine Referenzvariable fb auf ein Feedback-Delegateobjekt angelegt und mit null initialisiert. Diese Variable zeigt auf den Kopf einer verketteten Liste von Delegates. Der Wert null zeigt an, dass es in der Liste noch keine Einträge gibt. Anschließend wird ein Feedback-Delegateobjekt angelegt, das die Methode App.FeedbackToConsole einhüllt. Mit dem C#-Operator += wird dieses Objekt in die verkettete Liste fb eingetragen. Nun verweist die Variable fb auf den Kopf der verketteten Liste.

Schließlich wird noch ein weiteres Feedback-Delegateobjekt angelegt, das die Methode App.FeedbackToMsgBox einhüllt. Auch dieses Objekt wird wieder mit dem C#-Operator += in die verkettete Liste aufgenommen und die Variable fb wird aktualisiert, damit sie auf den neuen Listenkopf verweist. Beim folgenden ProcessItems-Aufruf wird der Kopf der verketteten Liste mit den Feedback-Delegates an die aufgerufene Funktion übergeben. In ProcessItems ruft die Codezeile, die für den Aufruf der Rückrufmethoden zuständig ist, der Reihe nach alle Rückrufmethoden auf, die von den Delegateobjekten in der verketteten Liste angesprochen werden. Anders gesagt, für jedes zu bearbeitende Element wird erst FeedbackToConsole aufgerufen und dann FeedbackToMsgBox. Weiter unten werde ich noch genauer erklären, wie die Delegateskette funktioniert.

Wichtig ist an dieser Stelle, dass alle Schritte in diesem Beispiel typsicher sind. Wenn Sie zum Beispiel ein Feedback-Delegateobjekt anlegen, überprüft der Compiler, ob die App-Methoden FeedbackToConsole und FeedbackToMsgBox auch den vom Feedback-Objekt geforderten Prototypen haben. Beide Methoden müssen dieselben Parameter haben (Object, Int32 und Int32) und auch denselben Ergebnistyp, in diesem Fall also void. Stimmen die Prototypen nicht damit überein, wird sich der Compiler mit folgender Fehlermeldung beschweren: "Error CS0123: The signature of method 'App.FeedbackToMsgBox()' does not match this delegate type."

Aufruf von Instanzmethoden

Bisher war davon die Rede, wie man statische Methoden mit Hilfe von Delegates aufruft. Allerdings lassen sich mit den Delegates auch Instanzmethoden von bestimmten Objekten aufrufen. Dafür muss der Delegate natürlich wissen, welche Objektinstanz von der Methode bearbeitet werden soll.

Die Methode InstanceCallbacks in Listing L1 zeigt, wie der Rückruf in eine Instanzmethode funktioniert. Der Code hat große Ähnlichkeit mit dem Aufruf der statischen Methoden. Nach der Erstellung des Set-Objekts wird hier auch ein App-Objekt angelegt wird. Dieses App-Objekt hat weder Felder noch irgendwelche Properties und wird nur zu Demonstrationszwecken angelegt. Zur Erstellung des Delegateobjekts erhält der Feedback-Konstruktor ein appobj.FeedbackToFile. Der Delegate hüllt also eine Referenz auf die Methode FeedbackToFile ein. Bei FeedbackToFile handelt es sich um eine Instanzmethode (nicht statisch). Wird diese Methode aufgerufen, so wirkt sie auf das Objekt, das hier appobj genannt wird (es wird im verborgenen this-Parameter übergeben). Im Prinzip funktioniert FeedbackToFile wie die Methoden FeedbackToConsole und FeedbackToMsgBox, nur öffnet die Methode eine Datei und hängt die Beschreibung des bearbeiteten Elements ans Ende der Datei an.

Entmystifizierte Delegates

Bei oberflächlicher Betrachtung scheint der Umgang mit den Delegates wirklich sehr einfach zu sein: Man definiert sie mit dem C#-Schlüsselwort delegate, legt mit dem bekannten new-Operator eine Instanz an und ruft die dahinterstehende Funktion mit der Syntax auf, die für den Methodenaufruf üblich ist (statt des Methodennamens benutzen Sie dabei eine Variable, die sich auf das Delegateobjekt bezieht).

Tatsächlich ist das Geschehen aber wesentlich komplexer, als es nach den Beispielen den Anschein hat. Die Compiler und die CLR (common language runtime) geben sich große Mühe, diese zusätzliche Komplexität hinter der Bühne zu verbergen. In diesem Abschnitt möchte ich näher auf die Zusammenarbeit zwischen Compiler und CLR eingehen, die zur Implementierung der Delegates erforderlich ist. Dieses Wissen wird Ihnen dabei helfen, die Delegates besser zu verstehen und sie effizient und effektiv einzusetzen. Außerdem kommen dabei einige zusätzliche Leistungen der Delegates zur Sprache, auf die Sie in Ihrem Code zurückgreifen können.
Beginnen wir mit folgender Codezeile:

public delegate void Feedback( 
   Object value, Int32 item, Int32 numItems); 

Sobald der Compiler auf diese Zeile stößt, stellt er eine vollständige Klassendefinition zusammen, die ungefähr wie der Code aus Listing L2 aussieht.

L2 Die Klasse Feedback

public class Feedback : System.MulticastDelegate { 
   // Konstruktor 
   public Feedback(Object target, Int32 methodPtr); 
   // Methode mit demselben Prototyp, der im  
   // Quelltext angegeben wurde. 
   public void virtual Invoke( 
      Object value, Int32 item, Int32 numItems); 
   // Die folgenden Methoden erlauben den asynchronen  
   // Aufruf der fraglichen Methode. Auf diese Methoden 
   // komme ich in einem zukünftigen Artikel noch zurück. 
   public virtual IAsyncResult BeginInvoke( 
      Object value, Int32 item, Int32 numItems,  
      AsyncCallback callback, Object object); 
   public virtual void EndInvoke(IAsyncResult result); 
}

Sie können sich selbst davon überzeugen, dass der Compiler tatsächlich automatisch diese Klasse generiert hat, indem Sie mit dem ILDasm.exe das resultierende Modul untersuchen (Bild B1).

In diesem Beispiel hat der Compiler eine Klasse namens Feedback definiert, die sich vom Typ System.MulticastDelegate ableitet. Dieser Typ wiederum ist in der Framework Class Library zu finden. Alle Delegatestypen werden von MulticastDelegate abgeleitet. In diesem Beispiel ist die Klasse Feedback öffentlich, weil der Delegate im Quelltext als public deklariert wird. Hätte der Quelltext ihn geschützt (protected) oder zur Privatsache erklärt (private), so wäre auch die vom Compiler generierte Feedback-Klasse protected oder private. Delegatestypen können nicht nur innerhalb einer Klasse definiert werden (im Beispiel wird Feedback innerhalb der Klasse Set definiert), sondern auch mit globaler Gültigkeit. Da es sich bei Delegates im Prinzip um Klassen handelt, können sie überall dort definiert werden, wo man auch Klassen definieren darf.

Da alle Delegatestypen von MulticastDelegate abgeleitet werden, erben sie natürlich auch die Felder, Properties und Methoden von MulticastDelegate. Unter all diesen Bestandteilen der Klasse gibt es drei private Felder, die man kennen sollte (Tabelle T1).

T1 Private Felder in Delegatestypen

Feld

Typ

Beschreibung

_target

System.Object

Bezieht sich auf das Objekt, auf welches der Methodenaufruf wirken soll. Wird für Aufrufe von Instanzmethoden benutzt.

_methodPtr

System.Int32

Eine interne Integer, die von der CLR zur Identifizierung der aufzurufenden Methode benutzt wird.

_prev

System.MulticastDelegate

Verweist auf ein anderes Delegateobjekt. Ist normalerweise null.

Alle Delegates haben einen Konstruktor mit zwei Parametern, nämlich eine Referenz auf ein Objekt und ein Integer, mit dem die Rückrufmethode angegeben wird. Wie Sie im Quelltext des Beispiels gesehen haben, ruft man den Konstruktor aber mit Angaben wie App.FeedbackToConsole oder appobj.FeedbackToFile auf. Dem gesunden Menschenverstand nach dürfte sich der Code eigentlich gar nicht kompilieren lassen.

Nun weiß der Compiler aber, dass es um die Erstellung eines Delegates geht. Also analysiert er den Quelltext und findet heraus, welches Objekt und welche Methode gemeint sind. Für den target-Parameter wird eine Referenz auf das betreffende Objekt übergeben und für den methodPtr-Parameter ein spezieller Int32-Wert, der die gewünschte Methode benennt und anhand der MethodDef- oder MethodRef-Metadaten ermittelt wird. Bei statischen Methoden wird eine null als Argument für target übergeben. Innerhalb des Konstruktors werden diese beiden Parameter in den entsprechenden privaten Feldern gespeichert.
Außerdem setzt der Konstruktor das Feld _prev auf null. Dieses Feld dient zur Erstellung einer verketteten Liste von MulticastDelegate-Objekten. Ich möchte allerdings erst später näher auf das Feld _prev eingehen.

Im Prinzip kann man jedes Delegateobjekt daher als eine spezielle Verpackung für eine Methode und das dazugehörige Objekt betrachten, auf welches die Methode wirken soll. Die Klasse MulticastDelegate definiert zwei schreibgeschützte öffentliche Instanzproperties, nämlich Target und Method. Sobald eine Referenz auf ein Delegateobjekt zur Verfügung steht, können Sie diese Properties abfragen. Das Target-Property liefert eine Referenz auf das Objekt, auf welches die Methode nach ihrem Aufruf wirken soll. Handelt es sich um eine statische Methode, liefert Target natürlich null. Das Method-Property liefert ein System.Reflection.MethodInfo-Objekt, das die zuständige Rückrufmethode nennt.

Diese Informationen lassen sich auf verschiedene Weisen auswerten. So können Sie damit zum Beispiel überprüfen, ob sich ein Delegateobjekt tatsächlich auf eine Instanzmethode eines bestimmten Typs bezieht:

Boolean DelegateRefersToInstanceMethodOfType( 
   MulticastDelegate d, Type type) { 
   return((d.Target != null) && d.Target.GetType == type); 
}

Außerdem können Sie überprüfen, ob die aufzurufende Methode einen bestimmten Namen trägt (zum Beispiel FeedbackToMsgBox):

Boolean DelegateRefersToMethodOfName( 
   MulticastDelegate d, String methodName) { 
   return(d.Method.Name == methodName); 
}

Lassen Sie uns, nachdem nun geklärt ist, wie die Delegateobjekte gebaut werden, über den Aufruf der Rückrufmethode sprechen. Der Einfachheit halber möchte ich hier den ProcessItems-Code von Set wiederholen:

public void ProcessItems(Feedback feedback) { 
   for (Int32 item = 1; item <= items.Length; item++) { 
         // Rückruffunktion angegeben? Dann ruf sie auf. 
      if (feedback != null) { 
         feedback(items[item], item, items.Length); 
      } 
   } 
}

Direkt unter der if-Abfrage steht die Codezeile, die den Aufruf der Methode bewirkt. Bei sorgfältiger Untersuchung hat es den Anschein, als rufe ich tatsächlich eine Funktion namens feedback auf und übergebe ihr beim Aufruf drei Argumente. Allerdings gibt es in diesem Beispiel gar keine Funktion mit dem Namen feedback. Auch hier gilt wieder, dass der Compiler weiß, dass es sich bei feedback um eine Variable handelt, die sich auf ein Delegateobjekt bezieht. Folglich generiert der Compiler den Code, der für den Aufruf der Invoke-Methode vom Delegateobjekt erforderlich ist. Der Compiler sieht also folgende Zeile:

feedback(items[item], item, items.Length);

Er generiert den Code aber so, als lautete die Zeile folgendermaßen:

feedback.Invoke(items[item], item, items.Length);

Auch das können Sie im ILDasm.exe überprüfen, indem Sie sich den Code der Methode ProcessItems anschauen.

08Delegate02

B2 ProcessItems im disassemblierten IL-Code.

Bild B2 zeigt den MSIL-Code der Set-Methode ProcessItems. Der rote Pfeil verweist auf einen Befehl, der die Invoke-Methode von Set.Feedback aufruft. Wenn Sie den Code ändern, um Invoke explizit aufzurufen, wird sich der C#-Compiler mit einer Fehlermeldung beschweren: "Error CS1533: Invoke cannot be called directly on a delegate." C# lässt den direkten Invoke-Aufruf nicht zu (bei anderen Compilern kann es durchaus anders aussehen).

Der Compiler hatte diese Invoke-Methode definiert, als der die Feedback-Klasse zusammenstellte. Wird Invoke aufgerufen, so benutzt diese Methode die privaten Felder _target und _methodPtr für den Aufruf der gewünschten Methode des Zielobjekts. Die Signatur der Invoke-Methode passt genau zur Signatur des Delegates. Da mein Feedback- Delegate drei Argumente erwartet und void zurückgibt, erwartet die Invoke-Methode dieselben drei Argumente und speist den Aufrufer ebenfalls mit void ab.

Verkettung von Delegates

Wie Sie nun wissen, veranlasst die Deklaration eines Delegates den Compiler, eine Klasse zu generieren, die von System.MulticastDelegate abgeleitet wird. Jede Instanz dieser Klasse enthält die beiden privaten Felder _target und _methodPtr, aus denen hervorgeht, welche Methode mit dem Delegate aufgerufen wird. Außerdem hat jeder Delegate ein drittes privates Feld mit Namen _prev, das zur Verkettung mehrerer Delegates zu einer Liste dient. Ich möchte mich nun auf das _prev-Feld konzentrieren und auf die Frage, wie solche Ketten von Delegates aufgestellt und benutzt werden.

Die Geschichte der Delegates

(System.Delegate and System.MulticastDelegate)

Die Klassenbibliothek vom .NET Framework definiert die Klasse System.MulticastDelegate. Das ist die Klasse, von der schon einmal die Rede war. MulticastDelegate selbst wird von der Klasse System.Delegate abgeleitet, die ebenfalls in der Klassenbibliothek zu finden ist und selbst wiederum von System.Object abgeleitet wird.

Beim ersten Entwurf des .NET Frameworks stellten die Microsoft-Ingenieure schon den Bedarf an zwei verschiedenen Arten von Delegates fest, nämlich einfache (single-cast) und verkettbare (multicast). Von MulticastDelegate abgeleitete Typen lassen sich verketten, während die Typen, die von Delegate abgeleitet werden, nicht verkettet werden können. Der Typ System.Delegate wurde als Basistyp konzipiert. Diese Klasse implementiert die gesamte Funktionalität, die zum Aufruf der eingehüllten Methode erforderlich ist. Die Klasse MulticastDelegate wird von Delegate abgeleitet und hat zusätzlich die Eigenschaft, dass sich MulticastDelegate-Objekte verketten (in verkettete Listen eintragen) lassen.

Beim Kompilieren des Quelltextes überprüft der Compiler die Signatur des Delegates und wählt für die zu definierende Delegateklasse die passende Basisklasse aus. Für Methoden, die keine Ergebnisse an den Aufrufer zurückgeben (void), leitet der Compiler die Delegateklasse von System.MulticastDelegate ab, während er die Delegateklasse für Methoden, die Ergebnisse liefern, auch in Form von ref und out-Parametern, von System.Delegate ableitet. Das ist durchaus sinnvoll, weil der Aufrufer nur den Rückgabewert von der letzten aufgerufenen Methode aus der Delegateskette erhält.

Während des Beta-Tests des .NET Frameworks wurde schnell deutlich, dass es die Arbeit nicht gerade erleichtert, wenn sich die Entwickler mit zwei verschiedenen Basisklassen herumschlagen müssen. Außerdem erhalten die Delegates bei diesem Konzept auch willkürlich gewählte Beschränkungen. Zum Beispiel geben viele Methoden Ergebniswerte zurück, die man in vielen Situationen verwerfen kann. Da diese Methoden aber Ergebnisse liefern (nicht void), werden sie nicht von der Klasse MulticastDelegate abgeleitet und lassen sich daher nicht zu Listen verketten.

Um die Ursache der Verwirrung zu beheben, wollten die Microsoft-Entwickler die Klassen MulticastDelegate und Delegate zu einer einzigen Klasse zusammenfassen, damit sich prinzipiell alle Delegateobjekte verketten lassen. Die Compiler würden die Delegates dann von dieser einen Basisklasse ableiten. Diese Änderung würde die Komplexität etwas verringern und nicht nur dem Entwickler die Arbeit erleichtern, sondern auch dem .NET Framework-Team, dem CLR-Team (common language runtime), den Compilerteams und auch den Drittanbietern, die mit Delegates arbeiten.

Leider kam die Idee mit der Zusammenfassung der Klassen Delegate und MulticastDelegate erst relativ spät im Entwicklungszyklus vom .NET Framework auf. Also blieben die Klassen in der Beta 1 vom .NET Framework noch getrennt. In einer künftigen Versionen des .NET Frameworks werden sie sicherlich zu einer einzigen Klasse zusammengefasst.

Obwohl Microsoft also die Zusammenlegung dieser beiden Klassen in der Klassenbibliothek vom .NET Framework noch hinauszögert, hat man alle Microsoft-Compiler so abgeändert, dass sie die Delegatestypen jetzt stets von der Klasse MulticastDelegate ableiten. Ich habe also nicht gelogen, als ich weiter oben sagte, alle Delegatestypen würden von MulticastDelegate abgeleitet. Durch diese Änderung in den Compilern lassen sich alle Instanzen der Delegatetypen zu Listen verketten, und zwar unabhängig von den Rückgabewerten der betreffenden Methoden.

Warum muss man sich überhaupt mit solchen Problemen beschäftigen? Nun, wenn Sie sich mit den Delegates anfreunden und sie mehr und mehr in den eigenen Programmen einsetzen, werden Sie in der Dokumentation vom .NET Framework SDK sicherlich auf die Typen Delegate und MulticastDelegate stoßen. Mir geht es einfach darum, dass Sie sich von den Beziehungen zwischen diesen beiden Klassen nicht verwirren lassen. Obwohl alle Delegates, die Sie definieren, MulticastDelegate als Basisklasse haben, werden Sie zudem gelegentlich Ihre Typen mit Methoden bearbeiten, die nicht von der Klasse MulticastDelegate definiert werden, sondern von Delegate.

So gibt es in der Klasse Delegate zum Beispiel statische Methoden mit den Namen Combine und Remove (Was diese Methoden tun, werde ich später noch erläutern.). Aus den Signaturen dieser beiden Methoden geht hervor, dass sie Delegate-Parameter haben. Da Ihr Delegatetyp von der Klasse MulticastDelegate abgeleitet wird, die sich wiederum von Delegate ableitet, dürfen auch Instanzen von Ihrem Delegatetyp an die Methoden Combine und Remove übergeben werden.

08Delegate034

B3 Delegates lassen sich verketten.

Vergleich der Delegates auf Gleichheit
Die Basisklasse Delegate überschreibt die virtuelle Methode Equals von ihrer Basisklasse Object. Daher erbt der MulticastDelegate-Typ die Equals-Implementierung von Delegate. Die Equals-Implementierung von Delegate überprüft die Gleichheit zweier Delegateobjekte, indem sie überprüft, ob deren Felder _target und _methodPtr auf dasselbe Objekt und auf dieselbe Methode verweisen. Stimmen diese Felder überein, gibt Equals true zurück, andernfalls false. Die folgenden Zeilen sollen das demonstrieren:

// Konstruiere 2 Delegateobjekte, die auf dasselbe 
// Ziel und dieselbe Methode verweisen. 
Feedback fb1 = new Feedback(FeedbackToConsole); 
Feedback fb2 = new Feedback(FeedbackToConsole); 
// Obwohl fb1 und fb2 eigentlich zwei verschiedene Objekte 
// bezeichnen, beziehen sie sich beide auf dasselbe Ziel 
// und auf dieselbe Methode. 
Console.WriteLine(fb1.Equals(fb2));   // Zeigt "True"

Außerdem bieten sowohl Delegate als auch MulticastDelegate Überladungen für die Gleichheits- (==) und Ungleichheitsoperatoren (!=) an. Daher können Sie statt der Equals-Methode auch diese Operatoren benutzen. Der folgende Code führt somit zum selben Ergebnis wie der obige:

// Konstruiere 2 Delegateobjekte, die auf dasselbe 
// Ziel und dieselbe Methode verweisen. 
Feedback fb1 = new Feedback(FeedbackToConsole); 
Feedback fb2 = new Feedback(FeedbackToConsole); 
// Obwohl fb1 und fb2 eigentlich zwei verschiedene Objekte 
// bezeichnen, beziehen sie sich beide auf dasselbe Ziel 
// und auf dieselbe Methode. 
Console.WriteLine(fb1 == fb2);   // Zeigt "True"

Wie man Delegates auf Gleichheit testet, wird insbesondere dann wichtig, wenn man damit beginnt, Delegatesketten zu manipulieren. Das ist das Thema des nächsten Abschnitts.

Delegatesketten

Wie Sie selbst im Laufe der Zeit feststellen werden, sind schon einzelne Delegates ungemein nützlich. Zudem lassen sie sich auch verketten, was ihre Nützlichkeit noch steigert. Weiter oben habe ich schon erwähnt, dass jedes MulticastDelegate-Objekt ein privates Feld mit Namen _prev hat. Dieses Feld ist für eine Referenz auf ein weiteres MulticastDelegate-Objekt vorgesehen. Jedes Objekt vom Typ MulticastDelegate oder von einem Typ, der von MulticastDelegate abgeleitet ist, kann also eine Referenz auf ein MulticastDelegate-Objekt oder auf ein davon abgeleitetes Objekt aufnehmen. Dieses Feld ermöglicht die Verkettung der Delegateobjekte zu Listen.
Zur Manipulation solcher verketteter Listen mit Delegateobjekten bietet die Klasse Delegate drei statische Methoden an:

class System.Delegate { 
   // Kombiniert die Listen, die durch head und tail repräsentiert 
   // werden. Zurückgegeben wird head (head ist 
   // der zuletzt aufgerufene Delegate). 
   public static Delegate Combine(Delegate tail, Delegate head); 
   // Erstellt aus dem Delegatesarray eine verkettete Liste 
   // (Eintrag 0 ist der Kopf und wird als letzter 
   // Delegate aufgerufen). 
   public static Delegate Combine(Delegate[] delegateArray); 
   // Entfernt den Delegate aus der Kette, der zum angegebenen  
   // Ziel/Methode passt. Der neue Kopf wird zurückgegeben und 
   // als letzter Delegate aufgerufen. 
   public static Delegate Remove(Delegate source, Delegate value); 
}

Wenn Sie ein neues Delegateobjekt anlegen, wird dessen _prev-Feld auf null gesetzt. Von ihm geht also keine verkettete Liste aus. Zur Verkettung zweier Delegates zu einer Liste rufen Sie eine der statischen Combine-Methoden von Delegate auf:

Feedback fb1 = new Feedback(FeedbackToConsole); 
Feedback fb2 = new Feedback(FeedbackToMsgBox); 
Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2); 
// Die linke Seite von Bild B3 zeigt, wie die Kette nach 
// der Ausführung des obigen Codes aussieht. 
App appobj = new App(); 
Feedback fb3 = new Feedback(appobj.FeedbackToStream); 
fbChain = (Feedback) Delegate.Combine(fbChain, fb3);

Bild B3 zeigt, wie die Kette nach der Ausführung des Codes aussieht. In der Dokumentation können Sie sich davon überzeugen, dass der Typ Delegate noch eine weitere Version der Combine-Methode anbietet, die als Argument ein Array mit Referenzen auf Delegate-Objekte erwartet. Mit dieser Combine-Version könnte man den obigen Code folgendermaßen formulieren:

Feedback[] fbArray = new Feedback[3]; 
fbArray[0] = new Feedback(FeedbackToConsole); 
fbArray[1] = new Feedback(FeedbackToMsgBox); 
App appobj = new App(); 
fbArray[2] = new Feedback(appobj.FeedbackToStream); 
Feedback fbChain = Delegate.Combine(fbArray);

Wird ein Delegate aufgerufen, so generiert der Compiler den Code für den Aufruf der Invoke-Methode des Delegates, wie weiter oben beschrieben. Als Beispiel benutzte ich dort einen Delegate vom Typ Feedback:

public delegate void      
   Feedback( 
   Object value, Int32 item, Int32 numItems);

Das veranlasste den Compiler, eine Feedback-Klasse zu definieren, die eine Invoke-Methode hat. Der folgende Pseudocode zeigt, wie man sich das vorstellen kann:

class Feedback : MulticastDelegate { 
   public void virtual Invoke( 
      Object value, Int32 item, Int32 numItems) { 
      // Wenn es noch andere Delegates in der Kette gibt, 
      // die vorher aufgerufen werden sollen, dann tu es. 
      if (_prev != null) _prev.Invoke(value, item, numItems); 
      // Rufe nun die gewünschte Methode des Zielobjekts auf. 
     _target.methodPtr(value, item, numItems); 
   } 
}

Wie Sie sehen, wird beim Aufruf eines Delegateobjekts zuerst der vorige Delegate aus der Liste aufgerufen. Nach der Rückkehr des vorigen Delegates zum Aufrufer wird der Rückgabewert verworfen. Anschließend kann der Delegate die Methode aufrufen, für die er selbst zuständig ist. Der Code aus Listing L3 demonstriert das.

L3 Aufruf eines Delegateobjekts

Feedback fb1 = new Feedback(FeedbackToConsole); 
Feedback fb2 = new Feedback(FeedbackToMsgBox); 
Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2); 
// An diesem Punkt verweist fbChain auf einen Delegate, 
// der FeedbackToMsgBox aufruft. Dieser Delegate 
// verweist auf einen weiteren Delegate, der Feed- 
// backToConsole aufruft. Dieser zweite Delegate  
// verweist auf keinen weiteren Delegate. 
// Nun rufen wir den Delegate vom Listenkopf auf. 
// Intern ruft dieser seinen Vorgänger auf, und so 
// weiter. 
if (fbChain != null) fbChain(null, 0, 10); 
// Bitte beachten: in fbChain ist in der obigen Zeile  
// zwar niemals null, aber die Prüfung einer Delegates- 
// referenz auf null vor dem Aufruf ist keine schlechte 
// Gewohnheit.

Bisher habe ich Ihnen aber nur Beispiele mit meinem Delegatetyp Feedback gezeigt, der keinen Wert an den Aufrufer zurückgibt. Ich könnte meinen Feedback-Typ auch folgendermaßen definieren:

public delegate Int32 Feedback( 
   Object value, Int32 item, Int32 numItems);

Dann würde die Invoke-Methode intern so aussehen (auch dies wieder als Pseudocode):

class Feedback : MulticastDelegate { 
   public Int32 virtual Invoke( 
      Object value, Int32 item, Int32 numItems) { 
      // Wenn es noch weitere Delegates in der Kette gibt, die 
      // zuerst aufgerufen werden müssen, dann tu es. 
      if (_prev != null) _prev.Invoke(value, item, numItems); 
   // Rufe die eigentliche Zielmethode auf. 
   return _target.methodPtr(value, item, numItems); 
   } 
}

Wenn der Kopf einer Delegateskette aufgerufen wird, sorgt er für den Aufruf aller anderen Delegates aus der Kette. Die Rückgabewerte des vorigen Delegates werden verworfen. Ihr Anwendungscode erhält nur den Rückgabewert von dem Delegate, der an der Spitze der Kette steht (also den Wert von der letzten aufgerufenen Rückrufmethode).

Einmal konstruiert, lassen sich die Delegateobjekte nicht mehr verändern. Anders gesagt, das _prev-Feld in den Delegateobjekten ist immer auf null gesetzt. Und das ändert sich auch nicht. Wenn Sie ein Delegateobjekt in eine Kette einhängen, konstruiert Combine intern ein neues Delegateobjekt, das dasselbe _target und dasselbe _methodPtr-Feld wie das ursprüngliche Objekt hat. Das _prev-Feld wird auf den alten Kopf der Kette gesetzt. Combine gibt die Adresse dieses neuen Delegateobjekts an den Aufrufer zurück (Listing L4).

L4 So wird ein Delegateobjekt in eine Kette eingehängt

Feedback fb = new Feedback(FeedbackToConsole); 
Feedback fbChain = (Feedback) Delegate.Combine(fb, fb); 
// fbChain verweist auf eine Kette mit zwei Delegates. 
// Eines dieser Objekte aus der Kette ist mit dem Objekt 
// identisch, auf das fb verweist. Das andere Objekt 
// wurde von Combine zusammengebaut. Das _prev-Feld 
// dieses Objekts verweist auf fb und Combine gibt eine 
// Referenz auf dieses neue Objekt zurück. 
// Verweisen fb und fbChain auf genau dasselbe Objekt? 
// False. 
Console.WriteLine((Object) fb == (Object) fbChain); 
// Verweisen fb und fbChain auf dasselbe Ziel und  
// dieselbe Methode? True. 
Console.WriteLine(fb.Equals(fbChain));

Soviel zum Aufbau der verketteten Delegateslisten. Wenden wir uns nun dem Löschen eines Delegates aus der Liste zu. Wenn Sie einen Delegate aus solch einer Liste entfernen möchten, rufen Sie die statische Methode Remove der Klasse Delegate auf (Listing L5). Der in Listing L5 gezeigte Code baut zuerst eine Liste auf, indem er zwei Delegateobjekte anlegt und mit Combine zu einer Liste verkettet. Dann ruft er die Remove-Methode auf. Der erste Parameter von Remove verweist auf den Kopf der Delegateobjektliste und der zweite verweist auf das Delegateobjekt, das aus der Liste zu entfernen ist. Ich weiß, dass es seltsam klingt, wenn man ein neues Delegateobjekt anlegt, um es aus der Kette zu entfernen. Dieser Umstand ist durchaus erklärungsbedürftig.

L5 Ein Delegate wird mit Delegate.Remove aus der Liste entfernt

Feedback fb1 = new Feedback(FeedbackToConsole); 
Feedback fb2 = new Feedback(FeedbackToMsgBox); 
Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2); 
// fbChain verweist auf eine Kette mit 2 Delegates. 
// Rufe die Kette auf. 2 Methoden werden aufgerufen. 
if (fbChain != null) fbChain(null, 0, 10); 
fbChain = (Feedback) 
   Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox)); 
// fbChain verweist auf eine Kette mit 1 Delegate. 
// Rufe die Kette auf. 1 Methode wird aufgerufen. 
if (fbChain != null) fbChain(null, 0, 10); 
fbChain = (Feedback) 
   Delegate.Remove(fbChain, new Feedback(FeedbackToConsole)); 
// fbChain verweist auf eine Kette mit 0 Delegates. 
// (fbChain ist null) 
// Rufe die Kette auf. 0 Methoden werden aufgerufen. 
if (fbChain != null) fbChain(null, 0, 10); 
// Nun sehen Sie, warum ich ständig 
// fbchain mit null vergleiche.

Im Aufruf von Remove lege ich ein neues Delegateobjekt an. Die Felder _target und _methodPtr dieses Objekts werden wie gewünscht initialisiert und das _prev-Feld wird auf null gesetzt. Die Remove-Methode sucht in der Kette fbChain nach einem Delegateobjekt, das mit dem angegebenen Delegateobjekt übereinstimmt. Wie bereits beschrieben, vergleicht die überschriebene Equals-Methode der Delegate-Klasse nur die Felder _target und _methodPtr und ignoriert das _prev-Feld.

Wird ein übereinstimmendes Objekt gefunden, dann entfernt Remove dieses Objekt aus der Liste, indem sie das _prev-Feld seines Vorgängers entsprechend ändert. Remove gibt den Kopf der neuen Kette an den Aufrufer zurück. Ist das gesuchte Objekt nicht in der Kette vorhanden, tut Remove gar nichts (es wird keine Ausnahme gemeldet). Remove gibt dann denselben Wert an den Aufrufer zurück, den sie im ersten Parameter erhalten hat.
Bei jedem Remove-Aufruf wird immer nur höchstens ein Objekt aus der Liste entfernt, wie der Code in Listing L6 demonstriert.

L6 So werden Delegates aus der Liste entfernt

Feedback fb = new Feedback(FeedbackToConsole); 
Feedback fbChain = (Feedback) Delegate.Combine(fb, fb); 
// fbChain verweist auf eine Kette mit 2 Delegates. 
// Rufe die Kette auf: FeedbackToConsole wird 
// zweimal aufgerufen. 
if (fbChain != null) fbChain(...); 
// Entferne einen Delegate aus der Kette. 
fbChain = (Feedback) Delegate.Remove(fbChain, fb); 
// Rufe die Kette auf: FeedbackToConsole wird 
// einmal aufgerufen. 
if (fbChain != null) fbChain(...); 
// Entferne einen Delegate aus der Kette. 
fbChain = (Feedback) Delegate.Remove(fbChain, fb); 
// Rufe die Kette auf: 0 Methoden werden aufgerufen. 
if (fbChain != null) fbChain(...);

Die Delegatesketten-Unterstützung in C#

Um den C#-Entwicklern das Programmiererleben zu erleichtern, überlädt der C#-Compiler die Operatoren += und -= für die Delegatetypen. Diese Operatoren rufen hinter der Bühne natürlich wieder Delegate.Combine und Delegate.Remove auf. Aber die Verkettung der Delegates wird durch die Operatoren wesentlich einfacher. Der Code in Listing L7 demonstriert, wie Delegateobjekte in C# in eine Kette aufgenommen und wieder entfernt werden.

L7 So werden Delegates in C# in die Liste aufgenommen oder gelöscht

Feedback fb = new Feedback(FeedbackToConsole); 
App appobj = new App(); 
fb += new Feedback(appobj.FeedbackToStream); 
// Rufe die Kette auf: FeedbackToStream und FeedbackTo- 
// Console werden aufgerufen. 
if (fb != null) fb(...); 
// Entferne einen Delegate aus der Kette. 
fb -= new Feedback(FeedbackToConsole); 
// Rufe die Kette auf: FeedbackToStream wird aufgerufen. 
if (fb != null) fb(...); 
// Entferne den letzten Delegate aus der Kette. 
fb -= new Feedback(appobj.FeedbackToStream); 
// Rufe die Kette auf: 0 Methoden werden aufgerufen. 
if (fb != null) fb(...);

Intern übersetzt der Compiler den Operator += bei seiner Anwendung auf Delegates in entsprechende Aufrufe der Combine-Methode von Delegate. In vergleichbarer Weise wird der Operator -= bei seiner Anwendung auf Delegates in Aufrufe der Remove-Methode übersetzt. Sie können sich selbst überzeugen. Kompilieren Sie den gerade gezeigten Code und schauen Sie sich im ILDasm.exe die MSIL-Form des Codes an. Sie wird bestätigen, dass der C#-Compiler tatsächlich die Operatoren += und -= in die entsprechenden Aufrufe der statischen Methoden Combine und Remove des Typs Delegate übersetzt hat.

Aufruf einer Delegateskette

Damit wäre geklärt, wie man Delegates zu einer Liste verkettet und die Delegateobjekte aus der Liste aufruft. Es werden immer sämtliche Delegates aus der Liste aufgerufen, weil die Invoke-Methode jedes Delegatetyps immer den vorigen Delegate aus der Kette aufruft, sofern es einen gibt. Der Algorithmus ist offensichtlich sehr simpel. Allerdings hat diese Logik, die in vielen Situationen völlig ausreicht, auch ihre Grenzen.

So werden zum Beispiel alle Rückgabewerte der aufgerufenen Methoden verworfen, bis auf den letzten. Dieser einfache Algorithmus ist nicht in der Lage, die Ergebnisse von allen aufgerufenen Methoden an den Aufrufer zurückzugeben. Und das ist noch nicht alles. Was geschieht zum Beispiel, wenn einer der aufgerufenen Delegates eine Ausnahme meldet oder sich lange mit irgendwelchen Arbeiten aufhält? Da der Algorithmus jeden Delegate aus der Kette der Reihe nach aufruft, führt ein Problem in einem dieser Delegateobjekte dazu, dass die restlichen Delegates nicht mehr aufgerufen werden. Kein Musterbeispiel für einen "robusten" Algorithmus.

Für solche Fälle, in denen die vorgegebene Logik nicht ausreicht, bietet die Klasse MulticastDelegate eine Instanzmethode namens GetInvocationList an, mit der Sie jeden Delegate aus der Kette explizit aufrufen und einen Algorithmus benutzen können, der den Erfordernissen entspricht.

public class MulticastDelegate { 
   // Die Methode legt ein Delegatesarray an. Jeder Delegate 
   // in diesem Array ist ein Klon von seiner Vorlage aus der 
   // Kette (Eintrag 0 ist das Ende, das normaler- 
   // weise zuerst aufgerufen wird.) 
   public virtual Delegate[] GetInvocationList(); 
}

Die Methode GetInvocationList wirkt auf eine Referenz auf eine Delegateskette und liefert ein Array mit Referenzen auf Delegateobjekte. Intern geht GetInvocationList die Kette entlang, erzeugt von jedem Delegate aus der Kette einen Klon und trägt diesen Klon ins Array ein. Das _prev-Feld jedes Kons ist auf null gesetzt. Daher ist jedes Objekt isoliert und zieht keine Kette mit anderen Objekten nach sich.

Nun können Sie leicht einen Algorithmus entwickeln, der jeden Delegate aus dem Array explizit aufruft. Der Code in Listing L8 führt es vor.

L8 GetInvocationList ermöglicht die Änderung des Algorithmus

using System; 
using System.Text; 
// Definiere die Komponente Light. 
class Light { 
   // Diese Methode gibt den Zustand des Lichts an. 
   public String SwitchPosition() { return "The light is off"; } 
} 
// Definiere eine Komponente Fan. 
class Fan {  
   // Diese Methode gibt den Zustand des Lüfters an. 
   public String Speed() { throw new Exception("The fan broke due to  
                                               overheating"); } 
} 
// Definiere eine Komponente Speaker. 
class Speaker { 
   // Diese Methode gibt den Zustand des Lautsprechers  
   // an. 
   public String Volume() { return "The volume is loud"; } 
} 
class App { 
   // Definition eines Delegates, mit dem sich der  
   // Zustand der Komponenten abfragen lässt. 
   delegate String GetStatus(); 
   static void Main() { 
      // Deklariere eine leere Delegateskette. 
      GetStatus getStatus = null; 
      // Baue die 3 Komponenten und hänge ihre Status- 
      // methoden in die Delegateskette ein. 
      getStatus += new GetStatus(new Light().SwitchPosition); 
      getStatus += new GetStatus(new Fan().Speed); 
      getStatus += new GetStatus(new Speaker().Volume); 
      // Zeige den Zustandsbericht über die 3 Komponenten 
      // an. 
      Console.WriteLine(GetComponentStatusReport(getStatus)); 
   } 
   // Diese Methode erstellt den Statusbericht über die 
   // benutzten Komponenten. 
   static String GetComponentStatusReport(GetStatus status) { 
      // Ist die Kette leer, so gibt es nichts zu tun. 
      if (status == null) return null; 
      // Lege einen neuen Bericht an. 
      StringBuilder report = new StringBuilder(); 
      // Beschaffe das Array mit den Delegates. 
      Delegate[] arrayOfDelegates = status.GetInvocationList(); 
      // Gehe über die Delegates im Array. 
      foreach (GetStatus getStatus in arrayOfDelegates) { 
         try { 
            // Stelle den Zustandsstring der Komponente 
            // zusammen und hänge ihn an den Bericht an. 
            report.Append(getStatus() + "\r\n\r\n"); 
         } 
         catch (Exception e) { 
            // Generiere für die fragliche Komponente 
            // einen Fehlereintrag im Bericht. 
            Object component = getStatus.Target; 
            report.Append("Failed to get status from " + 
               ((component == null) ? "" : component.GetType() + ".") +  
               getStatus.Method.Name +  
               "\r\n   Error: " + e.Message + "\r\n\r\n"); 
         } 
      } 
      // Gib den Bericht an den Aufrufer zurück. 
      return report.ToString(); 
   } 
}

Fazit

Nun, es sieht so aus, als sei mir in dieser Kolumne der Platz ausgegangen, um Ihnen mehr über Delegates zu erzählen. An diesem Punkt haben Sie aber sicherlich genug erfahren, um Delegates definieren und benutzen zu können.