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 |
|---|---|
|
Wszystko, co jest w A lub B (unikalne elementy). |
|
Wszystko, co jest w obu (A i 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.
GO TO FULL VERSION