CodeGym /Kurse /C# SELF /Lambda-Ausdrücke und Delegates zum Vergleichen

Lambda-Ausdrücke und Delegates zum Vergleichen

C# SELF
Level 30, Lektion 2
Verfügbar

1. Einführung

Stell dir vor, du hast eine Liste von Studierenden und musst sie einmal nach Alter, dann nach Nachname und dann nach Durchschnittsnote sortieren – aber nur für die, die alle Prüfungen bestanden haben. Wenn du für jeden solchen "Einmal-Vergleich" eine eigene Klasse schreibst, die IComparer<T> implementiert, wird dein Projekt schnell zu einem Friedhof kleiner Vergleichsklassen. Das ist unpraktisch: Der Code wird aufgebläht und unübersichtlich.

Für solche Fälle bietet C# eine elegantere Lösung: Du kannst die Vergleichslogik direkt an die Methode Sort übergeben, ohne eine eigene Klasse zu bauen.

Dafür brauchen wir Delegates und ihre kompakten Brüder – Lambda-Ausdrücke.

Delegates – unsere flexiblen Helfer

Bevor wir zu Lambdas kommen, lass uns klären, was ein Delegate ist. Einfach gesagt: Ein Delegate ist ein Typ, der einen Verweis auf eine Methode speichert. Klingt ein bisschen metaphysisch, oder? Stell es dir so vor:

Du hast eine To-Do-Liste, und manche dieser Aufgaben sind "Anleitungen" oder "Rezepte". Ein Delegate ist wie eine spezielle Variable, die einen Verweis auf so ein "Rezept" (Methode) speichern kann. Und wenn du die Aufgabe erledigen willst, rufst du einfach die Delegate-Variable auf, und sie "ruft" die Methode auf, auf die sie zeigt.

In C# werden Delegates für Callbacks (Methoden später aufrufen, meist als Reaktion auf ein Event), Event-Handling (z.B. Button-Klicks) und natürlich zum Übergeben von Methoden als Argumente an andere Methoden verwendet (damit eine Methode eine andere Methode aufrufen kann) – genau das brauchen wir jetzt fürs Sortieren.

Die Methode List<T>.Sort() hat mehrere Überladungen (Versionen), und eine davon nimmt einen speziellen Delegate namens Comparison<T>.

2. Der Delegate Comparison<T>

Was ist Comparison<T>?

Comparison<T> ist ein in .NET eingebauter Delegate, der speziell dafür gemacht ist, zwei Objekte vom Typ T zu vergleichen. Sein "Rezept" sieht so aus: Er nimmt zwei Objekte vom Typ T entgegen (nennen wir sie x und y) und gibt eine Ganzzahl (int) zurück:

  • Eine negative Zahl (z.B. -1), wenn x "kleiner" als y ist.
  • Null (0), wenn x "gleich" y ist.
  • Eine positive Zahl (z.B. 1), wenn x "größer" als y ist.

Nach genau diesen Regeln funktionieren auch IComparable.CompareTo und IComparer.Compare. Die Logik ist also die gleiche, nur dass wir sie jetzt als "Methodenvariable" übergeben können, statt als eigene Klasse.

Schauen wir uns das mal am Beispiel an. Zurück zu unseren Studierenden. Angenommen, wir haben eine Klasse Student:

public class Student
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public double AverageGrade { get; set; }

    public Student(string firstName, string lastName, int age, double averageGrade)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
        AverageGrade = averageGrade;
    }

    public void PrintInfo()
    {
        Console.WriteLine($"Student: {FirstName} {LastName}, Alter: {Age}, Note: {AverageGrade:F2}");
    }
}

Um jetzt die Liste der Studierenden nach Alter zu sortieren, können wir eine eigene statische Methode schreiben, die der Signatur von Comparison<Student> entspricht:

public class Program
{
    // Methode, die der Signatur von Comparison<Student> entspricht
    // Sie vergleicht zwei Studierende nach ihrem Alter
    public static int CompareStudentsByAge(Student student1, Student student2)
    {
        // Wir nutzen die eingebaute CompareTo-Methode für Zahlen,
        // die -1, 0 oder 1 je nach Vergleich zurückgibt.
        return student1.Age.CompareTo(student2.Age);
    }

    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("Ivan", "Petrov", 20, 4.5),
            new Student("Maria", "Sidorova", 22, 4.8),
            new Student("Alexey", "Ivanov", 19, 3.9),
            new Student("Elena", "Kozlova", 20, 4.2) // Zwei Studierende mit gleichem Alter
        };

        Console.WriteLine("--- Liste der Studierenden vor dem Sortieren ---");        
        foreach (var s in students)
            s.PrintInfo();

        Console.WriteLine("--- Sortiere Studierende nach Alter (mit Delegate) ---");
        students.Sort(CompareStudentsByAge); //Wir übergeben die Methode CompareStudentsByAge als Parameter

        foreach (var s in students)
            s.PrintInfo();
    }
}

Code erklärt:

  1. Wir haben eine statische Methode CompareStudentsByAge erstellt, die zwei Studierende nimmt und ein int zurückgibt, wie es Comparison<Student> verlangt.
  2. In Main haben wir eine Liste von Studierenden erstellt.
  3. Wenn wir students.Sort(CompareStudentsByAge); aufrufen, rufen wir die Methode CompareStudentsByAge() nicht sofort auf! Wir übergeben nur einen Verweis auf diese Methode. List<T>.Sort() ruft dann unsere Methode CompareStudentsByAge so oft auf, wie sie es für die Sortierung braucht, mit verschiedenen Studierendenpaaren. Das ist so, als würdest du jemandem die Lieferadresse geben, statt gleich den ganzen LKW loszuschicken.

Dieser Ansatz ist viel angenehmer, als für jeden kleinen Sortierfall eine eigene Vergleichsklasse zu bauen. Aber es geht noch kürzer!

3. Jetzt kommen Lambda-Ausdrücke

Sogar das Schreiben einer eigenen Methode wie CompareStudentsByAge kann übertrieben wirken, wenn die Vergleichslogik einfach ist und nur ein- oder zweimal gebraucht wird. Für solche Fälle wurden in C# Lambda-Ausdrücke eingeführt.

Was ist ein Lambda-Ausdruck? Im Grunde ein anonymer Methodencode oder, wie ich gern sage, eine "methodenlose Methode". Damit kannst du ein kleines Stück Code (Methode) direkt dort schreiben, wo du es brauchst, ohne es extra zu deklarieren. Das ist wie eine schnelle Notiz auf einem Post-it, die du direkt an die Aufgabe klebst, statt ein ganzes Handbuch zu schreiben.

Der Hauptoperator für Lambda-Ausdrücke ist => (gesprochen "Pfeil" oder "geht zu"). Er trennt die Eingabeparameter vom Methodenkörper.

Grundsyntax von Lambda-Ausdrücken

Angenommen, du hast einen Delegate (Methodenverweis) und übergibst ihn an die Funktion Sort():

public static int CompareStudentsByAge(Student student1, Student student2)
{
   return student1.Age.CompareTo(student2.Age);
}

students.Sort(CompareStudentsByAge); //Wir übergeben die Methode CompareStudentsByAge als Parameter

Das geht auch kürzer:

//Wir übergeben eine anonyme Methode als Parameter
students.Sort( (Student student1, Student student2) => student1.Age.CompareTo(student2.Age) );

Hier ersetzen wir den Methodennamen durch die zwei wichtigsten Dinge:

  • Parameter: (Student student1, Student student2)
  • Methodeninhalt: student1.Age.CompareTo(student2.Age)

So eine kompakte Methodenschreibweise nennt man Lambda-Ausdruck: (Parameter) => Ausdruck

Wie das funktioniert

Der C#-Compiler erzeugt beim Kompilieren aus dem Lambda-Ausdruck eine echte Methode.

Angenommen, du hast so einen Code:

students.Sort( (s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade) );

Das Kompilierergebnis sieht ungefähr so aus:

public static int CompareStudents_Lambda123(Student s1, Student s2)
{
   return s2.AverageGrade.CompareTo(s1.AverageGrade);
}

students.Sort( CompareStudents_Lambda123 );

4. Sortierbeispiel mit Lambda-Ausdrücken

Lass uns unser Beispiel mit den Studierenden mit einem Lambda-Ausdruck umschreiben:

public class Program
{
    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("Ivan", "Petrov", 20, 4.5),
            new Student("Maria", "Sidorova", 22, 4.8),
            new Student("Alexey", "Ivanov", 19, 3.9),
            new Student("Elena", "Kozlova", 20, 4.2)
        };

        Console.WriteLine("--- Liste der Studierenden vor dem Sortieren ---");        
        foreach (var s in students)
            s.PrintInfo();

        // Jetzt steht die Vergleichslogik direkt hier "an Ort und Stelle"
        Console.WriteLine("--- Sortiere Studierende nach Alter (mit Lambda-Ausdruck) ---");
        students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));

        foreach (var s in students)
            s.PrintInfo();

        // Für absteigende Sortierung einfach das Ergebnis mit -1 multiplizieren
        Console.WriteLine("\n--- Sortiere Studierende nach Durchschnittsnote (absteigend) ---");
        // s2.CompareTo(s1) statt s1.CompareTo(s2)
        students.Sort((s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade));

        foreach (var s in students)
            s.PrintInfo();
    }
}

Was passiert hier?

  1. students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));
    • student1 und student2 sind die Parameter, die Sort an unsere anonyme Methode übergibt (wie x und y bei Comparison<T>).
    • => ist der Lambda-Operator.
    • student1.Age.CompareTo(student2.Age) ist der Körper des Lambda-Ausdrucks. In diesem Fall ist es nur ein Ausdruck, dessen Ergebnis zurückgegeben wird.
  2. Für die Sortierung nach Durchschnittsnote absteigend haben wir einfach s1 und s2 in CompareTo getauscht. Das ist der klassische Trick, um die Sortierreihenfolge umzukehren.

Warum ist das praktisch?

  • Kompakt: Du musst keine eigenen Methoden oder Klassen für jede kleine Vergleichslogik schreiben.
  • Lesbar: Die Vergleichslogik steht direkt beim Sort()-Aufruf – das macht den Code gerade bei einfachen Fällen viel verständlicher.
  • Flexibel: Du kannst die Sortierbedingungen ganz easy "on the fly" ändern.

5. Delegates und Lambdas – das perfekte Team

Vielleicht fragst du dich: Ist ein Lambda-Ausdruck dasselbe wie ein Delegate oder was anderes?

Eigentlich ist ein Lambda-Ausdruck nur syntactic sugar für das Erstellen einer Delegate-Instanz oder eines Expression Trees (dazu später mehr). Wenn der Compiler einen Lambda-Ausdruck sieht, wandelt er ihn "unter der Haube" in eine passende Delegate-Instanz um. In unserem Fall erwartet List<T>.Sort() einen Comparison<T>-Delegate, also weiß der Compiler, dass (student1, student2) => student1.Age.CompareTo(student2.Age) genau zu Comparison<Student> werden soll.

So machen Lambda-Ausdrücke unseren Code super kurz, und Delegates sind die "Behälter", die diesen Code transportieren und ausführbar machen. Sie arbeiten Hand in Hand!

Wann was verwenden?

  • IComparable<T>: Verwende das, wenn dein Typ eine natürliche, offensichtliche Sortierreihenfolge hat. Zum Beispiel, wenn du Produkte nach Artikelnummer sortierst. Dieses Interface legt die "Standard"-Reihenfolge fest.
  • IComparer<T>: Nimm das, wenn du mehrfach wiederverwendbare Vergleichslogik brauchst, aber nicht die Hauptklasse "zumüllen" willst oder wenn du mehrere verschiedene Sortierarten hast. Zum Beispiel ein IComparer für Preis, ein anderer für Name, und du nutzt sie an verschiedenen Stellen im Programm.
  • Delegates (Comparison<T>) und Lambda-Ausdrücke: Perfekt für einmalige, ad-hoc Sortierungen, wenn die Vergleichslogik einfach ist und keine eigene Klasse braucht. Das ist der häufigste und sauberste Weg für die meisten Sortieraufgaben in C#. Außerdem kannst du so Logik an andere Methoden übergeben, z.B. an Filter-Methoden (Find, FindAll) oder Such-Methoden (FindIndex), die wir schon besprochen haben.
Feature IComparable<T> IComparer<T> Comparison<T> / Lambda-Ausdruck
Wo definiert? In der Klasse T selbst In einer eigenen Vergleichsklasse Kann Methode oder anonymes Ausdruck sein
Flexibilität Feste "natürliche" Reihenfolge Mehrfach, wiederverwendbare Reihenfolgen Ad-hoc (on the fly), für einen Methodenaufruf
Boilerplate Wenig, innerhalb der Klasse Mittel (eigene Klasse) Minimal (vor allem bei Lambdas)
Beispiel
someList.Sort()
someList.Sort(new MyComparer())
someList.Sort((x, y) => x.Prop.CompareTo(y.Prop))
Lesbarkeit Gut für natürliche Reihenfolge Hängt vom Namen des Comparers ab Super für einfache, spezifische Vergleiche

6. Praktische Anwendung und Ausblick

Lambda-Ausdrücke sind nicht nur "syntactic sugar" fürs Sortieren. Sie sind ein mächtiges Werkzeug, das überall im modernen C#-Code verwendet wird. Du wirst sie sehr oft sehen:

  • In LINQ (Language Integrated Query): Das ist wahrscheinlich das häufigste Einsatzgebiet für Lambdas. LINQ erlaubt SQL-ähnliche Abfragen auf Collections, und Lambdas werden für Filter-, Sortier- und Projektionbedingungen verwendet. Wir werden LINQ bald lernen, und du wirst sehen, wie Lambdas es supermächtig und bequem machen.
  • Beim Event-Handling: Lambdas machen es einfach, zu beschreiben, was bei einem Event (z.B. Button-Klick im UI) passieren soll.
  • Beim asynchronen Programmieren: Um Aufgaben zu definieren, die parallel laufen sollen.
  • In vielen .NET-APIs: Viele Methoden in der .NET-Standardbibliothek nehmen Delegates (und damit Lambda-Ausdrücke) als Parameter, um flexible Logik zu ermöglichen.

Wenn du Lambda-Ausdrücke beherrschst, verbesserst du nicht nur deine Sortier-Skills, sondern machst einen riesigen Schritt zum Verständnis von modernem C#-Code und Bibliotheken. Das ist eine Fähigkeit, die in jedem Vorstellungsgespräch gefragt ist und in jedem Projekt nützlich sein wird!

7. Typische Fehler und Stolperfallen

Wenn du mit Delegates und Lambda-Ausdrücken zum Vergleichen arbeitest, gibt es ein paar Dinge, auf die du achten solltest:

Falsches Vergleichsergebnis: Denk dran, dass CompareTo oder deine Vergleichslogik eine negative Zahl, null oder eine positive Zahl zurückgeben muss. Wenn du aus Versehen etwas anderes zurückgibst, kann die Sortierung falsch laufen oder sogar Fehler werfen. Der häufigste Anfängerfehler: true oder false statt int zurückgeben. Die Methode Sort erwartet eine Zahl, weil sie wissen muss, ob ein Element "größer" ist – nicht nur, ob sie gleich sind.

Umgang mit null-Werten: Wenn Elemente in deiner Collection null sein können, führt ein Methodenaufruf auf einem null-Objekt (z.B. student1.Age.CompareTo(...), wenn student1 null ist) zu einer NullReferenceException. Deine Vergleichslogik sollte also null-Werte explizit behandeln. Die Faustregel: null gilt als "kleiner" als alles andere. Wenn beide null sind, sind sie gleich. Wenn einer null ist und der andere nicht, ist null "kleiner".

// Beispiel für null-Behandlung im Lambda-Ausdruck für Vergleiche
students.Sort((s1, s2) => {
    if (s1 == null && s2 == null) return 0;
    if (s1 == null) return -1; // null ist am kleinsten
    if (s2 == null) return 1;  // nicht-null ist größer als null
    return s1.Age.CompareTo(s2.Age); // Vergleichen, wenn beide nicht null sind
});

Zum Glück enthalten Collections in echten Projekten selten null-Werte, aber du solltest das trotzdem im Hinterkopf behalten!

Performance: Auch wenn Lambda-Ausdrücke super praktisch sind, kann ihr übermäßiger Einsatz in sehr "heißen" Schleifen oder bei riesigen Collections die Performance minimal beeinflussen, verglichen mit hochoptimierten IComparer-Klassen, die vielleicht speziell getestet und getunt wurden. Für die meisten Alltagsaufgaben ist der Unterschied aber vernachlässigbar, und der Gewinn an Lesbarkeit und Einfachheit überwiegt deutlich.

Komplexe Vergleichsketten: Wie wir im Beispiel mit Nachname- und dann Vorname-Sortierung gesehen haben, kannst du mit Lambdas mehrere Bedingungen verschachteln. Das ist viel angenehmer, als zehn ifs in eine Zeile zu quetschen! Wichtig ist: Immer erst das Ergebnis des ersten Vergleichs prüfen (lastNameComparison != 0), bevor du zur nächsten Ebene gehst.

Lambda-Ausdrücke und Delegates sind grundlegende Konzepte in C#, die dir den Weg zu einem flexibleren und funktionaleren Programmierstil öffnen. Wenn du sie verstehst und anwenden kannst, wird dein Code viel sauberer, effizienter und moderner. Probier weiter aus, und bald wirst du sie ganz automatisch nutzen!

2
Aufgabe
C# SELF, Level 30, Lektion 2
Gesperrt
Sortieren von Zahlen mit Lambda-Ausdruck
Sortieren von Zahlen mit Lambda-Ausdruck
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION