Advanced  Services Delegates

Was ist ein Delegate

Um zu klären, was ein Delegate ist gehen wir zunächst von seiner Verwendung aus. So kann die Verwendung eines Delegate wie folgt aussehen:


Variante 1: Direkte Wertzuweisung
MyDelegate del = methodName;

Dies erinnert an einen vertrauten Vorgang und wir kennen das etwa von

int x = 17;

oder noch ähnlicher mit

string st = "delegates";

Ein Delegate ist also ein Datentyp. Man kann Variablen anlegen und diesen Variablen Werte zuweisen. Die Werte sind in diesem Fall die Namen von Methoden. Wie das funktioniert folgt etwas später. Die Wertzuweisung kann aber auch folgendermaßen geschehen:


Variante 2: Wertzuweisung mit Hilfe eines Konstruktors
MyDelegate del = new MyDelegate(methodName); [unüblich da nicht notwendig]

Auch das erinnert an einen vertrauten Vorgang. Offensichtlich ist ein Delegate ein Klasse, sonst könnte man ja die Instanzen nicht mit Hilfe eines Konstruktors initialisieren. (Wir werden gleich sehen, daß es eine besondere Form einer Klasse ist.) Die Variablen (oder besser Instanzen) können dann mit Hilfe eine Konstruktors erzeugt werden. Diese Art der Initialisierung ist aber nicht üblich, da der Compiler bei der direkten Zuweisung des Methodennamens im Hintergund den (einzigen) Konstruktor aufruft.


Deklaration eines Delegate

Da Methoden unterschiedlichste Signaturen haben können, kann es nicht einen Delegatetyp für alle Methoden geben. Ein Methodenname kann nur dann einer Delegateinstanz zugewisen werden, wenn die Methode die Signatur erfüllt, die das Delegate (der Delegatetyp) vorgibt. Ein Delegatetyp steht immer nur für eine ganz bestimmte Methodensignatur und zwar inklusive des Returntyps! Bevor wir also einer Instanz eines Delegate einen Wert zuweisen können muß daher erklärt werden, für welche Methoden der Delegatetyp zuständig sein soll. Dies wird vorweg durch eine Deklaration festgelegt. Die Deklaration erfolgt mit dem Schlüsselwort Delegate (kleingeschrieben!) gefolgt von einer Methodensignatur (einschließlich Returntyp), wobei der für die Methode gewählte Name zum Typnamen des Delegate wird. Damit legt man sozusagen einen Prototyp einer Methode fest.


Beispiel 1:  delegate double ConvertIntToDouble(int x);

Auch dieses einfache Beispiel zeigt bereits, welche Flexibilität man durch den Einsatz von Delegates erreichen kann.

Das so deklarierte Delegate hat nun den Typnamen ConvertToDouble und ist nun zuständig für Methoden mit einem Parameter vom Typ int und dem Returntyp double. Jetzt brauchen wir noch eine reale Methode. Die folgende Klasse stellt eine passende Methode zur Vewrfügung.

class MyClass
{
  public double Convert(int i)
  {
     return 1.0*i;
  }
}

Die Verwendung dieser Methode geschieht nun wie folgt.

MyClass mc = new MyClass();
ConvertIntToDouble citd = mc.Convert;

Über die Intellisense des Visualstudios erfährt man, daß es nur einen Konstruktor gibt und daß dieser nur Methoden der zuvor vereinbarten Signatur akzeptiert. Aus der Deklaration eines Delegate generiert der Compiler im Hintergrund eine spezielle Klasse mit genau einem zur vereinbarten Signatur passenden Konstruktor der bei der direkten Zuweisung aztomatisch aufgerufen wird.

Oft gibt es Methoden aus der API, welche die Signatur eines Delegate erfüllen. Auch hier ist das der Fall, es gibt nämlich in System die Hilfsklasse Convert, die eine Sammling von statischen Konvertierungsmethoden anbietet. Die Methode public static double ToDouble(int value) hat die passende Signatur. Wir können also folgende Zuweisung machen:

ConvertIntToDouble citd = Convert.ToDouble;

Hier sieht man, daß man auch statische Methoden zuweisen kann.

Das sieht zunächst nicht sehr sinnvoll aus. Das nächste Beispiel wird aber die typische Verwendung von Delegates zeigen. Die typische Situation dabei ist, daß es eine Methode gibt, in deren Parameterliste ein Delegatetyp auftaucht.





Beispiel 2:  Ein generisches Delegate

Wir deklarieren wie folgt

public delegate bool FindMatch<in T>>(T t);

Das so deklarierte Delegate hat nun den Typnamen FindMatch und ist zuständig für Methoden mit einem Parameter vom Typ T und Returntyp int. Wir wollen dieses Delegate verwenden um in Stringarrays nach einem bestimmten String zu suchen. Dazu schreiben wir uns eine Methode mit der folgenden Signatur:

private static string FindFirst(string[] sArr, FindMatch<string> matchFinder)
{
  // not jet implemented
}

Der Methode FindFirst können wir nun ein Array übergeben und eine Methode die das Array nach einem bestimmten Kriterium untersucht. Im Erfolgsfall kann dann FindFirst() den gefundenen String zurückgeben. Wird kein String gefunden, so gibt FindFirst() null zurück.

Wir wollen feststellen, ob ein String vorkommt der länger als 7 Zeichen ist und schreiben uns dafür eine Methode, die die Signatur des Delegate erfüllt.

public static bool Matcher(string s)
{
  return s.Length > 7;
}

Jetzt übergeben wir der Methode FindFirst() ein Array und den Matcher:

static void Beispiel()
{
  string[] sArr = {"bhd", "cdehjvg", "dhdsvhjbdcs", "hgsh", "oiu2zrthj" };

  string match = FindFirst(sArr, Matcher);
  if (match != null)
    Console.WriteLine("found: " + match);
  else
    Console.WriteLine("no match found");
}

Und so schaut die Methode FindFirst() aus:

private static string FindFirst(string[] sArr, FindMatch<string> matchFinder)
{
  foreach( string st in sArr )
    if (matchFinder(st))
      return st;

  return null;
}

Mit der Methode FindFirst() können wir aber das Array auch nach anderen Kriterien untersuchen. Dazu brauchen wir nur eine passende Methode als zweiten Parameter. Hier etwa eine Methode, mit der man feststellen kann, ob ein String vorkommt der Ziffern enthält.




static bool Matcher2(string s)
{
  for( int i=0; i< s.Length; i++)
  {
    if (Char.IsDigit(s,i))
      return true;
  }
  return false;
}

Und so setzen wir die obige Methode ein:

string match2 = FindFirst(sArr, Matcher2);
if (match2 != null)
  Console.WriteLine("found: " + match2);
else
  Console.WriteLine("no match found");

Nun klären wir noch die Frage, wo man ein Delegate vereinbaren kann.





Wo kann man ein Delegate deklararieren

Die Deklaration eine Delegate kann innerhalb eine Klasse, außerhalb einer Klasse, oder sogar auch außerhalb eines Namensbereichs stehen. Wir merken uns:


Wo kann man eine Variable vom Typ eines Delegate anlegen

Eine Delegatevariable kann man überall dort anlegen wo man auch andere Variablen anlegen kann.


Die Multicasteigenschaft der Delegates

Bisher haben wir ein Delegate lediglich wie ein Alias benützt. Die Multicastfähigkeit der Delegates zeigt uns, daß ein Delegate weit mehr kann. Ein Delegate ist nämlich in der Lage (quasi) beliebig viele Methoden zu speichern. Um dies zu erreichen verwenden wir nicht das Gleichheitszeichen, sondern den +=-operator. Im folgenden vereinbaren wir ein Delegate und drei Methoden und "sammeln" diese in einer Delegateinstanz. Über diese Instanz werden wir dann alle drei Methoden aufrufen. Mit dem Operator -= kann man übrigens gezielt auch einzelne Methoden aus dem Delegate entfernen.

namespace DelegateDemo
{
   // Deklaration des Delegate
   delegate double MyDelegate(double x);

   class Program
   {
      // Anlegen einer Instanz
      private static MyDelegate myDelegate;

      // drei Methoden, die dem Delegate zugewiesen wird
      public static double method1(double x)
      {
         Console.WriteLine("method1 called");
         return 1;
      }

      public static double method2(double x)
      {
         Console.WriteLine("method2 called");
         return 2;
      }

      public static double method3(double x)
      {
         Console.WriteLine("method3 called");
         return 3;
      }

      public static void Main(string[] args)
      {
         // Verwendung des Konstruktors
         myDelegate = new MyDelegate(method1);

         // Aufaddieren
         myDelegate += method3;
         myDelegate += method2;

         // Aufruf aller Methoden über das Delegate
         double erg = myDelegate(-8);
         Console.WriteLine("erg = " + erg);
      }  // end main

   } // end class Program

} // end namespace

Hier ein Screenshot des Aufrufs:



Überraschenderweise werden über einen einzigen Aufruf double erg = myDelegate(-8); alle zugewiesenen Methoden aufgerufen. Die Aufrufreihenfolge entspricht der Zuweisungsreihenfolge und der Returnwert stammt offensichtlich von der zuletzt aufgerufenen Methode.

Es ist aber auch möglich nur eine ganz bestimmte Methode aufzurufen. Dazu verwenden wir die Methode GetInvocationList() über die jedes Delegate verfügt. Damit erhalten wir ein Array von Delegates. Sodann kann man sich die gewünschte Methode aus dem Array aussuchen und gezielt nur diese aufrufen. Dabei ist zu beachten, daß die Methode GetInvocationList() ein Array des allgemeineren Typs System.Delegate liefert, dessen Elemente dann einzeln zum eigentlichen Typ heruntergecastet werden müssen, siehe hierzu auch die Vererbungshierarchie der Delegates weiter unten.


Codeschnipsel: Aufruf aller Methoden über die InvocationList
System.Delegate[] all = myDelegate.GetInvocationList();

// einzeln casten notwendig
for (int i = 0; i < all.Length; i++)
{
   Console.WriteLine("ret = {0}", ((MyDelegate)all[i])(17));
}

oder eleganter

System.Delegate[] all = myDelegate.GetInvocationList();

// einzeln casten notwendig
foreach (Delegate del in all)
{
   Console.WriteLine("ret = {0}", ((MyDelegate)del)(-8));
}

Codeschnipsel: Aufruf einer bestimmten Methode über die InvocationList
Delegate[] all = myDelegate.GetInvocationList();

// Aufruf einer bestimmten Methoden mit Hilfe der Invocationlist
Console.WriteLine("ret = {0}", ((MyDelegate)all[1])(17));

Die Vererbungshierarchie der Delegates

Wie bereits erwähnt ist ein Delegate eine spezielle Form einer Klasse. Ein selbstentworfenes Delegate ist immer eine Ableitung der Klasse System.MulticastDelegate. Die folgende Graphik zeigt die Vererbungshierarchie.