C# im Fokus

Effizienter Programmieren mit Closure (Teil 2)
Kommentare

Im letzten Teil der C#-Kolumne wurden Delegates und anonyme Methoden beleuchtet. Anonyme Methoden erlauben im Zusammenspiel mit Delegates nun auch in C# die Verwendung von Closure-Konstrukten. Aber was ist überhaupt ein Closure? Dieser Frage gehen wir in dieser Ausgabe der C#-Kolumne nach.

Verwendung von Closures

Die definierte Anforderung alle Namen mit einem bestimmten Buchstaben herauszufiltern, wurde mit der oberen Implementierung gelöst. Die gezeigte Umsetzung hat jedoch einen gravierenden Nachteil, da der entsprechende Buchstabe hart kodiert wurde. I. d. R. müssen Filterkriterien jedoch dynamisch zur Laufzeit angepasst werden können, d. h., der festgelegte Buchstabe „M“ innerhalb des Filters muss dynamisiert werden. Für den Delegate bedeutet dies, er muss eine Information aus dem umgebenden Kontext beziehen. Listing 5 zeigt dazu eine mögliche Umsetzung.

public static void FindNames() {
  string literal = "S";
  Predicate match = (delegate(string toCheck) {
    return toCheck.StartsWith(literal);
  });
  List result = nachnamen.FindAll(match);
  foreach (string entry in result)
    Console.WriteLine(entry);
}  

Die Variable literal wurde außerhalb des Delegates an- und mit dem Wert „S“ belegt. Innerhalb der Delegate-Funktion wird auf die äußere Variable zugegriffen und der Wert der Variablen ausgewertet. Wird das so abgeänderte Beispiel ausgeführt, ergibt sich die in Abbildung 1 dargestellte Ausgabe.

Abb. 1: Ausgabe der gefilterten Liste mittels äußeren Parameters
Abb. 1: Ausgabe der gefilterten Liste mittels äußeren Parameters

An dieser Stelle ist nun ein Closure entstanden und die Variable literal wird in diesem Kontext als upvalue bezeichnet. Wichtig ist an dieser Stelle zu verstehen, dass ein Closure eine umgebende Kontextvariable kapselt. Dies wird anhand des Beispiels in Listing 6 deutlich.

public static void FindNames() {
  string literal = "S";
  Predicate match = (delegate(string toCheck)  {
    return toCheck.StartsWith(literal);
  });
  List result = nachnamen.FindAll(match);
  foreach (string entry in result)
    Console.WriteLine(entry);
  literal = "Z";  // Neue Zuweisung eines Wertes
  result = nachnamen.FindAll(match);
  foreach (string entry in result)
    Console.WriteLine(entry);
}  

Dort wird ein zweites Mal die Filterfunktion mit dem erzeugten Predicate aufgerufen, lediglich die Variable literal wird verändert. Wie nun anhand der Konsolenausgabe in Abbildung 2 deutlich wird, bezieht der Closure jeweils den aktuellen Wert der Variablen.

Abb. 2: Verwendung des gleichen Delegates mit verändertemVariablenwert
Abb. 2: Verwendung des gleichen Delegates mit verändertemVariablenwert

Dies trifft sogar zu, wenn der Gültigkeitsbereich der verwendeten Variablen nicht mehr existiert. Listing 7 zeigt, wie ein Predicate Delegate innerhalb einer Methode angelegt und zurückgegeben wird.

public static Predicate CreatePredicate() {
  string localVariable = "S";
  Predicate match = (delegate(string toCheck) {
    return toCheck.StartsWith(localVariable);
  });
  return match;
}  

Intern nutzt der Delegate die lokale Variable localVariable. Diese Variable ist eigentlich nur innerhalb der Methode CreatePredicate gültig, dennoch kann der zurückgegebene Closure weiterhin auf die Variable zugreifen.

Closure im Detail

Wie in den zuvor dargestellten Beispielen deutlich wurde, ist ein Closure in der Lage, äußere Variablen innerhalb seines Kontexts zu konservieren. Ändert sich der Wert der äußeren Variablen, sieht ebenfalls die Closure-Variable den neuen Wert. Ein so ähnliches Verhalten könnte mit Referenzvariablen abgebildet werden, Closures verwenden hingegen ein anderes Verfahren. Der Compiler erstellt aus einem Closure intern eine neue verschachtelte, private und geschlossene (sealed) Klasse. Die verwendeten äußeren Variablen werden zu öffentlichen (public) Membervariablen der neuen Klasse, d. h., aus einem Closure-Konstrukt wird zur Übersetzungszeit eine neue Klasse erstellt. Da die verwendeten äußeren Variablenwerte innerhalb der neuen Klasse in eigene Membervariablen gespeichert werden, sind sie auch noch zugänglich, wenn der ursprüngliche Kontext der Variable ungültig geworden ist. Wird dieses Verhalten nicht beachtet, kann es leicht zu falschen Ergebnissen führen, wie es das Beispiel in Listing 8 demonstriert.

private static void Demo() {
  Action[] func = new Action[10];
  for (int i = 0; i < 10; i++) {
    // int j = i;
    func[i] = (delegate() {
      Console.WriteLine("Wert von i: {0}", i); 
// anstatt i hier j verwenden
    });
  }
  foreach (Action item in func)
    item();
}  

Die for-Schleife erstellt 10 Action-Delegates, innerhalb des Delegates wird die äußere Variable i verwendet und lediglich auf der Konsole ausgegeben. Nachdem alle Delegates erstellt wurden, werden die gespeicherten Delegates nacheinander aufgerufen. Abbildung 3 zeigt die Konsolenausgaben nach der Ausführung.

Abb. 3: Closures beziehen jeweils die aktuellen Werte einer äußeren Variablen
Abb. 3: Closures beziehen jeweils die aktuellen Werte einer äußeren Variablen

Wie zu erkennen ist, geben alle gespeicherten Delegates jeweils den letzten Wert der Variablen i aus und nicht den Wert, den die Variable zur Erstellungszeit des Delegates besaß. Um dieses Problem zu umgehen, muss innerhalb der for-Schleife eine neue Variable angelegt werden. Der aktuelle Wert der Variablen i wird bei jedem Schleifendurchlauf der neuen Variablen zugewiesen. Innerhalb des Delegates wird anschließend die neue Variable verwendet. In Listing 8 sind die notwendigen Änderungen bereits kommentiert dargestellt.

Zusammenfassung

Mittels Closures können Algorithmen effizienter und einfacher umgesetzt werden. Zu beachten sind die Konservierung und die Verwendung von äußeren Variablen. Durch die Konservierung überleben verwendete äußere Variablen innerhalb eines Delegates, auch wenn der Gültigkeitsbereich der Variablen schon nicht mehr existiert. Solange jedoch der Gültigkeitsbereich der äußeren Variablen noch besteht, ist jede Wertänderung auch innerhalb des Closures sichtbar.

Marc André Zhou arbeitet als Senior Consultant bei der Logica Deutschland GmbH & Co. KG. Seine Schwerpunkte liegen im Bereich Softwarearchitekturen und Frameworks, hier hauptsächlich im .NET-Umfeld. Sie erreichen ihn per E-Mail.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -