CodeGym /Kurse /C# SELF /Operationen Union, ...

Operationen Union, Intersect, Except in LINQ

C# SELF
Level 33 , Lektion 3
Verfügbar

1. Einführung

Jetzt geht's los mit einem echt spannenden und in der Praxis oft gebrauchten Thema: die Mengenoperationen in LINQ — Union, Intersect, Except. Damit kannst du mit Collections arbeiten, als würdest du echte Mengenalgebra machen (oder Sticker aus zwei Packungen auf dem Schreibtisch eines Programmierers zählen — was, zugegeben, fast das Gleiche ist). Falls du noch nie von Mengenalgebra gehört hast (oder sie schon vergessen hast), klingt das vielleicht erstmal abschreckend. Aber eigentlich ist das super simpel: Stell dir zwei Tüten mit Obst vor. Mengenalgebra ist einfach ein Weg herauszufinden, welches Obst in beiden Tüten ist, welches nur in einer und was du alles hast, wenn du beide zusammenkippst. Wie ein Spiel mit Stickern: alles zusammenlegen, die gemeinsamen finden oder die einen von den anderen abziehen. Mehr ist das nicht.

Praxis: Diese Methoden sind praktisch, wenn du die Ergebnisse von zwei verschiedenen Abfragen zusammenführen, gemeinsame Elemente finden oder den Unterschied zwischen Collections bestimmen willst. Zum Beispiel:

  • Eine Gesamtliste aller Produkte erstellen, die entweder im Verkaufs- oder im Einkaufsliste vorkommen.
  • Produkte finden, die in beiden Listen sind — also die gemeinsamen Positionen.
  • Oder herausfinden, welche Produkte im Lager sind, aber nicht verkauft wurden — da gammelt wohl was rum!

Echte Aufgaben: Mengenalgebra begegnet dir überall: User nach Abos filtern, Leute finden, die in mehreren Gruppen waren, einzigartige oder sich überschneidende Bestellungen suchen, Ergebnisse von zwei verschiedenen Datenbankabfragen vergleichen und noch viel mehr.

2. Die Operation Union — Collections zusammenführen

Ganz easy, also direkt zur Praxis. Angenommen, wir haben zwei Listen mit Produkten:

List<string> lagerProdukte = new List<string> { "Milch", "Brot", "Käse", "Eier" };
List<string> zuletztVerkauft = new List<string> { "Brot", "Käse", "Salami", "Tee" };

Was macht Union?

Union gibt dir einzigartige Elemente zurück, die mindestens in einer der Collections vorkommen.
Das ist also das Zusammenführen.

var alleProdukte = lagerProdukte.Union(zuletztVerkauft);

foreach (var produkt in alleProdukte)
{
    Console.WriteLine(produkt);
}
// Gibt aus: Milch, Brot, Käse, Eier, Salami, Tee

So sieht das visuell aus:

Lager Verkauft Union (Vereinigung)
Milch Brot Milch
Brot Käse Brot
Käse Salami Käse
Eier Tee Eier
Salami
Tee

In der Mengenalgebra ist Union das "oder": Gib mir alles, was irgendwo vorkommt. Stell dir zwei Kisten mit verschiedenen Teesorten vor — wenn du alle Geschmacksrichtungen probieren willst, ist es egal, ob ein Tee doppelt ist — du trinkst ihn nur einmal.

Union entfernt automatisch Duplikate (basierend auf der Implementierung von Equals und GetHashCode für den Elementtyp). Wenn du das Zusammenführen für eigene Klassen (z.B. Product) machst, achte darauf, dass diese Methoden korrekt implementiert sind, sonst verhält sich Union komisch: gleiche Elemente könnten als verschieden gelten!

Nicht vergessen: Die Reihenfolge der Elemente in der Ergebnis-Collection bleibt wie in der ersten Collection, neue kommen am Ende dazu (in der Reihenfolge, wie sie das erste Mal in der zweiten Collection auftauchen).

Beispiel mit Objekten

Wir machen weiter mit unserer "Shop"-App. Wir haben zwei Listen von Product:

public class Product
{
    public string Name { get; set; }
    public string Kategorie { get; set; }

    // Für korrektes Union/Intersect/Except braucht man richtiges Equals und GetHashCode!
    public override bool Equals(object? obj) =>
        obj is Product other && Name == other.Name && Kategorie == other.Kategorie;

    public override int GetHashCode() => HashCode.Combine(Name, Kategorie);
}

List<Product> lager = new()
{
    new Product { Name = "Milch", Kategorie = "Milchprodukte" },
    new Product { Name = "Brot", Kategorie = "Backwaren" },
};

List<Product> verkauft = new()
{
    new Product { Name = "Brot", Kategorie = "Backwaren" },
    new Product { Name = "Salami", Kategorie = "Wurstwaren" },
};

var alle = lager.Union(verkauft);

// Achtung: Elemente werden nicht dupliziert,
// auch wenn sie "gleich aussehen", aber laut Logik gleich sein sollten. 

foreach (var p in alle)
    Console.WriteLine($"{p.Name} ({p.Kategorie})");

Typischer Fehler: Wenn du Equals/GetHashCode nicht überschreibst, behandelt Union verschiedene Instanzen mit gleichen Werten als unterschiedliche Elemente!

3. Die Operation Intersect — Schnittmenge von Collections

Intersect gibt dir die Elemente zurück, die in allen Ausgangs-Collections vorkommen. Das ist das "und" in der Mengenalgebra.

Beispiel

Erinnern wir uns an die Listen aus dem letzten Beispiel:

var gemeinsameProdukte = lagerProdukte.Intersect(zuletztVerkauft);
foreach (var produkt in gemeinsameProdukte)
{
    Console.WriteLine(produkt);
}
// Gibt aus: Brot, Käse

Visualisierung:

Lager Verkauft Intersect (Gemeinsam)
Brot Brot Brot
Käse Käse Käse

Wofür ist das nützlich?

  • Du willst wissen, welche Produkte aus deinem Lager heute verkauft wurden.
  • In Vorstellungsgesprächen wird oft gefragt: "Wie findet man die Schnittmenge von zwei Listen?" — Mit LINQ geht das in einer Zeile!
  • In Business-Apps braucht man Schnittmengen oft für Filter mit kombinierten Kriterien.

Besonderheiten und typische Fehler

Wenn ein Element mehrfach in der Collection ist, gibt es im Ergebnis nur ein Exemplar davon.
Für komplexe Objekte (wie die Klasse Product) ist wieder die richtige Implementierung von Equals/GetHashCode wichtig.

Beispiel mit Objekten

var gemeinsameObjekte = lager.Intersect(verkauft);
foreach (var p in gemeinsameObjekte)
    Console.WriteLine($"{p.Name} ({p.Kategorie})");
// Gibt nur Brot (Backwaren) aus

4. Die Operation Except — Differenz von Collections

Except gibt dir die Elemente, die nur in der ersten Collection sind, aber nicht in der zweiten.

Beispiel

var nurImLager = lagerProdukte.Except(zuletztVerkauft);
foreach (var produkt in nurImLager)
{
    Console.WriteLine(produkt);
}
// Gibt aus: Milch, Eier

Das sind also die Produkte, die im Lager sind, aber nicht verkauft wurden.

Visualisierung:

Lager Verkauft Except (nur Lager)
Milch Milch
Eier Eier
Brot Brot (übersprungen)
Käse Käse (übersprungen)

Analogie

Das ist, als würdest du aus deinem Stapel alle Dokumente entfernen, die du schon verschickt hast — übrig bleibt nur, was noch bei dir liegt und sonst nirgends.

Beispiel mit Objekten

var nichtVerkauft = lager.Except(verkauft);
foreach (var p in nichtVerkauft)
    Console.WriteLine($"{p.Name} ({p.Kategorie})");
// Gibt aus: Milch (Milchprodukte)

! Nicht offensichtliche Punkte

Die Methode Except ist reihenfolgesensitiv: A.Except(B) ist nicht das gleiche wie B.Except(A)! Die erste Collection ist "wo wir abziehen", die zweite "was wir entfernen".

5. Kombination und Komposition von mehreren LINQ-Operationen

Manchmal reicht eine Operation nicht. Angenommen, du willst Produkte ausgeben, die entweder nur im Lager oder nur bei den verkauften sind — aber nicht in beiden gleichzeitig (das sogenannte "symmetrische Differenz").

Symmetrische Differenz ("XOR" für Mengen):

var nurInEiner = lagerProdukte.Except(zuletztVerkauft)
                   .Union(zuletztVerkauft.Except(lagerProdukte));
foreach (var produkt in nurInEiner)
{
    Console.WriteLine(produkt);
}
// Gibt aus: Milch, Eier, Salami, Tee

Für komplexe Logik ist es praktisch, LINQ-Methoden zu kombinieren:

// Finde Produkte, die weder im Lager noch bei den verkauften sind, sondern nur in der Liste "erwartet"
List<string> erwartet = new() { "Kaffee", "Tee", "Milch" };
var nurErwartet = erwartet.Except(lagerProdukte.Union(zuletztVerkauft));
foreach (var produkt in nurErwartet)
    Console.WriteLine(produkt);
// Gibt aus: Kaffee

6. Arbeiten mit eigenen Typen und IEqualityComparer

Manchmal willst du Objekte nicht nach allen Feldern vergleichen: Zum Beispiel ist dir nur der Produktname wichtig, die Kategorie aber egal. Dafür unterstützen die LINQ-Methoden einen zusätzlichen Parameter — IEqualityComparer<T>, der festlegt, wie Elemente verglichen werden.

Beispiel für einen eigenen Comparer:

class ProduktNameComparer : IEqualityComparer<Product>
{
    public bool Equals(Product? x, Product? y) => x?.Name == y?.Name;
    public int GetHashCode(Product obj) => obj.Name.GetHashCode();
}

var comp = new ProduktNameComparer();
var uniqueByName = lager.Union(verkauft, comp);
foreach (var p in uniqueByName)
    Console.WriteLine(p.Name); // Milch, Brot, Salami

Das ist besonders praktisch, wenn du nicht Equals/GetHashCode im ganzen Modell ändern willst, sondern nur einmalig nach einer bestimmten Regel vergleichen möchtest.

7. Visuelle Schemata und Tabellen

Fassen wir die Anwendungen zusammen (für die Listen A und B):

Operation Ergebnis
A.Union(B)
Alles, was in A oder B ist (einzigartige Elemente).
A.Intersect(B)
Alles, was in beiden (A und B) ist.
A.Except(B)
Alles, was nur in A ist, aber nicht in B.

Schema (Venn Diagramm, wenn wir zeichnen könnten):


      [A]        [B]
      oooooooooo
      oooooooo oooo
      ooooo oo   ooo
      ooo    ooo  oo
      oo      o  ooo
      ooo    ooo ooo
  • Union: alles, was in beiden Kreisen ist.
  • Intersect: nur die Schnittmenge (Mitte).
  • Except: nur das, was im Kreis A ist, ohne die Überschneidung mit B.

8. Typische Fehler bei Union, Intersect und Except

Fehler Nr. 1: Verwendung mit Objekten ohne Equals und GetHashCode.
Wenn deine Klasse diese Methoden nicht überschreibt, funktionieren Union, Intersect und Except nicht korrekt: Objekte mit gleichem Inhalt werden als verschieden angesehen. Das Ergebnis ist dann oft unerwartet (und meistens nutzlos).

Fehler Nr. 2: Versuch, Objekte nur nach bestimmten Feldern ohne IEqualityComparer zu vergleichen.
Wenn du z.B. Produkte nur nach Namen vergleichen willst, aber nicht nach der ganzen Struktur, versteht Intersect das nicht einfach so. Ohne einen expliziten IEqualityComparer passt das Ergebnis nicht zu deinen Erwartungen.

Fehler Nr. 3: Falsche Erwartungen bezüglich der Reihenfolge der Elemente.
Viele denken, dass die Ergebnis-Collection die Reihenfolge vom Zusammenführen oder Schneiden beibehält. Das Verhalten hängt aber von der Methode ab: Union behält die Reihenfolge der ersten Collection, aber Intersect und Except können die Elemente in beliebiger Reihenfolge zurückgeben. Verlass dich besser nicht auf die Reihenfolge.

Fehler Nr. 4: Performance ignorieren bei großen Collections.
Wenn die Daten groß sind, können die Methoden langsam werden. Denk über vorheriges Aggregieren, Filtern oder die Nutzung von Hash-Strukturen (wie HashSet) nach, um die Operationen zu beschleunigen.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION