C# im Fokus

Effizienter Programmieren mit Closure
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.

Programmiersprachen werden stetig weiterentwickelt und von Version zu Version wächst – neben der Anzahl der Framework-Klassen – auch der syntaktische Sprachumfang. Dies gilt auch für das Sprachkonzept Closure. In der ersten C#-Version wurde dieses Sprachkonstrukt noch nicht direkt unterstützt, erst mit .NET 2.0 wurde es möglich, Closure-nahe Implementierungen einfach zu realisieren. Vor .NET 2.0 konnten Closure-Konstrukte nur durch explizite Implementierung innerer Klassen umgesetzt werden. Closures sind dabei keine neue Erfindung, Programmiersprachen wie Ruby unterstützen schon seit geraumer Zeit Closures. Das Closure-Konzept selbst wurde in den 60er- Jahren entwickelt und erstmals in der Programmiersprache Scheme – ein Dialekt der Sprache Lisp – vollständig unterstützt. Oft werden Closures im C#-Kontext mit anonymen Delegates gleichgesetzt, dem ist aber nicht so. Anonyme Delegates und Closure stellen zwei unterschiedliche Konzepte dar.

Funktionale Welt

Der Ursprung des Closure-Konzepts liegt in funktionalen Programmiersprachen wie Lisp oder Ruby. Damit Closures auch in imperativen Programmiersprachen verwendet werden können, muss es möglich sein, Funktionen als Parameter übergeben zu können. Funktionen können somit eine Aufrufkette bilden, in der Ergebnisse einer Berechnung einen weiteren Funktionsaufruf auslösen. Funktionale Programmiersprachen nutzen diese als „Continuation Passing Style“ bezeichnete Technik, um Ergebnisse einer Berechnung direkt zu einem weiteren Funktionsaufruf weiterzuleiten. Innerhalb von C# steht seit der ersten Version der Delegate-Typ bereit, der es ermöglicht, einen Funktionsaufruf über einen Parameter weiterzugeben. Listing 1 zeigt dazu ein einfaches Beispiel.

delegate void FuncCall();
class Program {
  public static void A() {
    Console.WriteLine("Delegate wurde aufgerufen ...");
  }
  public static void B(FuncCall func) {
    Console.WriteLine("Delegate Aufruf folgt.");
    func();
  }
  static void Main(string[] args) {
    B(A);
  }
}  

Der Methode B kann über den Parameter ein definierter Delegate übergeben werden. Innerhalb der Methode B wird dann der übergebene Delegate aufgerufen. Zusätzlich zeigt Listing 2 zwei weitere Möglichkeiten, einen Delegate anzulegen und zu übergeben.

static void Main(string[] args)
{
  B(delegate {
    Console.WriteLine("Delegate wurde angelegt.");
  });
  B(() => {
    Console.WriteLine("Delegate wurde angelegt.");
  });
}  

Da die dargestellten Möglichkeiten bereits im ersten Teil der Kolumne ausführlich beschrieben wurden, wird hier nicht weiter darauf eingegangen.

Verwendung von Delegates

Das .NET Framework erlaubt an vielen Stellen die Verwendung von Delegates, um Funktionalitäten per Parameter zu übergeben. Ein klassisches Beispiel dazu ist die Erstellung eines neuen Thread-Objekts, wobei direkt ein Delegate zur Ausführung übergeben werden kann. Auch viele Collection-Klassen besitzen Methoden, die ein Delegate als Parameter erwarten. Oft führen diese (Delegate-)Methoden dann logische Vergleiche der Collection-Elemente durch, um z. B. Filter zu realisieren. Um die Arbeitsweise von Closures besser erläutern zu können, wird das nachfolgende Beispiel schrittweise zu einem Closure modifiziert. Das Beispiel ist recht trivial: Aus einer Namensliste sollen nur die Namen, die mit einem bestimmten Buchstaben beginnen, herausgefiltert und in eine neue Liste geschrieben werden. Diese Anforderung kann auf mehrere Arten umgesetzt werden. Im klassischen Fall würde eine Schleife über die Einträge iterieren und per if-Konstrukt die passenden Einträge in eine neue Liste schreiben. Das .NET Framework ermöglicht allerdings unter Zuhilfenahme von Delegates eine wesentlich effizientere Lösung, um die Daten zu filtern. Listing 3 zeigt die umgesetzte Lösung.

class Program {
  public static void FindNames() {
    List result = nachnamen.FindAll
                          (new Predicate(IsMatchCriteria));
    foreach(string entry in result)
      Console.WriteLine(entry);
  }
  public static bool IsMatchCriteria(string toCheck) {
    return toCheck.StartsWith("M");
  }
}  

Die generische Klasse List ermöglicht über die Methode FindAll, ein Predicate zu übergeben. Bei dem Predicate handelt es sich um einen Funktionszeiger, der für jeden Eintrag in der Liste aufgerufen wird. Der Funktionszeiger wird mit der Methode IsMatchCriteria assoziiert. Diese Methode gibt je nach Prüfung true bzw. false zurück. Auf die zusätzliche Implementierung der Methode IsMatchCriteria kann – wie bereits erläutert wurde – dank anonymer Delegates verzichtet werden (Listing 4).

public static void FindNames() {
  List result = nachnamen.FindAll(delegate(string toCheck) { 
    return toCheck.StartsWith("M");
  });
}  
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -