CodeGym /Kursy /C# SELF /Operacje Union,

Operacje Union, Intersect, Except w LINQ

C# SELF
Poziom 33, Lekcja 3
Dostępny

1. Wprowadzenie

Przechodzimy do mega ciekawego i często używanego w praktyce tematu: operacje na zbiorach w LINQ — Union, Intersect, Except. Pozwalają one pracować z kolekcjami tak, jakbyśmy bawili się prawdziwą algebrą zbiorów (albo liczyli naklejki w dwóch paczkach na biurku programisty — co, przyznaj, jest prawie tym samym). Jeśli nigdy nie słyszałeś o algebrze zbiorów (albo już o niej zapomniałeś), może to brzmieć groźnie. Ale serio, to bardzo prosty koncept: wyobraź sobie dwa worki z owocami. Algebra zbiorów to sposób, żeby ogarnąć, jakie owoce są w obu workach, jakie tylko w jednym, a jakie — razem, jeśli je połączysz. To jak zabawa z naklejkami: możesz wszystko wrzucić razem, znaleźć wspólne albo wykluczyć jedne z drugich. I tyle.

Praktyka: Te metody są super, gdy trzeba połączyć wyniki dwóch różnych zapytań, znaleźć wspólne elementy albo określić różnicę między kolekcjami. Na przykład:

  • Zrobić wspólną listę wszystkich produktów, które są albo na liście sprzedaży, albo na liście zakupów.
  • Znaleźć produkty, które są i tu, i tu — wspólne pozycje.
  • Albo sprawdzić, jakie produkty są na magazynie, ale nie były kupowane — ktoś się tam zasiedział!

Realne zadania: Algebra zbiorów jest wszędzie: filtrowanie użytkowników po subskrypcjach, znajdowanie tych, którzy byli w różnych grupach, szukanie unikalnych lub przecinających się zamówień, porównywanie wyników dwóch różnych zapytań do bazy i masa innych rzeczy.

2. Operacja Union — łączenie kolekcji

Prosto, więc od razu przechodzimy do praktyki. Załóżmy, że mamy dwie listy produktów:

List<string> warehouseProducts = new List<string> { "Mleko", "Chleb", "Ser", "Jajka" };
List<string> recentlySold = new List<string> { "Chleb", "Ser", "Salami", "Herbata" };

Co robi Union?

Union zwraca unikalne elementy, które są przynajmniej w jednej z kolekcji.
W skrócie — łączenie.

var allProducts = warehouseProducts.Union(recentlySold);

foreach (var product in allProducts)
{
    Console.WriteLine(product);
}
// Wypisze: Mleko, Chleb, Ser, Jajka, Salami, Herbata

Tak to wygląda wizualnie:

Magazyn Sprzedane Union (Łączenie)
Mleko Chleb Mleko
Chleb Ser Chleb
Ser Salami Ser
Jajka Herbata Jajka
Salami
Herbata

W języku algebry zbiorów, Union to operacja "lub": daj mi wszystko, co jest gdziekolwiek. Wyobraź sobie dwie puszki z różnymi rodzajami herbaty — jeśli chcesz spróbować wszystkich smaków, nie ważne, jeśli jakaś herbata się powtarza — wypijesz ją tylko raz.

Union automatycznie usuwa duplikaty (bazując na implementacji metody Equals i GetHashCode dla typu elementów). Jeśli robisz łączenie dla własnych klas (np. Product), upewnij się, że te metody są poprawnie zaimplementowane, bo inaczej Union zacznie się dziwnie zachowywać: te same elementy mogą być traktowane jako różne!

Pamiętaj: Kolejność elementów w wyniku zachowuje kolejność z pierwszej kolekcji, a nowe — są dodawane na końcu (w tej kolejności, w jakiej pierwszy raz pojawiły się w drugiej kolekcji).

Przykład z obiektami

Kontynuujemy naszą apkę "Sklep". Mamy dwie listy Product:

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

    // Żeby Union/Intersect/Except działały poprawnie, trzeba dobrze zaimplementować Equals i GetHashCode!
    public override bool Equals(object? obj) =>
        obj is Product other && Name == other.Name && Category == other.Category;

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

List<Product> stock = new()
{
    new Product { Name = "Mleko", Category = "Nabiał" },
    new Product { Name = "Chleb", Category = "Piekarnia" },
};

List<Product> sold = new()
{
    new Product { Name = "Chleb", Category = "Piekarnia" },
    new Product { Name = "Salami", Category = "Wędliny" },
};

var all = stock.Union(sold);

// Uwaga: elementy się nie dublują,
// nawet jeśli "na oko" są takie same, ale powinny być równe według logiki. 

foreach (var p in all)
    Console.WriteLine($"{p.Name} ({p.Category})");

Typowy błąd: Jeśli nie nadpiszesz Equals/GetHashCode, Union potraktuje różne instancje z tymi samymi wartościami jako różne elementy!

3. Operacja Intersect — przecięcie kolekcji

Intersect zwraca te elementy, które są we wszystkich źródłowych kolekcjach. To takie "i" w algebrze zbiorów.

Przykład

Przypomnijmy sobie listy z poprzedniego przykładu:

var commonProducts = warehouseProducts.Intersect(recentlySold);
foreach (var product in commonProducts)
{
    Console.WriteLine(product);
}
// Wypisze: Chleb, Ser

Wizualizacja:

Magazyn Sprzedane Intersect (Wspólne)
Chleb Chleb Chleb
Ser Ser Ser

Gdzie się przyda?

  • Chcesz wiedzieć, które produkty z magazynu zostały dziś sprzedane.
  • Na rozmowach rekrutacyjnych lubią pytać: "Jak znaleźć przecięcie dwóch list?" — LINQ robi to w jednej linijce!
  • W aplikacjach biznesowych przecięcie często służy do filtrowania połączonych kryteriów.

Specyfika i typowe błędy

Jeśli element jest kilka razy w kolekcji, w wyniku będzie tylko jeden egzemplarz tego elementu.
Dla złożonych obiektów (np. klasy Product) znowu ważna jest poprawna implementacja Equals/GetHashCode.

Przykład z obiektami

var commonObjects = stock.Intersect(sold);
foreach (var p in commonObjects)
    Console.WriteLine($"{p.Name} ({p.Category})");
// Wypisze tylko Chleb (Piekarnia)

4. Operacja Except — różnica kolekcji

Except zwraca elementy, które są tylko w pierwszej kolekcji, ale nie ma ich w drugiej.

Przykład

var productsOnlyInStock = warehouseProducts.Except(recentlySold);
foreach (var product in productsOnlyInStock)
{
    Console.WriteLine(product);
}
// Wypisze: Mleko, Jajka

Czyli to produkty, które są na magazynie, ale nie były sprzedane.

Wizualizacja:

Magazyn Sprzedane Except (tylko magazyn)
Mleko Mleko
Jajka Jajka
Chleb Chleb (pominięte)
Ser Ser (pominięte)

Analogicznie

To tak, jakbyś wyrzucił ze swojej kupki wszystkie dokumenty, które już wysłałeś — zostanie tylko to, co jeszcze masz i nigdzie indziej.

Przykład z obiektami

var unsold = stock.Except(sold);
foreach (var p in unsold)
    Console.WriteLine($"{p.Name} ({p.Category})");
// Wypisze: Mleko (Nabiał)

! Nieoczywiste momenty

Metoda Except jest czuła na kolejność: A.Except(B) — to nie to samo, co B.Except(A)! Pierwsza kolekcja — "skąd odejmujemy", druga — "co usuwamy".

5. Łączenie i kompozycja wielu operacji z LINQ

Są sytuacje, gdy jedna operacja to za mało. Załóżmy, że trzeba wypisać produkty, które są albo tylko na magazynie, albo tylko w sprzedanych — ale nie w obu kolekcjach naraz (tzw. "różnica symetryczna").

Różnica symetryczna ("XOR" dla zbiorów):

var onlyInOne = warehouseProducts.Except(recentlySold)
                   .Union(recentlySold.Except(warehouseProducts));
foreach (var product in onlyInOne)
{
    Console.WriteLine(product);
}
// Wypisze: Mleko, Jajka, Salami, Herbata

Do złożonych logik fajnie łączyć metody LINQ:

// Znaleźć produkty, których nie ma ani na magazynie, ani w sprzedanych, tylko na liście "oczekiwane dostawy"
List<string> expected = new() { "Kawa", "Herbata", "Mleko" };
var onlyExpected = expected.Except(warehouseProducts.Union(recentlySold));
foreach (var product in onlyExpected)
    Console.WriteLine(product);
// Wypisze: Kawa

6. Praca z własnymi typami i IEqualityComparer

Czasem nie chcesz porównywać obiektów po wszystkich polach: np. ważna jest tylko nazwa produktu, a kategoria już nie. Do tego metody LINQ obsługują dodatkowy parametr — IEqualityComparer<T>, który określa, jak porównywać elementy.

Przykład własnego comparera:

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

var comp = new ProductNameComparer();
var uniqueByName = stock.Union(sold, comp);
foreach (var p in uniqueByName)
    Console.WriteLine(p.Name); // Mleko, Chleb, Salami

To mega wygodne, jeśli nie chcesz zmieniać Equals/GetHashCode w całym modelu, a tylko raz chcesz porównać obiekty według konkretnej zasady.

7. Schematy wizualne i tabele

Podsumujmy zastosowania (dla list A i B):

Operacja Wynik
A.Union(B)
Wszystko, co jest w A lub B (unikalne elementy).
A.Intersect(B)
Wszystko, co jest w obu (A i B).
A.Except(B)
Wszystko, co jest tylko w A, ale nie w B.

Schemat (Venn Diagram, gdybyśmy mogli rysować):


      [A]        [B]
      oooooooooo
      oooooooo oooo
      ooooo oo   ooo
      ooo    ooo  oo
      oo      o  ooo
      ooo    ooo ooo
  • Union: wszystko, co jest w obu kołach.
  • Intersect: tylko przecięcie (środek).
  • Except: tylko to, co w kole A, bez przecięcia z B.

8. Typowe błędy przy użyciu Union, Intersect i Except

Błąd nr 1: używanie z obiektami bez Equals i GetHashCode.
Jeśli twoja klasa nie ma nadpisanych tych metod, metody Union, Intersect i Except będą działać źle: takie same obiekty będą traktowane jako różne. W efekcie dostaniesz nieoczekiwany (i często bezużyteczny) wynik.

Błąd nr 2: próba porównywania obiektów po części pól bez IEqualityComparer.
Na przykład, jeśli chcesz porównywać produkty tylko po nazwie, a nie po całej strukturze, samo Intersect tego nie ogarnie. Bez jawnego IEqualityComparer wynik nie będzie taki, jak oczekujesz.

Błąd nr 3: złe oczekiwania co do kolejności elementów.
Wielu zakłada, że wynikowa kolekcja zachowuje kolejność łączenia lub przecięcia. Ale zachowanie zależy od metody: Union zachowuje kolejność pierwszej kolekcji, ale Intersect i Except mogą zwrócić elementy w nieprzewidywalnej kolejności. Lepiej w ogóle nie polegać na kolejności.

Błąd nr 4: ignorowanie wydajności przy pracy z dużymi kolekcjami.
Jeśli dane są duże, metody mogą działać wolno. Pomyśl o wcześniejszej agregacji, filtrowaniu albo użyciu struktur haszujących (np. HashSet), żeby przyspieszyć operacje.

2
Zadanie
C# SELF, poziom 33, lekcja 3
Niedostępne
Użycie Intersect do znalezienia wspólnych elementów
Użycie Intersect do znalezienia wspólnych elementów
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION