C#: Delegaten und Lambda-Ausdrücke

Alles zu Zusi-Performance, Frameraten, ruckelnden Bildern, Grafik, Treibern usw.
Antworten
Nachricht
Autor
Benutzeravatar
Roland Ziegler
Beiträge: 5508
Registriert: 04.11.2001 22:09:26
Wohnort: 32U 0294406 5629020
Kontaktdaten:

C#: Delegaten und Lambda-Ausdrücke

#1 Beitrag von Roland Ziegler »

Ein weitere Beitrag aus der C#-Welt, diesmal mehr an Einsteiger und Schnupperer gewandt.

Bis vor gut einem Jahr war ich noch recht skeptisch, was die universelle Anwendung funktionaler Sprachelemente in C# anging. Die gab es von Anfang an, die Delegaten (delegates). Delegaten sind im Prinzip nichts anderes als typsichere Zeiger auf Funktionen. In nicht-typsicherer Form gibt es sie auch in C (und vermutlich auch in manchen Pascal-Dialekten). In C# Version 1 (C#1) bildeten Delegaten die saubere und transparente Grundlage für Ereignisse (Events), ein wesentliches Konstruktionselement grafischer Benutzeroberflächen, aber nicht ganz der reinen OO-Lehre entsprechend.

(Java ging einen anderen Weg und verpackte jedes Ereignis in eine Schnittstelle (Interface) und deren Implementierung in eine Klasse. Diese Klassen sind für Ereignisverarbeitung in der Regel anonym, und mit der Zulässigkeit solcher anonymen Klassen und vor allen deren Nebenwirkungen weicht auch Java von der reinen OO-Lehre ab.)

Die C#-Delegaten enthalten eine - möglicherweise auch bei MS zunächst gar nicht entdeckte - ungeahnte Mächtigkeit, die nur eben nicht aus dem OO-Paradigma, sondern aus der funktionalen Programmierung stammt.

Schon in C#1 sind Delegaten auch jenseits von Ereignissen oft einfacher und schneller anzuwenden, als jeweils Schnittstellen und diese implementierende Klassen zu schreiben.

Ab C#2 und vor allem mit C#3 aber tun sich neue Wege auf, die bei schließlich verbesserter Lesbarkeit weniger Schreibarbeit erfordern. Ich will das an einem Beispiel illustrieren. Das Beispiel ist nur endlich elegant, denn es ist eingebettet in das zwar weiterhin sehr praktische Windows-Forms, das aber ein wenig darunter leidet, ohne die Typsicherheit der Generika auskommen zu müssen.

Das Beispiel soll auf Knopfdruck eine Gruppe Steuerelementen aktivieren oder deaktivieren, in diesem Fall seien es Radio-Buttons.

Bild

Zunächst C#1. Die Radio-Buttons sollen sich in einer extra Liste (ArrayList) befinden. Natürlich könnte man auch Form.Controls nutzen, aber diese Collection bietet später für C#2 und C#3 nicht die erweitere Funktionalität. Der Einheitlichkeit wegen also diese Wahl.

Code: Alles auswählen

    // C# 1
    private void button1_Click(object sender, EventArgs e) {
      foreach (Object o in radioButtons1) {
        RadioButton rb = o as RadioButton;
        if (rb != null)
          rb.Enabled = checkBoxEnable.Checked;
      }
    }
Hier wird explizit über alle Objekte der Liste iteriert, der Typ aus Object in RadioButton gewandelt (eine der großen Ärgerlichkeiten aus Net 1.0/1.1) und dann der Aktivierungszustand (Enabled) entsprechend der Checkbox gesetzt. Ist ein einfacher Ansatz und einfach zu lesen. Und kommt noch ganz ohne Delegaten aus.

Mit C#2 kamen die Generika und damit Typsicherheit für Collections. Große Erleichterung. Im anschließenden ersten C#2-Beispiel aber mache ich unsere Aktivierung/Deaktivierung der Radio-Button zunächst deutliche komplexer als zuvor. Es soll hierbei das Prinzip aufgezeigt werden, dass im folgenden weiter verwendet wird.

C#2 bietet Collections, die als Methode direkt ForEach anbieten. Die Schleife läuft darin intern, ich muss sie nicht mehr ausformulieren. Meine Radio-Buttons stehen jetzt in einer List<RadioButton>. Ich will ForEach anwenden. ForEach enthält einen Parameter, nämlich das, was in der Schleife getan werden soll. Dieser Parameter ist ein Delegat, der auf eine Funktion verweist, die ausgeführt werden soll. In diesem Fall ist der Delegat vordefiniert:

Code: Alles auswählen

delegate void Action<T> (T t);
Der Delegat heißt Action. Es wird ein Parameter vom Typ T erwartet und es wird nichts zurückgegeben. Das ist aber nur die Typ-Definition des Delegaten. Um ihn anzuwenden brauchen wir zudem eine konkrete Methode, die vom Delegaten ausgeführt werden soll (an die der Delegat die Arbeit delegiert, daher der Name) sowie eine konkrete Instanz des Deelgaten, in der er an die konkrete Methode gebunden wird. Das folgende Code-Fragment benutzt noch C#1-Syntax, mit Ausnahme natürlich der Generika-Typen.

Code: Alles auswählen

    void action(RadioButton rb) {
      rb.Enabled = checkBoxEnable.Checked;
    }


    // C# 1-2 ForEach with explictit delegate
    private void button12_Click(object sender, EventArgs e) {
      Action<RadioButton> actionDelegate = new Action<RadioButton>(action);
      radioButtons2.ForEach(actionDelegate);
    }
Oben die Definition einer Methode action, die der Signatur eines Action-Delegaten entspricht und nachher die tatsächliche Arbeit macht. In der Button-Click-Ereignisbehandlung darunter wird zuerst eine konkrete Instanz des Delegaten erzeugt, in der die action-Methode eingebunden wird. Die neue typsichere Liste meiner Radio-Buttons heiße radioButtons2. Statt der expliziten Schleife rufe ich ForEach der Liste auf, mit dem Delegaten als Parameter. Dieser Aufruf ist prägnant kurz, aber der Aufwand, dahin zu kommen, doch beträchtlich. Und zunächst mal kein erkennbarer Fortschritt gegenüber dem ursprünglichen Ansatz.

C#2 hat aber noch andere Spracherweiterungen mit sich gebracht, z.B. die anonymen Methoden. Anonyme Methoden ermöglichen eine vereinfachte Schreibweise für Delegaten, bei denen Methoden-Definition und Instantiierung zusammengefasst werden (für einmalige Anwendung). Das sieht dann so aus:

Code: Alles auswählen

    // C# 2 ForEach with anonymous method
    private void button2_Click(object sender, EventArgs e) {
      radioButtons2.ForEach(delegate(RadioButton rb) {
        rb.Enabled = checkBoxEnable.Checked;
      });
    }
Wieder gibt es den Aufruf der ForEach-Methode. Diesmal ohne jedes Vorgeplänkel. Mit dem Schlüsselwort delegate wird Methode und Instanz für den Action-Parameter von ForEach an Ort und Stelle definiert. In runden Klammern finden wir Parameter-Deklaration der Methode und in geschweiften Klammern den Körper der Methode. Weder die Methode noch die Instanz des Delegaten enthalten einen Namen, daher die Bezeichnung Anonyme Methode. Nach wie vor aber vermag auch diese Darstellung nicht wirklich zu überzeugen. Vergleicht man mit der Ausführung in C#1, so bleibt das C#1-Beispiel besser lesbar und nicht wirklich komplizierter.

Bringen wir nun C#3 ins Spiel und die Lambda-Ausdrücke. Lambda-Ausdrücke sind eine Darstellungsform für Funktionen, und stammen aus der bereits oben erwähnten Funktionalen Programmierung, ein für Normalanwender immer noch fremdes Paradigma und nicht zu verwechseln mit prozeduraler Programmierung aus der Zeit vor OO. In C# sind Lambda-Ausdrücke eigentlich nur eine neue Darstellungsform für anonyme Methoden. Aber endlich sparen wir tatsächlich Code und gewinnen gleichzeitig Lesbarkeit.

Code: Alles auswählen

    // C# 3 ForEach with lambda expression
    private void button3_Click(object sender, EventArgs e) {
      radioButtons2.ForEach(rb => rb.Enabled = checkBoxEnable.Checked);
    }
Unser Aufruf zur Aktivierung/Deaktivierung der Radio-Buttons wird nun zum echten Einzeiler. Der Lambda-Operator => trennt Parameter-Deklaration von der Ausführung. Vor dem =>-Operator steht eigentlich (RadioButton rb), wie in der anonymen Methode zuvor. Da der Typ des Parameters sich aber aus dem Kontext ergibt, können wir ihn weglassen. Und wenn es nur ein einzelner Parameter ist, die runden Klammern auch. Hinter dem Operator steht eigentlich {rb.Enabled = checkBoxEnable.Checked;}, ebenfalls wie in der anonymen Methode zuvor. Geschweifte Klammern entfallen in der Lambda-Notation, und das Semikolon auch. Hätte unsere anonyme Methode einen Rückgabewert - hier nicht der Fall - so dürften wir uns auch das return sparen, Lambda sorgt schon dafür.

Vereinfachte Syntax, bessere Lesbarkeit, aber kein neues Konzept, sondern nur eine neue Darstellung für Delegaten mit anonymen Methoden: Das verbirgt sich hinter Lambda-Ausdrücken. Und wenn man den Zusammenhang mit den Delegaten verstanden hat, schwindet auch die Skepsis.

Benutzeravatar
Roland Ziegler
Beiträge: 5508
Registriert: 04.11.2001 22:09:26
Wohnort: 32U 0294406 5629020
Kontaktdaten:

Re: C#: LINQ für einfache Collections, Extension Methods

#2 Beitrag von Roland Ziegler »

Einige objektorientierte Programmiersprachen haben sich in den letzten Jahren weiterentwickelt und für ein anderes Paradigma geöffnet, dem der "funktionalen" Sprache (nicht zu verwechseln mit "prozedural").

Bekanntestes Element dabei ist der Lambda-Ausdruck, zunächst in C# eingeführt, seit letztem Jahr auch genormt für C++.

Auch die LINQ-Ausdrücke in C# ("Language Integrated Query") sind letztendlich Lambda-Ausdrücke, in etwas anderer Notation. LINQ wurde zunächst wegen seiner Fähigkeit gewürdigt, zum ersten Mal in einer klassischen universellen Programmiersprache auf saubere Art Compile-Zeit sichere Datenbankabfragen zu ermöglichen. Zuvor war immer eine Übersetzung nötig, die zumeist recht simpel über Zwischentexte läuft, deren mögliche Syntaxfehler erst zur Laufzeit ermittelt werden können.

Aber LINQ ist nicht beschränkt auf Datenbanken. LINQ funktioniert mit allen Datentypen, die die Schnittstelle IEnumerable<Type> implementieren. Dazu gehören neben sämtlichen Collections auch die beiden quasi-nativen Datentypen Array und String. Ein String ist eine Zeichenkette, eine Collection von Char.

Mein kleines LINQ-Beispiel soll die Anwendung außerhalb von Datenbanken verdeutlichen: Die simple Aufgabe sei, die Zahl der Textzeilen in einem String zu ermitteln. Der übliche Weg ist, die Zeilenvorschübe im Text zu finden, zu zählen, und zum Schluss noch 1 hinzu addieren.

Das Finden überlassen wir LINQ. Es sieht dann so aus:

Code: Alles auswählen

int numLines (string text) {
  int cnt = (from c in text
                where c == '\n'
                select c).Count() + 1;
  return cnt;
}
"from ... in ... where ... select" erinnert an SQL, in umgekehrter Reihenfolge, ist aber lediglich eine von der üblichen Syntax abweichende andere Schreibweise für Lambda-Ausdrücke.

Die Eingangsvariable des LINQ-Ausdrucks ist text vom Typ String. String implementiert IEnumerable<Char>. Damit ist klar, dass ein Element in String vom Typ Char sein muss. Damit ist der Typ von c zu Char definiert. In der where-Klausel erfolgt der Verglich von c mit dem Zeilenumbruch, Funktionalität innerhalb des Lambda-Ausdrucks. Die select-Klausel wiederum definiert den Rückgabewert. Für LINQ ist definiert, dass die Rückgabewerte aggregiert werden und das Aggregat wiederum IEnumerable implementiert, und zwar vom Typ des Ergebnisses des select-Klausel, hier wieder Char. Die Methode Count() ist ein aus dem Extension-Fundus (s.u.) und gibt die Anzahl der Aggregat-Elemente an, also die Anzahl Zeilenvorschübe. Plus 1 und wir haben die Anzahl Zeilen.

Was aussieht wie Hokus-Pokus ist reinstes C#, ohne Tricks und doppelten Boden. Das kann man belegen, wenn man den erzeugten Code über Reflection auflöst:

Code: Alles auswählen

int cnt = text.Where<char>((char c) => {
    bool flag = c == '\n';
    return flag;
  }
).Count<char>() + 1;
Jetzt steht hier Standard-Syntax für Lambda.

Ein Schmankerl und teils notwendige Voraussetzung, teils hilfreiche Vereinfachung für LINQ und vieles andere in der Nutzung von Lambda sind die Extension-Methods. eien Fähigkeit von C# Version 3, bestehende Klassen und Interfaces nachträglich und anwendungsbezogen um neue Methoden zu erweitern. Diese Methoden sind von Natur aus statisch, eine notwendige Einschränkung.

String in seiniger heutigen Form (mit IEnumerable<Char>) existiert seit C# Version 2, LINQ aber erst seit C# Version 3. Weder IEnumerable<T> noch String kennen eine Where<T>() Methode, genauso wenig wie sie Count<T>() kennen. Where<T>() und Count<T>() als Mitbringsel von C# 3 sind entsprechend auch in einer anderen Assembly als String oder IEnumerable<T> definiert.

Was auf den ersten Blick wie Bastelei aussieht, stellt sich bei näherem Hinsehen als überaus mächtig heraus, vor allem, da Extension-Methods kein exklusives Privileg für .Net-Framework eigene Klassen sind, sondern von jedermann für jede Art Klasse verwendet werden können.

Dazu abschließend zwei Beispiele:

Ähnlichkeitsvergleich zweier Gleitkommazahlen:

Code: Alles auswählen

public static bool Match (this double val1, double val2, double precision) {
  return Math.Abs (val1 - val2) <= precision;
}

Code: Alles auswählen

double d1 = 1.0;
 double d2 = 1.001;
 bool match = d1.Match (d2, 1e-3);
Textausgabe von Objektinhalten mit automatischer Einrückung:

Code: Alles auswählen

public static void Dump (this object o, TextWriter osm, Indent ind) {
  osm.Write (ind);
  osm.WriteLine (o.ToString ());
}

Code: Alles auswählen

MyClass myClass = <some data source>;
Indent ind = <some indentation source>;
myClass.Dump (Console.Out, ind);
(Die Einrückung wird durch die Klasse Indent gewährleistet, eine typische Anwendung für RAII, Resource Acquisition Is Initialization.)
Zuletzt geändert von Roland Ziegler am 27.06.2012 12:26:09, insgesamt 1-mal geändert.

Antworten