Laden...

[Artikel] Delegaten, anonyme Methoden, Lambda-Ausdrücke & Co.

Erstellt von dN!3L vor 14 Jahren Letzter Beitrag vor 13 Jahren 58.378 Views
dN!3L Themenstarter:in
2.891 Beiträge seit 2004
vor 14 Jahren
[Artikel] Delegaten, anonyme Methoden, Lambda-Ausdrücke & Co.

Da die Verwendung von Delegaten (und Events) für Anfänger oft schwer zu verstehen und Lambda-Ausdrücke selbst für einige Fortgeschrittene verwirrend sind, soll dieser Artikel einen kleinen Überblick über die Konzepte Delegat, anonyme Methode und Lambda-Ausdruck geben.

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"));
	}
}

Der Algorithmus zur Ermittlung des anzuzeigenden Textes wurde ausgelagert – die Methode Display ist somit unabhängig vom Algorithmus. Zudem ist der Algorithmus austauschbar (HelloStrategy vs. GoodbyeStrategy). Die Ausgabe ist übrigens

	Hello, World
	Goodbye, World

Doch 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);

Wie man sieht, werden Delegatendefinitionen mit dem Schlüsselwort delegate eingeleitet. Danach folgt der Rückgabetyp (string), gefolgt vom Namen des Delegatentyps. Zuletzt kommt dann – analog zu Methodendefinitionen – die Parameterliste.

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"
}

Der Effekt beim Aufruf von c ist, dass sowohl Hello als auch Goodbye aufgerufen/ausgeben werden – denn c besteht aus a (Aufruf von Hello) plus b (Aufruf von b). Man kann sich (Muticast-)Delegaten also sogar als Liste von Referenzen auf Methoden vorstellen. Daher ergibt der Aufruf von d auch (nur) ein Goodbye (b), da a aus c (bestehend aus a und b) wieder entfernt wurde.

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;
		});

Oder auch kürzer:


GetTextDelegate getTextDelegate = 
	delegate(string name)
	{
		return "Hello, "+name;
	};

Außer dem Schlüsselwort delegate statt einem Methodennamen und der fehlenden Angabe des Rückgabetypen gibt es erst einmal keine Unterschiede zu normalen benannten Methoden. Beispiel 2, nur mit anonymen Methoden, sieht dann folgendermaßen aus:


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"));
	}
}

Ein interessantes Feature von anonymen Methoden ist außerdem, dass man die Parameterliste auch komplett weglassen kann - wenn man die Parameter eh nicht benutzt, kann man sich so Schreibarbeit sparen. So kann man beispielsweise

button.Click += delegate(object sender,EventArgs e)
				{
					MessageBox.Show("Klick");
				};

vereinfachen zu


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"));
}

Der aufmerksame Leser wird sicher merken, dass value zwar in der anonymen Methode benutzt wird, aber in dessen Kontext gar nicht definiert wurde. Solche Variablen werden auch als äußere Variablen bezeichnet (im Gegensatz zu lokalen Variablen). "OK, nettes Feature" wird sich Manchereiner denken, aber das Beste kommt noch: Was wird denn auf der Konsole ausgegeben? Richtig, "Goodbye, World". Auf solche Sachen muss man Acht geben, wenn man anonyme Methoden benutzt. Um solches Verhalten besser verstehen zu können, hilft es, zu wissen, wie anonyme Methoden eigentlich implementiert sind.


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()+" ");

Die Ausgabe dürfte etwas unerwartet ausfallen: "5 5 5 5 5 ". Der Grund für dieses Verhalten ergibt sich aus dem Kasten "Anonyme Methoden unter der Haube": Für jeden Schleifendurchlauf wird ein und dasselbe Exemplar der generierten Klasse verwendet - und für die äußere Variable letztendlich den als letztes zugewiesenen Wert.
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(); });
}

Hier ist die Ausgabe "1 2 3 4 5". Anders als beim "normalen" Variablen macht es bei äußeren Variablen also durchaus einen Unterschied, ob sie innerhalb oder außerhalb einer Schleife deklariert werden!

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;
}

Um daraus einen Lambda-Ausdruck zu machen, löschen wir das delegate-Schlüsselwort und fügen einen =&gt;-Operator (lies "goes to") hinter der Parameterliste ein (wenn es keine Parameter gibt, ist ein leeres Klammernpaar zu benutzen). Fertig ist der erste Lambda-Ausdruck:


(string name) => 
{
	return "Hello, "+name;
}

Die obige Form mit geschweiften Klammern wird auch Anweisungslambda genannt.
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; }

Da wir außerdem nur einen einzigen Parameter haben, brauchen wir die runden Klammern auch nicht mehr. Macht dann:


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"));
	}
}

Völlig verkürzt sieht der Spaß dann so aus:


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&lt;T&gt;, für zwei Parameter Action&lt;T1,T2&gt;, für drei Action&lt;T1,T2,T3&gt; und für vier Action&lt;T1,T2,T3,T4&gt;. 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);

Ein Lambda-Ausdruck ohne Parameter sieht dann übrigens folgendermaßen aus:


Action action = () => Console.WriteLine("Hello, World");
action();

Für Lambda-Ausdrücke mit einem Rückgabewert (="Funktion") gibt es die Func-Delegaten. Entprechend zu den Action-Delegaten gibt es wieder Überladungen für bis zu vier Parametern. Der letzte generische Typ ist dabei immer der des Rückgabewertes. So zum Beispiel eine Funktion mit zwei Parametern:


Func<string,int,char> func = (value,index) =>
	{
		return value[index];
	};
char firstChar = func("Hello, World",0);

Benutzt man die entsprechenden Func-Delegaten, sieht Beispiel 5 folgendermaßen aus:


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);

Zunächst sollte noch erwähnt werden, dass Where und Select generische Typparameter haben, die aber vom Compiler automatisch hergeleitet werden können. Vollständig ausgeschrieben sieht die Abfrage dann so aus:


IEnumerable<string> query = persons.Where<Person>(p => p.Age>50).Select<Person,string>(p => p.Name);

Der Ablauf dafür ist folgender: Angefangen wird mit der Menge persons, die als Eingabemenge für Where dient. Where wiederum erzeugt eine spezielle Ausgabemenge, die nun für Select als Eingabemenge dient. Schließlich enthält query die Menge, die Select als Ausgabe liefert. Es gilt jetzt also, herauszufinden, was die einzelnen Methoden als Ausgabemenge produzieren.
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&lt;TDelegate&gt; (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;

Auch in diesem Fall sind Lambda-Ausdrücke wieder nur Compilermagie. Denn "in Wirklichkeit" erweitert der Compiler diese Zeile zu:


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;

Bei Verwendung von Methodenaufrufen, Zugriff auf Eigenschaften, äußere Variablen oder Ähnliches erhält man dann auch Referenzen auf die jeweiligen Metainformationsobjekte (aus dem System.Reflection-Namensraum).

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"!

925 Beiträge seit 2004
vor 14 Jahren

Danke für die Ausführungen. Ich verneige mich vor dir! 👍 👍 👍

1.002 Beiträge seit 2007
vor 14 Jahren

Hallo dN!3L,

danke für diesen tollen Artikel! Weiter so 😃!

m0rius

Mein Blog: blog.mariusschulz.com
Hochwertige Malerarbeiten in Magdeburg und Umgebung: M'Decor, Ihr Maler für Magdeburg

42 Beiträge seit 2006
vor 14 Jahren

Hallo dN!3L,
ich möchte mich auch recht herzlich für diesen wirklich tollen Artikel bedanken 👍
Hab mich die letzte Stunde mal intensiv deinem Artikel und meinem VS gewidmet.
Echt gut nachvollziehbar und super geschrieben 🙂

In Sachen Linq, Lambda-Expressions und anonymen delegates ist mir jetzt endlich mal ein Licht aufgegangen 🙂

Einen schönen Abend noch und weiter so 👍

~ There's no knowledge that is not power~

624 Beiträge seit 2006
vor 14 Jahren

Hallo dN!3L,

Wirklich ein super Artikel 👍

Gruss
w1z4rd

5.657 Beiträge seit 2006
vor 13 Jahren

Danke für die Ausführungen. Ich verneige mich vor dir! 😮 😮 😮

Dem kann ich mich nur anschließen! Was für eine Arbeit du dir gemacht hast für so einen guten Artikel... Vielen Dank!

Weeks of programming can save you hours of planning

W
955 Beiträge seit 2010
vor 13 Jahren

Danke für den ausführlichen Artikel. Ich habe einige Fragen/Bemerkungen:
* Kontravarianz. Das Beispiel (aus dem Zusammenhang gerissen)


(string name) =>
{
    return "Hello, "+name;
}

wird erläutert mit "Da es ja Kontravarianz gibt und der Compiler zudem den Typen herleiten kann, ...". Kannst Du mal sagen wo da Kontravarianz auftaucht? Der Compiler leitet den Typen ab und fertig.
* Es wäre vorteilhaft gewesen auf die LINQ Query Expressions etwas näher einzugehen. Ich weiß dass der Artikel nicht direkt Linq behandelt und dass ein Link zu diesem Thema eingefügt wurde. Ich denke das es besser für den Leser ist ein solches Beispiel zu sehen. Viele fragen sich 'Weshalb soll ich das verwenden, da spart man vllt etwas Tiparbeit, verschlechtert aber immens die Lesbarkeit des Codes.' Gerade dieser letzte Schritt, die Query Expressions, sind das Ziel gewesen, ein mächtiges Abfragesystem mit intuiver, leicht verständlicher Syntax zu schaffen.
* Auf Expression Trees könnte noch genauer eingegangen werden. Dieses könnte ein mächtiges Werkzeug sein wenn man dem Benutzer gestattet, ausführlich seine zu bearbeitenden Daten filtern zu können, die man dann anfragen muß.
Aber vllt sollte bei der Menge an Informationen dann sowieso ein Teil 2 erscheinen, trotzdem Danke für Deine Bemühungen.

dN!3L Themenstarter:in
2.891 Beiträge seit 2004
vor 13 Jahren

Hallo zusammen,

erstmal vielen Dank für das ganze Feedback. Freut mich, dass der Artikel so gut ankommt.

@witte:

Kannst Du mal sagen wo da Kontravarianz auftaucht? Der Compiler leitet den Typen ab und fertig.

Der Text, den du zitierst, bezieht sich zwar auf einen anderen Codeausschnitt, als von dir gepostet - was aber nebensächlich ist, da du völlig recht hast. Der Compiler leitet einfach die Parametertypen aus dem Typ, an den zugewiesen werden soll, ab und fertig.

Es wäre vorteilhaft gewesen auf die LINQ Query Expressions etwas näher einzugehen. [...] Auf Expression Trees könnte noch genauer eingegangen werden.

In diesem Artikel geht es hauptsächlich - wie der Name ja schon sagt - um Delegaten, anonyme Methoden, Lambda-Ausdrücke & was damit zu tun hat.
Das LINQ-Beispiel soll verdeutlichen, wie man Delegaten einsetzen kann bzw. auch, wie man LINQ-Ausdrücke in Lamda-Form lesen und verstehen kann.
Da man mit Lamda-Ausdrücken auch Ausdrucksbäume erzeugen kann, darf dieser Teil natürlich nicht fehlen. Was man schlussendlich mit den erzeugten Ausdrucksbäumen macht oder wie man Ausdrucksbäume auf andere Art erzeugen kann, ist hier völlig nebensächlich.
Ich versuche hier, den Inhalt des Werkzeugkoffer möglichst detailliert zu erläutern und die Benutzung der Werkzeuge zu erklären. Ich will hier nicht erklären, wie man damit einen Schrank zusammenbaut (falls du damit überhaupt einen Schrank bauen willst).

Besten Gruß,
dN!3L

699 Beiträge seit 2007
vor 13 Jahren

Hallo dN!3L,

super Artikel, den Du da geschrieben hast 👍.
Und nachdem ich im letzten halben Jahr soviel wissen dazugelernt habe (hatte den Artikel mitte letzten Jahres schon einmal gelesen, und musste feststellen, das ich deinen ausführungen nicht ganz folgen kann.), kann ich dem Artikel nun auch sehr gut folgen, und konnte noch einige kleine Punkte dazulernen.

Ein riesen Dank von mir, für den super Artikel und die mühe die Du dir damit gegeben hast.

Grüße Stephan