1. Einleitung
In der Programmierung ist ein Closure kein Weg, eine Tür in JavaScript zuzumachen, sondern ein Mechanismus, bei dem ein Lambda-Ausdruck oder eine anonyme Methode Variablen aus dem umgebenden Kontext aufnimmt und sich an sie „erinnert“, selbst nachdem der Block, in dem sie deklariert wurden, beendet ist. Einfach gesagt: Ein Closure ist eine Funktion, die sich die Bedingungen, unter denen sie entstanden ist, gemerkt hat und diese Werte wie einen kleinen Koffer mit persönlichen Sachen (Variablen) aufbewahrt.
Ein Closure ist eine Funktion zusammen mit der Umgebung (scope), die zum Zeitpunkt ihrer Erstellung existierte.
Das einfachste Beispiel für ein Closure
Lass uns das praktisch aufdröseln:
Func<int> MakeCounter()
{
int count = 0;
return () =>
{
count++;
return count;
};
}
Wir rufen so auf:
var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3
Wie funktioniert das?
- Die Variable count ist innerhalb der Methode MakeCounter deklariert.
- Das Lambda () => { ... } wird nach außen zurückgegeben und lebt jetzt außerhalb der Methode.
- Aber! Es erinnert sich an die Variable count, obwohl die Methode MakeCounter längst beendet ist.
Das ist ein Closure: das Lambda hat die Variable count aus dem umgebenden Kontext „eingeschlossen“ (captured).
Was genau „captured“ das Lambda?
- lokale Variablen aus der umgebenden Methode (scope),
- Methodenparameter,
- Variablen in Blöcken (for, foreach usw.).
Wichtig: Variablen werden nicht nach Wert, sondern nach Referenz captured! Wenn wir die Variable im Closure ändern, ändert sie sich auch „draußen“. Tatsächlich erzeugt der C#-Compiler für diese Variablen eine spezielle Hilfsklasse — das reicht aber als konzeptionelle Erinnerung, um sicher mit Closures zu arbeiten.
2. Closure und lexikalischer Gültigkeitsbereich
Wir erweitern das Beispiel der „Funktionsfabrik“ mit einem Closure:
Func<int, int> PowerFactory(int power)
{
return x =>
{
int result = 1;
for (int i = 0; i < power; i++)
result *= x;
return result;
};
}
Verwendung:
var square = PowerFactory(2); // x^2
var cube = PowerFactory(3); // x^3
Console.WriteLine(square(5)); // 25
Console.WriteLine(cube(2)); // 8
Die Funktionen square und cube wurden mit unterschiedlichen Werten der Variable power erzeugt und jede erinnert sich an ihren eigenen Wert. Für jeden Aufruf von PowerFactory wird ein eigener „Rucksack“ mit den captured Werten erzeugt.
3. Mutation von gecaptureten Variablen
Man fragt sich manchmal: Was passiert, wenn man in einer Schleife mehrere Lambdas erstellt, die eine Schleifenvariable capture? Hier treten leicht Fallstricke auf.
Beispiel: Closure in einer Schleife
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
action(); // ???
Was erwartest du? 0, 1, 2? Tatsächlich passiert folgendes:
3
3
3
Warum? Alle Lambdas referenzieren dieselbe Variable i. Am Ende der Schleife ist i bereits 3, und genau diesen Wert sehen alle unsere Actions.
Behobene Variante
Damit jedes Lambda seinen „eigenen“ Wert captured, erzeugen wir eine neue Variable innerhalb des Schleifenblocks:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}
foreach (var action in actions)
action(); // 0 1 2
Jetzt ist copy bei jeder Iteration eine neue Variable und das Closure captured genau diese.
4. Anwendung von Closures in realen Aufgaben
Datenverarbeitung und Callbacks
Wenn du asynchron arbeitest oder Ausführung verschiebst (Event-Handler, Filterung, Task-Scheduling), erlaubt ein Closure, Logik zusammen mit Parametern zu „verpacken“. Zum Beispiel:
void ProcessList(List<int> list, int threshold)
{
var filtered = list.Where(x => x > threshold);
foreach (var item in filtered)
Console.WriteLine(item);
}
Hier captured das Lambda in Where die Variable threshold.
Erstellen von „Fabriken“ für Funktionen
Parameter übergeben — Funktion mit diesem eingebauten Parameter zurückbekommen. Dieser Trick ist praktisch zur Konfiguration von Filtern, Vergleichern für Sortierungen, UI-Reaktionen usw.
Zustand verwalten
Manchmal will man ein bisschen Zustand ohne eigene Klasse speichern:
Func<string, string> CreateGreeting()
{
string prefix = "Hallo";
return name =>
{
return $"{prefix}, {name}!";
};
}
5. Nützliche Details
Unter der Haube: wie Closures in C# funktionieren
Alles, was vom Lambda captured wird, verwandelt der Compiler in eine Hilfsklasse: Variablen werden zu Feldern und das Lambda zu einer Methode. Deshalb lebt der Zustand der Variablen zwischen Aufrufen weiter.
Jede „Fabrik“ erzeugt ein kleines Objekt. Das ist normal — .NET verwaltet solche Objekte effizient und alloziert sie nur, wenn es wirklich nötig ist.
Merke: capture keine „großen“ Objekte ohne Grund
Wenn du ein großes Objekt (z. B. ein UI-Formular) captured, wird es nicht freigegeben, solange das Lambda lebt. Klassischer Grund für Leaks ist das Abonnieren von Events mit einem Lambda, das einen „schweren“ Kontext captured und das fehlende Unsubscribe (+=/-=).
Closures und Lebenszeit von Collections — Beispiel mit LINQ
Closures machen LINQ flexibel: Filter merken sich ihre Parameter.
List<string> colors = new List<string> { "Red", "Green", "Blue", "Yellow" };
string startsWith = "B";
var filtered = colors.Where(c => c.StartsWith(startsWith));
foreach (var color in filtered)
Console.WriteLine(color); // "Blue"
Wenn man dann startsWith ändert, ändert sich auch das Ergebnis:
startsWith = "R";
foreach (var color in filtered)
Console.WriteLine(color); // "Red"
Das passiert, weil das Closure auf dieselbe Variable startsWith verweist und die Methode StartsWith jedes Mal den aktuellen Wert prüft.
6. Typische Fehler bei der Arbeit mit Closures
Fehler Nr.1: capture einer Variable, die sich vor der Nutzung ändert.
Klassische Situation in Schleifen: ein Lambda im Closure „blickt“ auf dieselbe Schleifenvariable, die zum Zeitpunkt des Aufrufs schon einen anderen Wert hat. Ergebnis: die Funktion arbeitet nicht mit den erwarteten Daten. Lösung: eine separate Variable innerhalb der Schleife einführen.
Fehler Nr.2: capture zu viel Kontext.
Ein Closure zieht das ganze Objekt mit statt nur eines bestimmten Feldes/Werts. Das erzeugt unnötige Abhängigkeiten und macht den Code komplizierter. Capture nur das, was nötig ist.
Fehler Nr.3: Halten schwerer Ressourcen und Memory Leak.
Subscription auf ein Event per Lambda, das ein „schweres“ Objekt captured, und kein Unsubscribe — das Objekt wird nicht freigegeben. Achte auf die Lebenszeit von Subscriptions und benutze explizites Unsubscribe (-=).
Fehler Nr.4: Verlust der Übersichtlichkeit im Code.
Übermäßiger Gebrauch von Closures macht es schwer zu verstehen, woher Daten kommen und wie sie sich ändern, besonders wenn das Closure weit entfernt vom Aufruf deklariert ist. Halte Logik nahe beieinander und missbrauche Closures nicht.
GO TO FULL VERSION