Fangen wir also damit an, wozu man sowas überhaupt braucht. Schauen wir uns also einmal das Strategie-Entwurfsmuster an, das dazu dient, eine Familie von austauschbaren Algorithmen zu definieren: Mithilfe von Strategieobjekten kann man dort bestimmte Codeteile unabhängig von speziellen Algorithmen machen. Diesen Teil des Strategie-Entwurfsmusters (es fehlt z.B. der Kontext) möchte ich für ein einfaches Beispiel aufgreifen, das im folgenden Verlauf immer wieder Verwendung finden soll:
public interface IStrategy
{
string GetText(string name);
}
public class HelloStrategy : IStrategy
{
public string GetText(string name)
{
return "Hello, "+name;
}
}
public class GoodbyeStrategy : IStrategy
{
public string GetText(string name)
{
return "Goodbye, "+name;
}
}
public class Executor
{
public void Main()
{
IStrategy helloStrategy = new HelloStrategy();
IStrategy goodbyeStrategy = new GoodbyeStrategy();
this.Display(helloStrategy);
this.Display(goodbyeStrategy);
}
private void Display(IStrategy strategy)
{
Console.WriteLine(strategy.GetText("World"));
}
}
Hello, World Goodbye, WorldDoch was hat das ganze nun mit Delegaten, anonymen Methoden und Lambda-Ausdrücken zu tun? Nun, einfach gesagt macht man mit Delegaten & Co. genau das, nur spart man sich die Implementierung einer speziellen Strategie-Klasse – wie genau zeigen die folgenden Abschnitte.
Delegaten
Seit der Version 1.1 des .NET-Frameworks werden Delegaten unterstützt. In Sprachen wie C, C++ oder Pascal würde man Delegaten als Funktionszeigern kennen – allerdings sind Delegaten streng typisiert. Man hat also einen Datentyp, der Methoden aufnehmen kann.
Zur Veranschaulichung soll Beispiel 1 so abgeändert werden, dass statt der Strategieobjekte Delegaten verwendet werden. Die jeweilige Methode zur Ermittlung des anzuzeigenden Textes soll also direkt übergeben werden können, ohne dass man explizit die Schnittstelle IStrategy und die Methode GetText implementieren muss. Dazu muss man zunächst - aufgrund der strengen Typisierung - einen Delegatentyp für diese Methode anlegen. Dieser definiert die Signatur einer Methode (also die Typen und Reihenfolge der Parameter) und den Typ des Rückgabewertes. Die Namen des Delegaten und der Parameter sind dabei unwichtig (schon einmal ein Vorteil gegenüber von Strategieobjekten). Zurück zum Beispiel: Die Methode GetText hat den Rückgabetyp string und nimmt einen Parameter ebenfalls vom Typ string entgegen. Die Delegat-Definition sieht dann folgendermaßen aus:
delegate string GetTextDelegate(string name);
Als nächstes nehmen wir uns die Methode Display vor: Diese soll jetzt nicht mehr ein Objekt vom Typ IStrategy entgegennehmen, sondern kann direkt ein Objekt vom Typ GetTextDelegate benutzen. Da dieses Parameterobjekt dann direkt eine Methode repräsentiert, geschieht der Aufruf jetzt durch getTextDelegate("World") (statt strategy.GetText("World")).
Beim Aufruf der Display-Methode kann jetzt direkt die Methode GetHelloText als Referenz übergeben werden (was durch eine implizite Konvertierung ein GetTextDelegate-Objekt erstellt; alternativ kann auch der GetTextDelegate-Konstruktor aufgerufen werden). Wie bereits erwähnt, ist der Name der Methode und die Namen der Parameter dabei unwichtig – die Signatur muss passen (Rückgabe- und Parametertypen, siehe auch Abschnitt "Kovarianz und Kontravarianz"). Das überarbeitete Beispiel 1 sieht jetzt also folgendermaßen aus:
public class Executor
{
public delegate string GetTextDelegate(string name);
public string GetHelloText(string name)
{
return "Hello, "+name;
}
public string GetGoodbyeText(string name)
{
return "Goodbye, "+name;
}
public void Main()
{
GetTextDelegate getHelloText = this.GetHelloText;
GetTextDelegate getGoodbyeText = this.GetGoodbyeText;
this.Display(getHelloText);
this.Display(getGoodbyeText);
}
private void Display(GetTextDelegate getTextDelegate)
{
Console.WriteLine(getTextDelegate("World"));
}
}
Kovarianz und Kontravarianz
Kovarianz und Kontravarianz erlauben es, dass die Signatur des Delegattypen und die der Methode nicht exakt übereinstimmen müssen. Durch Kovarianz kann einem Delegat eine Methode zugewiesen werden, dessen Rückgabetyp eine Spezialisierung des Rückgabetyps des Delegaten ist. Hat der Delegat beispielsweise einen Rückgabewert vom Typ Stream, kann ihm auch eine Methode zugewiesen werden, die den Rückgabetyp FileStream hat. Kontravarianz ermöglicht es dagegen, dass die Parametertypen der Methode eine Generalisierung des entsprechenden Parametertypen aus der Delegatdefinition ist. Hat beispielsweise der Delegat einen Parameter vom Typ FileStream, kann ihm eine Methode zugewiesen werden, dessen entsprechender Parameter den Typ Stream hat.
Beim Thema "Rückgabe- und Parametertypen" sollte nicht unerwähnt bleiben, dass bei Delegaten auch Generika verwendet werden können.
Multicastdelegaten & Ereignisse
Eine nützliche Eigenschaft von Delegaten ist, dass deren Operator + überladen ist, sodass mehrere Delegaten miteinander kombiniert werden können. Am besten macht das ein Beispiel klar (etwas abgewandelt aus der MSDN-Doku):
public delegate void DoSomethingDelegate(string value);
public void Hello(string value)
{
Console.WriteLine("Hello, "+value);
}
public void Goodbye(string value)
{
Console.WriteLine("Goodbye, "+value);
}
public void Main()
{
DoSomethingDelegate a = this.Hello;
DoSomethingDelegate b = this.Goodbye;
DoSomethingDelegate c = a + b;
DoSomethingDelegate d = c - a;
a("A");
// > "Hello, A"
b("B");
// > "Goodbye, B"
c("C");
// > "Hello, C"
// "Goodbye, C"
d("D");
// > "Goodbye, D"
}
Ein spezieller Typ von Multicastdelegaten sind Ereignisse ("events"). Diese werden durch das event-Schlüsselwort gekennzeichnet und können dann nur in der Klasse oder Struktur aufgerufen werden, in der sie auch deklariert wurden. Ereignishandler sind nichts weiter als Methoden, die dann durch den Delegaten aufgerufen werden. Das Abonnieren von Ereignissen mittels += bedeutet nichts weiter, als das eine (Ereignisbehandlungs-)Methode dem Multicastdelegaten hinzugefügt wird.
Anonyme Methoden
Wie man eventuell schon am Namen erraten kann, sind anonyme Methoden solche, die keinen Namen haben. Einige werden sich jetzt fragen, "Häh, Methoden ohne Namen?". Ja das geht - allerdings nur im Zusammenhang mit Delegaten. Im Beispiel 2 haben wir Delegaten erstellt, indem wir ihnen benannte Methoden zugewiesen haben. Ab C# 2.0 ist es zudem möglich, den Methodenrumpf der referenzierten Methode direkt "vor Ort" (bei der Delegatenerstellung) anzugeben. Da eine solche Methode nur über den Delegaten aufgerufen werden kann, braucht sie auch keinen Namen - daher "anonyme Methoden". Zu Veranschaulichung ändern wird Beispiel 2 so, dass anonyme Methoden verwendet werden. Beispielsweise erstellen einen neuen Delegaten nicht durch new GetTextDelegate(this.GetHelloText) (benannte Methode), sondern folgendermaßen (anonyme Methode; Methodenimplementierung "direkt vor Ort"):
GetTextDelegate getTextDelegate =
new GetTextDelegate(
delegate(string name)
{
return "Hello, "+name;
});
GetTextDelegate getTextDelegate =
delegate(string name)
{
return "Hello, "+name;
};
public class Executor
{
public delegate string GetTextDelegate(string name);
public void Main()
{
GetTextDelegate getHelloText = delegate(string name) { return "Hello, "+name; };
GetTextDelegate getGoodbyeText = delegate(string name) { return "Goodbye, "+name; };
this.Display(getHelloText);
this.Display(getGoodbyeText);
}
private void Display(GetTextDelegate getTextDelegate)
{
Console.WriteLine(getTextDelegate("World"));
}
}
button.Click += delegate(object sender,EventArgs e)
{
MessageBox.Show("Klick");
};
button.Click += delegate
{
MessageBox.Show("Klick");
};
Closures
Closures sind laut Wikipedia "eine Programmfunktion, die beim Aufruf ihren Definitionskontext reproduziert, selbst wenn dieser Kontext außerhalb der Funktion schon nicht mehr existiert." Auch hier soll wieder ein Beispiel der besseren Veranschaulichung dienen:
public void Main()
{
string value = "Hello, ";
GetTextDelegate getText = delegate(string name)
{
return value+name;
});
value = "Goodbye, ";
Console.WriteLine(getText("World"));
}
Anonyme Methoden sind eigentlich nur Syntaxzucker. In Wirklichkeit generiert der Compiler automatisch eine Klasse, legt dort eine benannte Methode an und benutzt diese dann. Ebenso werden die äußeren Variablen als Klassenvariablen angelegt. Der Quelltext würde vom Compiler folgendermaßen übersetzt werden (Namen vereinfacht): [csharp] [CompilerGenerated] internal sealed class GeneratedClass { public string value; public string AnonymousMethod(string name) { return (this.value + name); } } public void Main() { GeneratedClass generatedClass = new GeneratedClass(); generatedClass.value = "Hello, "; GetTextDelegate getText = new GetTextDelegate(generatedClass.AnonymousMethod); generatedClass.value = "Goodbye, "; Console.WriteLine(getText("World")); } [/csharp]
Man muss sich unbedingt bewusst sein, dass sich die Werte der äußeren Variablen zwischen der Deklaration und der Ausführung eines Delegaten durchaus geändert haben können. Es wird immer der Wert verwendet, den die äußere Variable zur Ausführungszeit hat (bzw. in ihrem Kontext zuletzt hatte).
Eine "beliebte" Fehlerquelle ist beispielsweise die Verwendung der Laufvariablen als äußere Variable für Delegatsdeklarationen innerhalb einer for(each)-Schleife:
List<Func<string>> functions = new List<Func<string>>();
foreach (int value in new int[] { 1,2,3,4,5 })
functions.Add(delegate() { return value.ToString(); });
foreach (Func<string> func in functions)
Console.Write(func()+" ");
Lösen kann man das Problem, indem man eine äußere Variable benutzt, die nur innerhalb eines Schleifendurchlaufs gültig ist:
foreach (int value in new int[] { 1,2,3,4,5 })
{
int i = value;
functions.Add(delegate() { return i.ToString(); });
}
Lambda-Ausdrücke
Neben der Fähigkeit, Ausdrucksbaumstrukturen erstellen zu können (später dazu mehr), ist es mit Lambda-Ausdrücken möglich, (anonyme) Delegatendefinitionen noch kürzer zu gestalten. Nehmen wir als Beispiel folgende anonyme Methode:
delegate(string name)
{
return "Hello, "+name;
}
(string name) =>
{
return "Hello, "+name;
}
Aber das war noch längst nicht alles: Da es ja Kontravarianz gibt und der Compiler zudem den Typen herleiten kann, kann man auch die Parametertypen weglassen:
(name) => { return "Hello, "+name; }
name => { return "Hello, "+name; }
Unser Lambda-Ausdruck hat außerdem noch die Eigenschaft, dass die rechte Seite nur aus einem einzigen Ausdruck besteht. Damit erfüllt er die Struktur eines Ausdruckslambdas (im Gegensatz zum Anweisungslambda). Daher können wir die geschweiften Klammer und das Semikolon auch weglassen. Da es sich um eine Rückgabeanweisung handelt, kann man (bzw. muss dann sogar) das return-Schlüsselwort ebenfalls entfernen. Das macht dann zusammen:
name => "Hello, "+name
Beispiel 3 mit "langen" Lambda-Ausdrücken sieht dann folgendermaßen aus:
public class Executor
{
public delegate string GetTextDelegate(string name);
public void Main()
{
GetTextDelegate getHelloText = (string name) => { return "Hello, "+name; };
GetTextDelegate getGoodbyeText = (string name) => { return "Goodbye, "+name; };
this.Display(getHelloText);
this.Display(getGoodbyeText);
}
private void Display(GetTextDelegate getTextDelegate)
{
Console.WriteLine(getTextDelegate("World"));
}
}
public class Executor
{
public delegate string GetTextDelegate(string name);
public void Main()
{
GetTextDelegate getHelloText = name => "Hello, "+name;
GetTextDelegate getGoodbyeText = name => "Goodbye, "+name;
this.Display(getHelloText);
this.Display(getGoodbyeText);
}
private void Display(GetTextDelegate getTextDelegate)
{
Console.WriteLine(getTextDelegate("World"));
}
}
Lambda-Ausdrücke sind ein reines Compilerfeature, dass seit Visual Studio 2008 verfügbar ist. Das heißt, dass man zum Beispiel auch in Anwendungen für das .NET Framework 2.0 Lambda-Ausdrücke verwenden kann (sofern man sie nicht in Verbindung mit Ausdrucksbaumstrukturen verwendet, welche erst ab dem .NET-Framework 3.5 verfügbar sind).
Funktionen und Aktionen
Wer hätte es gedacht, den Quelltext aus Beispiel 5 bekommt man noch kleiner. Denn damit man nicht dauernd explizit Delegattypen definieren muss, gibt dafür schon etwas Vorgefertigtes. Im Namensraum System (Assembly System.Core) existieren dafür (ab Framework-Version 3.5) die Func- und Action-Delegate mit ihren diversen generischen Überladungen.
Action-Delegaten sind für Lambda-Ausdrücke ohne Rückgaben. Für einen Lambda-Ausdruck ohne Parameter verwendet man die Action-Delegaten, für solche mit einem Parameter Action<T>, für zwei Parameter Action<T1,T2>, für drei Action<T1,T2,T3> und für vier Action<T1,T2,T3,T4>. Hier ein Beispiel für eine Aktion mit zwei Parametern:
Action<string,int> action = (value,count) =>
{
for (int i=0;i<count;i++)
Console.WriteLine(value);
};
action("Hello, World",10);
Action action = () => Console.WriteLine("Hello, World");
action();
Func<string,int,char> func = (value,index) =>
{
return value[index];
};
char firstChar = func("Hello, World",0);
public class Executor
{
public void Main()
{
Func<string,string> getHelloText = name => "Hello, "+name;
Func<string,string> getGoodbyeText = name => "Goodbye, "+name;
this.Display(getHelloText);
this.Display(getGoodbyeText);
}
private void Display(Func<string,string> func)
{
Console.WriteLine(func("World"));
}
}
Wie bereits geschrieben, können Lambda-Ausdrücke auch mit dem .NET-Framework 2.0 verwendet werden. Allerdings sind darin die [tt]Action[/tt]- und [tt]Func[/tt]-Delegaten noch nicht enthalten (bzw. lediglich der [url][tt]Action<T>[/tt]-Delegat[/url]). Je nach Anwendungsfall bieten sich aber z.B. auch die vorgefertigten [url][tt]Predicate<T>[/tt]-[/url], [url][tt]Comparison<T>[/tt]-[/url], [url][tt]Converter<TIn,TOut>[/tt]-[/url] oder [url][tt]EventHandler<TEventArgs>[/tt]-[/url]Delegaten aus der [FONT]mscorlib.dll[/FONT] für eine Verwendung in .NET 2.0 an.
LINQ mit Lambda-Ausdrücken verstehen
Mit den bisherigen Informationen haben wir eigentlich auch schon alles zusammen, um LINQ-Abfrageausdrücke (in Lambda-Notation) verstehen zu können. Nehmen wir als Beispiel folgenden Ausdruck (persons ist eine Liste mit Person-Objekten):
var query = persons.Where(p => p.Age>50).Select(p => p.Name);
IEnumerable<string> query = persons.Where<Person>(p => p.Age>50).Select<Person,string>(p => p.Name);
Fangen wir mit Where an: Where führt eine Selektion der Elemente aus persons aus. Der Algorithmus, welche Objekte selektiert werden, wurde dabei ausgelagert und muss vom Programmierer übergeben werden. Als Parameter wird also eine Methode erwartet, die ein Person-Objekt entgegennimmt und einen Wahrheitswert zurückgibt, der angibt, ob das jeweilige Element in die Ausgabemenge kommt oder nicht. Im Beispiel wird als Parameter also eine Funktion Func<Person,bool> übergeben, die nur true zurück gibt, wenn das Alter der übergebenen Person (p) größer als 50 ist.
Beim Select wurde wiederum der Algorithmus zur Projektion ausgelagert. Es wird also über alle Elemente iteriert, die von Where zurückgegeben wurden, der Wert, den die übergebene Funktion zurück gibt, in die Ausgabemenge eingefügt. Im Beispiel wird von jedem Person-Objekt die Eigenschaft Name zurückgegeben.
Alles in allem wird also der Name von jeder Person über 50 ermittelt. Ist doch eigentlich ganz einfach, oder?
Ausdrucksbaumstrukturen
Wie bereits anfangs erwähnt, bieten Lambda-Ausdrücke die Möglichkeit, Ausdrucksbaumstrukturen zu erstellen. Das heißt, dass der Methodenrumpf aus dem Lambda-Ausdruck in eine Baumstruktur umgewandelt wird. Dazu muss man einen Lambda-Ausdruck lediglich einem Objekt vom Typ Expression<TDelegate> (Namensraum System.Linq.Expressions) zuweisen. Eine wichtige Einschränkung, die man dabei beachten muss, ist, dass man dafür nur Ausdruckslambdas verwenden kann, also Lambdaausdrücke, die Funktionen darstellen. Hier ein Beispiel:
Expression<Func<int,int>> expression = (x) => 2*x;
ParameterExpression parameterExpression = Expression.Parameter(typeof(int),"x");
ConstantExpression constantExpression = Expression.Constant(2,typeof(int));
Expression body = Expression.Multiply(constantExpression,parameterExpression);
Expression<Func<int,int>> expression = Expression.Lambda<Func<int,int>>(body,new ParameterExpression[] { parameterExpression });
Der Code im Lambda-Ausdruck wird also in Form von Daten dargestellt. Doch wozu das Ganze? Nun, zum einen kann man die Methode Expression.Compile aufrufen, was einen entsprechenden Delegaten erzeugt. Da man Ausdrucksbäume auch bearbeiten bzw. manuell welche erstellen kann, bietet dies eine Möglichkeit, dynamisch Code zur Laufzeit zu kompilieren. Einen anderen Anwendungszweck veranschaulicht die IQueryable-Schnittstelle: Diese hat Methoden, welche Expression-Objekte als Parameter annehmen. Anhand des übergebenen Ausdrucksbaums kann beispielsweise der LINQ2SQL-QueryProvider einen äquivalenten SQL-Ausdruck erstellen. Das geht aber nur, weil der Code eben in Form von Daten übergeben wurde.
Den Ausdrucksbaum aus dem obigen Beispiel könnte wieder folgendermaßen "auseinandergenommen" werden:
ReadOnlyCollection<ParameterExpression> parameters = expression.Parameters;
BinaryExpression binaryExpression = (BinaryExpression)expression.Body;
ConstantExpression left = (ConstantExpression)binaryExpression.Left;
ParameterExpression parameterExpression = (ParameterExpression)binaryExpression.Right;
Zusammenfassung
Delegaten bieten die Möglichkeit, streng Methoden als Argumente an anderen Methoden zu übergeben. Hierfür muss ein typisierter Delegattyp erstellt werden - allerdings erlauben Ko- und Kontravarianz auch ein gewisses Maß an Flexibilität. Durch Kombination ist es auch möglich, Multicastdelegaten zu erstellen. Diese bilden zudem die Grundlage für Ereignisse.
Anonyme Methoden erweitern das Delegatenkonzept und bieten die Möglichkeit, keine separaten Methoden mehr erstellen zu müssen. Durch die Deklaration einer anonymen Methode direkt innerhalb einer anderen Methoden kommt auch das Konzept von so genannten äußeren Variablen ins Spiel.
Mithilfe von Lambda-Ausdrücken kann einerseits die Erstellung von anonymen Methoden noch weiter verkürzt werden. Zudem erhält C# durch Lambda-Ausdrücke funktionale Aspekte und Konzepte wie Currying, Partial Application und Function Composition können leicht umgesetzt werden. Andererseits bieten Lambda-Ausdrücke die Möglichkeit, einfach Ausdrucksbäume erstellen zu können, welche beispielsweise von QueryProvidern (LINQ) benutzt werden.
Schlagwörter: Delegates, anonyme Methoden, Lambda-Ausdrücke, Events, LINQ
Lamda schreibt man mit "b" vor dem "d"!