CodeGym /Kurse /C# SELF /Erfassung von Variablen ( Cl...

Erfassung von Variablen ( Closures)

C# SELF
Level 49 , Lektion 4
Verfügbar

1. Was ist ein Closure?

In der Programmierung ist ein Closure (closure) eine Funktion, die Variablen aus einem äußeren Kontext erfasst. Einfach gesagt: Wenn ein Lambda-Ausdruck oder eine anonyme Methode Variablen verwendet, die außerhalb ihres Körpers deklariert sind, wird diese Funktion zu einem Closure. Sie "erinnert" sich daran, welche Werte diese Variablen zum Zeitpunkt der Erstellung hatten.

Alltags-Analogie:
Stell dir vor, du hast ein geheimes Rezept auf einem Zettel notiert und in einen Umschlag gesteckt. Selbst wenn der Zettel später verloren geht oder nicht mehr direkt zugänglich ist (die Variable ist nicht mehr direkt verfügbar), hat jemand mit diesem Umschlag (dem Lambda) immer noch Zugriff auf das Rezept.

Ein einfaches Beispiel:

int x = 42;
Func<int> getX = () => x;
Console.WriteLine(getX()); // 42

Hier ist getX ein Closure, weil es die außerhalb deklarierte Variable x verwendet.

2. Warum ist das Erfassen von Variablen wichtig?

In C# werden Closures praktisch überall verwendet:

  • In Collections und LINQ-Abfragen
  • Um Parameter an Events oder asynchrone Methoden zu übergeben
  • Beim Erstellen von Event-Handlern in Schleifen
  • Um einen "Kontext" zwischen verschiedenen Aufrufen zu speichern

Ohne Closures wären viele gängige Praktiken in C# entweder unmöglich oder sehr umständlich.

Beispiel aus der Praxis

Angenommen, wir entwickeln eine Erinnerungs-App: Der Benutzer legt eine Reihe von Erinnerungen an, und irgendwann (in einer Minute, einer Stunde, einer Woche...) soll die App die richtige Nachricht anzeigen. Es ist einfach, dem Handler ein Lambda zu übergeben, das sich merkt, was genau erinnert werden soll. Das ist klassische Variable-Erfassung.

3. Wie implementiert C# die Erfassung von Variablen

Hinter den Kulissen macht C# einen Trick: Wenn du ein Lambda hast, das externe Variablen benutzt, erstellt der Compiler automatisch eine Hilfsklasse — eine display class. Alle "erfassten" Variablen werden zu Feldern dieser Klasse.

Im Schema sieht das so aus:


Äußere Variable ──► DisplayClass
                         ▲
                         │
                    Closure (Lambda)

Illustration im Code

So passiert es "unter der Haube":

int x = 5;
Func<int> f = () => x;
// Hier macht der Compiler ungefähr Folgendes:
class DisplayClass
{
    public int x;
    public int Lambda() => x;
}
DisplayClass display = new DisplayClass();
display.x = 5;
Func<int> f = display.Lambda;

Das erklärt, warum das Closure weiterhin den aktuellen Wert der Variablen sieht, selbst nachdem man den Block verlassen hat, in dem sie deklariert wurde.

4. Wird der Variablenwert "eingefroren" oder verändert?

In C# werden Variablen per Referenz erfasst, nicht per Wert. Das heißt: Wenn ein Lambda eine Variable nutzt und diese Variable an anderer Stelle geändert wird, sieht das Lambda den neuen Wert.

Beispiel:

int x = 10;
Func<int> getX = () => x;

x = 20;
Console.WriteLine(getX()); // 20, nicht 10!

Studierende erwarten oft, dass getX() immer 10 zurückgibt, weil die Variable "erfasst" wurde. In Wirklichkeit liest das Lambda die Variable, die noch existiert und verändert werden kann.

Wann wird der Wert trotzdem festgehalten?

Wenn die Variable in einer Schleife mit einer neuen Sichtbarkeit pro Iteration deklariert wird, zum Beispiel bei foreach, und für jede Iteration eine neue Variable erzeugt wird, "merkt" sich das Lambda den aktuellen Wert.

5. Beispiele: Closure in Schleifen — eine typische Falle

Häufiger Fehler

Wir wollen ein Array von Delegates erstellen, wobei jedes seinen eigenen Schleifenindex ausgibt:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    actions[i] = () => Console.WriteLine(i);
}
foreach (var action in actions)
    action();

Was gibt das Programm aus?

5
5
5
5
5

Ups! Warum nicht 0,1,2,3,4?

Grund:
Das Lambda erfasst dieselbe Variable i, die sich während der Schleifeniteration weiter verändert. Wenn du später die Delegates aufrufst, ist i bereits 5.

Wie macht man es richtig?

Man muss eine separate Variable im Schleifenrumpf erzeugen:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    int index = i; // Neue Variable für jede Iteration!
    actions[i] = () => Console.WriteLine(index);
}
foreach (var action in actions)
    action();

Jetzt gibt das Programm aus:

0
1
2
3
4

Wie hängt das mit display class zusammen?

Im ersten Fall hängen sich alle Delegates an dasselbe Feld — deshalb ist das Ergebnis identisch. Im zweiten Fall wird für jede Iteration eine neue lokale Variable erzeugt, und damit für jeden Delegate eine eigene DisplayClass mit einem eindeutigen Wert.

6. Praktische Szenarien für den Einsatz von Variablen-Erfassung

Beispiel 1: Event-Handling mit "Kontext"

Angenommen, in unserer kleinen App gibt es eine Liste von Tasks, und jedem ist ein Click-Handler für einen Button zugeordnet. Das Lambda im Handler soll sich merken, welchen Task es bearbeiten muss:

foreach (var task in tasks)
{
    button.Click += (sender, e) => CompleteTask(task);
}

Hier wird die Variable task in jeder Iteration erfasst. Es ist wichtig sicherzustellen, dass sie korrekt innerhalb der Schleife deklariert ist, damit man nicht in die oben beschriebene Falle tappt.

Beispiel 2: Asynchrone Operationen

Oft verwendet man Closures, um Parameter an asynchrone Logik zu übergeben — zum Beispiel indem man die Variable in einen lokalen "Slot" beim Start der asynchronen Aufgabe speichert:

for (int i = 0; i < 3; i++)
{
    int index = i; // Unbedingt!
    Task.Run(() => Console.WriteLine($"Task #{index}"));
}

Ohne lokale Variable würden alle Tasks dieselbe Nummer ausgeben, was meist nicht gewünscht ist.

Beispiel 3: LINQ-Abfragen

LINQ auf Collections nutzt oft Closures, um Elemente unter Berücksichtigung externer Variablen zu filtern oder zu transformieren. Zum Beispiel:

string prefix = "Task";
var filtered = tasks.Where(t => t.Name.StartsWith(prefix));

Hier merkt sich das Lambda in Where den Wert von prefix und ruft die Methode StartsWith auf.

7. Besonderheiten, Einschränkungen und typische Fehler beim Arbeiten mit Closures

Fehler Nr.1: Benutzung einer einzigen Variablen in der Schleife für alle Delegates.
Wenn in einer Schleife alle Delegates auf dieselbe Variable verweisen, ist das Ergebnis überraschend. Es ist wichtig, innerhalb der Schleife für jedes Delegate eine neue lokale Variable zu erzeugen, um eine gemeinsame Referenz zu vermeiden.

Fehler Nr.2: Closures auf Variablen außerhalb der Methode.
Wenn ein Closure ein Klassenfeld oder eine Variable erfasst, die außerhalb der aktuellen Methode deklariert ist, hält es eine Referenz auf diese Variable. Das kann zu Memory-Leaks führen, weil der Garbage Collector das Objekt nicht freigeben kann, solange es Referenzen aus Closures gibt.

Fehler Nr.3: langlebige Delegates mit Closures.
Wenn ein Delegate mit Closure lange gespeichert wird (z.B. in einem statischen Feld), bleiben auch die Variablen, auf die es verweist, länger im Speicher als erwartet. Das ist oft die Ursache für versteckte Memory-Leaks und Performance-Probleme.

1
Umfrage/Quiz
Lambda-Ausdrücke, Level 49, Lektion 4
Nicht verfügbar
Lambda-Ausdrücke
Syntax von Lambda-Ausdrücken
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION