Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

W poprzednich wpisach pracowaliśmy z pojedynczymi wartościami - int, string, bool. Ale w prawdziwych aplikacjach potrzebujesz przechowywać wiele wartości - listę użytkowników, koszyk produktów, wyniki wyszukiwania. Do tego służą kolekcje!

W tym wpisie poznasz tablice, List<T>, Dictionary<TKey, TValue>, HashSet<T>, oraz rewolucyjne Collection Expressions z C# 12 - nowy sposób tworzenia kolekcji który zmienia wszystko!

🔍 Czym jest kolekcja?

Kolekcja to obiekt który przechowuje grupę elementów. Myśl o kolekcji jak o pudełku z wieloma przedmiotami - możesz dodawać, usuwać, przeszukiwać, sortować.

W C# mamy wiele rodzajów kolekcji, każda z różnymi cechami:

  • Array - stały rozmiar, szybki dostęp przez indeks
  • List<T> - dynamiczny rozmiar, uporządkowana
  • Dictionary<K,V> - klucz-wartość, szybkie wyszukiwanie
  • HashSet<T> - unikalne wartości, szybkie sprawdzanie czy istnieje
Tablice (Arrays)

Podstawy tablic

// Deklaracja tablicy - określasz rozmiar
int[] numbers = new int[5];  // tablica 5 elementów (wszystkie = 0)
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;

Console.WriteLine(numbers[0]);  // 10
Console.WriteLine(numbers[4]);  // 50

// Inicjalizacja z wartościami - array initializer
int[] numbers2 = new int[] { 10, 20, 30, 40, 50 };

// Krótsza składnia (kompilator wywnioskuje typ)
int[] numbers3 = { 10, 20, 30, 40, 50 };

// Długość tablicy
Console.WriteLine(numbers3.Length);  // 5

// Tablice innych typów
string[] names = { "Jan", "Anna", "Piotr" };
bool[] flags = { true, false, true, true };
double[] prices = { 9.99, 19.99, 29.99 };
⚠️ Tablice mają STAŁY rozmiar!
int[] numbers = new int[3];  // 3 elementy
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;

// ❌ Nie możesz dodać więcej elementów!
// numbers[3] = 40;  // IndexOutOfRangeException!

// ❌ Nie możesz zmienić rozmiaru!
// numbers.Add(40);  // Array nie ma metody Add!

// Jeśli potrzebujesz dynamicznego rozmiaru - użyj List!

Iteracja po tablicy

string[] fruits = { "Jabłko", "Banan", "Gruszka", "Wiśnia" };

// For loop - dostęp przez indeks
for (int i = 0; i < fruits.Length; i++)
{
    Console.WriteLine($"{i}: {fruits[i]}");
}
// 0: Jabłko
// 1: Banan
// 2: Gruszka
// 3: Wiśnia

// Foreach - czytelniejsze (nie masz indeksu)
foreach (string fruit in fruits)
{
    Console.WriteLine(fruit);
}

// C# 7+ - deconstruction z indexem
foreach (var (fruit, index) in fruits.Select((f, i) => (f, i)))
{
    Console.WriteLine($"{index}: {fruit}");
}

Array methods

int[] numbers = { 5, 2, 8, 1, 9, 3 };

// Sort - sortowanie IN-PLACE (modyfikuje oryginalną tablicę!)
Array.Sort(numbers);
Console.WriteLine(string.Join(", ", numbers));  // 1, 2, 3, 5, 8, 9

// Reverse - odwracanie
Array.Reverse(numbers);
Console.WriteLine(string.Join(", ", numbers));  // 9, 8, 5, 3, 2, 1

// IndexOf - znajdź indeks elementu
int index = Array.IndexOf(numbers, 5);  // 2

// Clear - wyczyść zakres (ustaw na wartość domyślną)
Array.Clear(numbers, 0, 2);  // wyczyść pierwsze 2 elementy
Console.WriteLine(string.Join(", ", numbers));  // 0, 0, 5, 3, 2, 1

// Copy - kopiuj elementy
int[] copy = new int[numbers.Length];
Array.Copy(numbers, copy, numbers.Length);

// Resize - zmień rozmiar (tworzy NOWĄ tablicę!)
Array.Resize(ref numbers, 10);  // teraz ma 10 elementów

Range operator - C# 8+

int[] numbers = { 10, 20, 30, 40, 50, 60, 70, 80, 90 };

// Zakres [start..end]
int[] slice1 = numbers[0..3];    // { 10, 20, 30 } (elementy 0,1,2)
int[] slice2 = numbers[2..5];    // { 30, 40, 50 } (elementy 2,3,4)

// Od początku
int[] slice3 = numbers[..3];     // { 10, 20, 30 } (pierwsze 3)

// Do końca
int[] slice4 = numbers[5..];     // { 60, 70, 80, 90 } (od 5 do końca)

// Od końca (^)
int[] slice5 = numbers[^3..];    // { 70, 80, 90 } (ostatnie 3)
int[] slice6 = numbers[^5..^2];  // { 50, 60, 70 } (5 od końca do 2 od końca)

// Pojedynczy element od końca
int last = numbers[^1];    // 90 (ostatni element)
int secondLast = numbers[^2];  // 80 (przedostatni)
🔍 Range operator - jak działa?
  • [start..end] - od start (inclusive) do end (exclusive)
  • [..n] - pierwsze n elementów
  • [n..] - od n do końca
  • [^n] - n-ty element od końca (^1 = ostatni, ^2 = przedostatni)
  • [^n..^m] - od n-tego do m-tego od końca

Kiedy używać tablic?

✅ Używaj tablic gdy:
  • Znasz rozmiar z góry (nie zmienia się)
  • Potrzebujesz super szybkiego dostępu przez indeks
  • Pracujesz z low-level kodem (interop, unsafe)
  • Prosty, jednowymiarowy zbiór danych
// ✅ Dobry przypadek
int[] daysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
byte[] buffer = new byte[1024];  // fixed buffer
❌ NIE używaj tablic gdy:
  • Rozmiar się zmienia (dodajesz/usuwasz elementy)
  • Potrzebujesz częstego wyszukiwania po wartości
  • Potrzebujesz Insert/Remove na początku/środku
  • Zamiast tego użyj List<T>, Dictionary<K,V>, HashSet<T>
// ❌ Zły przypadek - użyj List!
int[] users = new int[10];  
// Co jeśli będzie 11 użytkowników? 😱
List<T> - dynamiczna lista

Podstawy List<T>

List<T> to najpopularniejsza kolekcja w C#. Dynamiczny rozmiar, łatwe API, świetna performance.

// Utworzenie pustej listy
List<int> numbers = new List<int>();

// Add - dodaj element na koniec
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);

Console.WriteLine(numbers.Count);  // 3 (nie Length - Count!)

// Dostęp przez indeks (jak array)
Console.WriteLine(numbers[0]);  // 10
numbers[1] = 25;  // zmiana wartości

// Inicjalizacja z wartościami - collection initializer
List<string> names = new List<string> { "Jan", "Anna", "Piotr" };

// Target-typed new (C# 9+)
List<int> numbers2 = new() { 1, 2, 3, 4, 5 };

// Var + target-typed new
var colors = new List<string> { "Red", "Green", "Blue" };

List<T> methods

var fruits = new List { "Jabłko", "Banan", "Gruszka" };

// Add - dodaj na koniec
fruits.Add("Wiśnia");
// { "Jabłko", "Banan", "Gruszka", "Wiśnia" }

// AddRange - dodaj wiele elementów
fruits.AddRange(new[] { "Truskawka", "Malina" });
// { "Jabłko", "Banan", "Gruszka", "Wiśnia", "Truskawka", "Malina" }

// Insert - wstaw na określonej pozycji
fruits.Insert(0, "Ananas");  // wstaw na początku
// { "Ananas", "Jabłko", "Banan", "Gruszka", "Wiśnia", "Truskawka", "Malina" }

// Remove - usuń element (pierwsza znaleziona)
fruits.Remove("Banan");
// { "Ananas", "Jabłko", "Gruszka", "Wiśnia", "Truskawka", "Malina" }

// RemoveAt - usuń po indeksie
fruits.RemoveAt(0);  // usuń pierwszy
// { "Jabłko", "Gruszka", "Wiśnia", "Truskawka", "Malina" }

// RemoveAll - usuń wszystkie spełniające warunek
fruits.RemoveAll(f => f.StartsWith("M"));  // usuń te zaczynające się na M
// { "Jabłko", "Gruszka", "Wiśnia", "Truskawka" }

// Clear - wyczyść całą listę
fruits.Clear();
Console.WriteLine(fruits.Count);  // 0

Wyszukiwanie w List<T>

var numbers = new List { 10, 20, 30, 40, 50, 30 };

// Contains - czy zawiera element
bool has30 = numbers.Contains(30);  // true

// IndexOf - znajdź indeks pierwszego wystąpienia
int index = numbers.IndexOf(30);  // 2

// LastIndexOf - znajdź indeks ostatniego wystąpienia
int lastIndex = numbers.LastIndexOf(30);  // 5

// Find - znajdź pierwszy element spełniający warunek
int? first = numbers.Find(n => n > 25);  // 30

// FindAll - znajdź wszystkie spełniające warunek
List<int> greaterThan25 = numbers.FindAll(n => n > 25);
// { 30, 40, 50, 30 }

// Exists - czy istnieje element spełniający warunek
bool exists = numbers.Exists(n => n > 100);  // false

// Any - LINQ (lepsze od Exists)
bool any = numbers.Any(n => n > 25);  // true

// Where - LINQ (lepsze od FindAll)
var filtered = numbers.Where(n => n > 25).ToList();

Sortowanie i odwracanie

var numbers = new List { 5, 2, 8, 1, 9, 3 };

// Sort - sortowanie in-place (modyfikuje listę!)
numbers.Sort();
Console.WriteLine(string.Join(", ", numbers));  // 1, 2, 3, 5, 8, 9

// Sort descending - malejąco
numbers.Sort((a, b) => b.CompareTo(a));
Console.WriteLine(string.Join(", ", numbers));  // 9, 8, 5, 3, 2, 1

// Reverse - odwróć kolejność
numbers.Reverse();

// LINQ OrderBy - NIE modyfikuje oryginalnej listy (zwraca nową)
var sorted = numbers.OrderBy(n => n).ToList();
var sortedDesc = numbers.OrderByDescending(n => n).ToList();
💡 Sortowanie obiektów
record Person(string Name, int Age);

var people = new List
{
    new("Anna", 25),
    new("Jan", 30),
    new("Piotr", 20)
};

// Sortuj po wieku
people.Sort((a, b) => a.Age.CompareTo(b.Age));
// Anna(25), Piotr(20), Jan(30) → Piotr(20), Anna(25), Jan(30)

// LINQ - czytelniejsze!
var sortedByAge = people.OrderBy(p => p.Age).ToList();
var sortedByName = people.OrderBy(p => p.Name).ToList();

// Sortowanie po wielu polach
var sorted = people
    .OrderBy(p => p.Age)
    .ThenBy(p => p.Name)
    .ToList();

Konwersja List ↔ Array

// List → Array
List<int> list = new() { 1, 2, 3, 4, 5 };
int[] array = list.ToArray();

// Array → List
int[] numbers = { 10, 20, 30 };
List<int> list2 = numbers.ToList();

// Uwaga: ToList() i ToArray() KOPIUJĄ elementy (nowa kolekcja!)
🔥 Collection Expressions - C# 12

Stary problem - verbose initialization

// Stary sposób - dużo pisania
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int[] array = new int[] { 1, 2, 3, 4, 5 };
ImmutableArray<int> immutable = ImmutableArray.Create(1, 2, 3, 4, 5);

// Powtarzasz typ, używasz różnych składni...

Rozwiązanie - Collection Expressions!

🎉 NOWOŚĆ C# 12 - Collection Expressions

Użyj [...] do tworzenia DOWOLNEJ kolekcji! Jedna składnia dla wszystkich!

// Collection expression - [...]
List<int> numbers = [1, 2, 3, 4, 5];
int[] array = [1, 2, 3, 4, 5];
ImmutableArray<int> immutable = [1, 2, 3, 4, 5];
Span<int> span = [1, 2, 3, 4, 5];

// ✅ Ta sama składnia dla wszystkich typów kolekcji!
// ✅ Kompilator wie jaki typ stworzyć z kontekstu!
🔍 Jak to działa?

Collection expressions [...] to compile-time feature. Kompilator patrzy na typ zmiennej i generuje odpowiedni kod:

List list = [1, 2, 3];
// Kompilator generuje:
// List<int> list = new List<int> { 1, 2, 3 };

int[] array = [1, 2, 3];
// Kompilator generuje:
// int[] array = new int[] { 1, 2, 3 };

Spread operator (..) - łączenie kolekcji

int[] first = [1, 2, 3];
int[] second = [4, 5, 6];

// Spread operator - .. rozpakowuje kolekcję
int[] combined = [..first, ..second];
// [1, 2, 3, 4, 5, 6]

// Możesz mieszać elementy i spread
int[] mixed = [0, ..first, 10, ..second, 20];
// [0, 1, 2, 3, 10, 4, 5, 6, 20]

// Działa z różnymi typami kolekcji
List<string> list1 = ["a", "b"];
string[] array1 = ["c", "d"];
IEnumerable<string> enumerable = ["e", "f"];

List<string> all = [..list1, ..array1, ..enumerable];
// ["a", "b", "c", "d", "e", "f"]
💡 Praktyczne przykłady spread operator
// Dodaj element na początek
List<int> numbers = [1, 2, 3];
List<int> withZero = [0, ..numbers];
// [0, 1, 2, 3]

// Dodaj element na koniec
List<int> withTen = [..numbers, 10];
// [1, 2, 3, 10]

// Wstaw w środku
List<int> inserted = [..numbers[..2], 99, ..numbers[2..]];
// [1, 2, 99, 3]

// Połącz wiele list
var users1 = GetActiveUsers();
var users2 = GetInactiveUsers();
var users3 = GetPendingUsers();
var allUsers = [..users1, ..users2, ..users3];

// Usuń duplikaty (przez HashSet)
int[] withDuplicates = [1, 2, 2, 3, 3, 3, 4];
int[] unique = [..new HashSet<int>(withDuplicates)];
// [1, 2, 3, 4]

Collection expressions dla różnych typów

// List
List<string> list = ["a", "b", "c"];

// Array
string[] array = ["a", "b", "c"];

// IEnumerable<T>
IEnumerable<int> enumerable = [1, 2, 3];

// IReadOnlyList<T>
IReadOnlyList<int> readOnly = [1, 2, 3];

// Span<T> i ReadOnlySpan<T>
Span<int> span = [1, 2, 3];
ReadOnlySpan<char> chars = ['a', 'b', 'c'];

// ImmutableArray<T>
ImmutableArray<int> immutable = [1, 2, 3];

// ImmutableList<T>
ImmutableList<string> immutableList = ["a", "b", "c"];

// HashSet<T> - NIE DZIAŁA (na razie)
// HashSet<int> set = [1, 2, 3];  // ❌ błąd kompilacji
// Musisz:
HashSet<int> set = [..new[] { 1, 2, 3 }];  // OK
// lub po prostu:
HashSet<int> set2 = new([1, 2, 3]);  // target-typed new
❌ Przed C# 12 - verbose
// Dużo pisania, różne składnie
List<int> list = new List<int> { 1, 2, 3 };
int[] array = new int[] { 1, 2, 3 };
ImmutableArray<int> ia = ImmutableArray.Create(1, 2, 3);

// Łączenie - skomplikowane
var combined = new List<int>();
combined.AddRange(list1);
combined.AddRange(list2);
combined.AddRange(list3);
✅ C# 12 - collection expressions
// Jedna składnia, czytelne!
List<int> list = [1, 2, 3];
int[] array = [1, 2, 3];
ImmutableArray<int> ia = [1, 2, 3];

// Łączenie - super proste!
var combined = [..list1, ..list2, ..list3];
Dictionary<TKey, TValue> - klucz-wartość

Podstawy Dictionary

Dictionary<TKey, TValue> przechowuje pary klucz-wartość. Super szybkie wyszukiwanie po kluczu (O(1))!

// Utworzenie pustego dictionary
Dictionary<string, int> ages = new Dictionary<string, int>();

// Add - dodaj parę klucz-wartość
ages.Add("Jan", 30);
ages.Add("Anna", 25);
ages.Add("Piotr", 35);

// Dostęp po kluczu
Console.WriteLine(ages["Jan"]);  // 30

// Zmiana wartości
ages["Jan"] = 31;

// Inicjalizacja z wartościami - collection initializer
Dictionary<string, string> capitals = new Dictionary<string, string>
{
    { "Polska", "Warszawa" },
    { "Niemcy", "Berlin" },
    { "Francja", "Paryż" }
};

// C# 6+ - Index initializer (czytelniejszy!)
Dictionary<string, int> scores = new Dictionary<string, int>
{
    ["Jan"] = 100,
    ["Anna"] = 95,
    ["Piotr"] = 87
};

// C# 9+ - target-typed new
Dictionary<int, string> users = new()
{
    [1] = "Admin",
    [2] = "User",
    [3] = "Guest"
};

Dictionary methods

var dict = new Dictionary<string, int>
{
    ["apple"] = 1,
    ["banana"] = 2,
    ["cherry"] = 3
};

// ContainsKey - czy klucz istnieje
if (dict.ContainsKey("apple"))
{
    Console.WriteLine("Apple exists!");
}

// ContainsValue - czy wartość istnieje (wolne!)
bool hasTwo = dict.ContainsValue(2);  // true

// TryGetValue - bezpieczny dostęp (bez exception!)
if (dict.TryGetValue("apple", out int value))
{
    Console.WriteLine($"Apple = {value}");
}
else
{
    Console.WriteLine("Apple not found");
}

// Remove - usuń parę
dict.Remove("banana");

// Keys i Values - kolekcje kluczy i wartości
foreach (string key in dict.Keys)
{
    Console.WriteLine(key);
}

foreach (int value in dict.Values)
{
    Console.WriteLine(value);
}

// Iteracja po parach
foreach (KeyValuePair<string, int> pair in dict)
{
    Console.WriteLine($"{pair.Key} = {pair.Value}");
}

// C# 7+ - deconstruction
foreach (var (key, value) in dict)
{
    Console.WriteLine($"{key} = {value}");
}
⚠️ Pułapki Dictionary
var dict = new Dictionary<string, int> { ["a"] = 1 };

// ❌ KeyNotFoundException jeśli klucz nie istnieje!
int value = dict["b"];  // WYJĄTEK!

// ✅ Bezpieczne sposoby:

// 1. TryGetValue
if (dict.TryGetValue("b", out int v))
{
    // klucz istnieje
}

// 2. ContainsKey
if (dict.ContainsKey("b"))
{
    int val = dict["b"];
}

// 3. GetValueOrDefault (C# 6+)
int result = dict.GetValueOrDefault("b", 0);  // 0 jeśli nie ma
💡 Praktyczny przykład - liczenie wystąpień
string text = "hello world hello";
string[] words = text.Split(' ');

// Zlicz ile razy każde słowo występuje
var wordCount = new Dictionary<string, int>();

foreach (string word in words)
{
    if (wordCount.ContainsKey(word))
    {
        wordCount[word]++;
    }
    else
    {
        wordCount[word] = 1;
    }
}

// Lub krócej z TryGetValue:
var wordCount2 = new Dictionary<string, int>();
foreach (string word in words)
{
    wordCount2[word] = wordCount2.GetValueOrDefault(word, 0) + 1;
}

// Wynik:
// "hello" = 2
// "world" = 1

// Najkrócej z LINQ:
var wordCount3 = words
    .GroupBy(w => w)
    .ToDictionary(g => g.Key, g => g.Count());

Kiedy używać Dictionary?

  • ✅ Szybkie wyszukiwanie po kluczu (O(1))
  • ✅ Mapowanie klucz → wartość (ID → User, code → description)
  • ✅ Liczenie wystąpień (word → count)
  • ✅ Cache/lookup tables
  • ✅ Unikalne klucze (duplikaty kluczy są zabronione)
HashSet<T> - unikalne wartości

Podstawy HashSet

HashSet<T> to kolekcja unikalnych elementów. Automatycznie ignoruje duplikaty. Super szybkie sprawdzanie czy element istnieje (O(1))!

// Utworzenie pustego HashSet
HashSet<int> numbers = new HashSet<int>();

// Add - dodaj element (zwraca bool: true jeśli dodano, false jeśli duplikat)
bool added1 = numbers.Add(10);  // true
bool added2 = numbers.Add(20);  // true
bool added3 = numbers.Add(10);  // false (duplikat!)

Console.WriteLine(numbers.Count);  // 2 (10 i 20)

// Inicjalizacja z wartościami
HashSet<string> fruits = new HashSet<string>
{
    "Jabłko", "Banan", "Jabłko", "Gruszka"
};
// Duplikat "Jabłko" został zignorowany
Console.WriteLine(fruits.Count);  // 3

// Z collection
int[] withDuplicates = { 1, 2, 2, 3, 3, 3, 4, 4, 4, 4 };
HashSet<int> unique = new HashSet<int>(withDuplicates);
// { 1, 2, 3, 4 } - duplikaty usunięte!

HashSet operations - teoria zbiorów

HashSet<int> set1 = new HashSet<int> { 1, 2, 3, 4, 5 };
HashSet<int> set2 = new HashSet<int> { 4, 5, 6, 7, 8 };

// UnionWith - suma zbiorów (modyfikuje set1!)
var union = new HashSet<int>(set1);
union.UnionWith(set2);
// { 1, 2, 3, 4, 5, 6, 7, 8 }

// IntersectWith - część wspólna
var intersect = new HashSet<int>(set1);
intersect.IntersectWith(set2);
// { 4, 5 }

// ExceptWith - różnica (elementy z set1 NIE występujące w set2)
var except = new HashSet<int>(set1);
except.ExceptWith(set2);
// { 1, 2, 3 }

// SymmetricExceptWith - różnica symetryczna (w jednym ale nie w obu)
var symmetric = new HashSet<int>(set1);
symmetric.SymmetricExceptWith(set2);
// { 1, 2, 3, 6, 7, 8 }

// Sprawdzanie relacji zbiorów
bool isSubset = set1.IsSubsetOf(set2);  // false
bool isSuperset = set1.IsSupersetOf(set2);  // false
bool overlaps = set1.Overlaps(set2);  // true (mają 4 i 5)
💡 Praktyczne przykłady HashSet
// 1. Usuwanie duplikatów z listy
List<int> listWithDupes = new() { 1, 2, 2, 3, 3, 3, 4 };
List<int> unique = new HashSet<int>(listWithDupes).ToList();
// [1, 2, 3, 4]

// 2. Szybkie sprawdzanie czy element istnieje
HashSet<string> allowedRoles = new() { "Admin", "User", "Guest" };
string userRole = "Moderator";

if (allowedRoles.Contains(userRole))  // O(1) - super szybkie!
{
    Console.WriteLine("Role allowed");
}

// 3. Znajdź wspólne elementy dwóch list
List<int> favorites1 = new() { 1, 5, 10, 15, 20 };
List<int> favorites2 = new() { 5, 10, 25, 30 };

var common = new HashSet<int>(favorites1);
common.IntersectWith(favorites2);
// { 5, 10 }

// 4. Email deduplication
string[] emails = 
{ 
    "jan@example.com", 
    "anna@example.com", 
    "jan@example.com",  // duplikat
    "piotr@example.com" 
};
var uniqueEmails = new HashSet<string>(emails);
// 3 unikalne emaile

HashSet vs List - kiedy którego używać?

Operacja List<T> HashSet<T>
Dostęp przez indeks ✅ O(1) - list[5] ❌ Brak indeksów
Contains ⚠️ O(n) - wolne ✅ O(1) - szybkie
Add ✅ O(1) - na końcu ✅ O(1) - ale ignoruje duplikaty
Duplikaty ✅ Dozwolone ❌ Automatycznie usuwane
Kolejność ✅ Zachowana (insertion order) ❌ Nie gwarantowana
Użycie Uporządkowana kolekcja z duplikatami Unikalne wartości, szybkie Contains
LINQ - podstawy pracy z kolekcjami

Czym jest LINQ?

🔍 LINQ = Language Integrated Query

LINQ to zestaw metod rozszerzających do pracy z kolekcjami. Pozwala filtrować, transformować, grupować dane w czytelny sposób.

Zamiast pisać pętle for/foreach, używasz LINQ - krócej i czytelniej!

Najpopularniejsze operatory LINQ

List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Where - filtrowanie
var even = numbers.Where(n => n % 2 == 0).ToList();
// [2, 4, 6, 8, 10]

// Select - transformacja (projection)
var doubled = numbers.Select(n => n * 2).ToList();
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// First / FirstOrDefault - pierwszy element
int first = numbers.First();  // 1
int firstEven = numbers.First(n => n % 2 == 0);  // 2
int? noMatch = numbers.FirstOrDefault(n => n > 100);  // null (C# 10+)

// Last / LastOrDefault - ostatni element
int last = numbers.Last();  // 10

// Single - dokładnie jeden element (rzuca wyjątek jeśli 0 lub więcej!)
// int single = numbers.Single();  // ❌ InvalidOperationException (więcej niż 1)

// Any - czy jakikolwiek spełnia warunek
bool hasEven = numbers.Any(n => n % 2 == 0);  // true
bool hasNegative = numbers.Any(n => n < 0);  // false

// All - czy wszystkie spełniają warunek
bool allPositive = numbers.All(n => n > 0);  // true
bool allEven = numbers.All(n => n % 2 == 0);  // false

// Count - ile elementów spełnia warunek
int evenCount = numbers.Count(n => n % 2 == 0);  // 5

// Sum, Average, Min, Max
int sum = numbers.Sum();  // 55
double avg = numbers.Average();  // 5.5
int min = numbers.Min();  // 1
int max = numbers.Max();  // 10

// OrderBy / OrderByDescending - sortowanie
var sorted = numbers.OrderBy(n => n).ToList();
var sortedDesc = numbers.OrderByDescending(n => n).ToList();

// Take / Skip - paginacja
var first3 = numbers.Take(3).ToList();  // [1, 2, 3]
var skip3 = numbers.Skip(3).ToList();  // [4, 5, 6, 7, 8, 9, 10]
var page2 = numbers.Skip(3).Take(3).ToList();  // [4, 5, 6] (strona 2, 3 na stronę)

// Distinct - unikalne wartości
int[] withDupes = [1, 1, 2, 2, 3, 3];
var unique = withDupes.Distinct().ToList();  // [1, 2, 3]

Łańcuchowanie (chaining) LINQ

List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Łańcuchowanie - każda metoda zwraca IEnumerable<T>
var result = numbers
    .Where(n => n % 2 == 0)      // filtruj parzyste
    .Select(n => n * 2)          // podwój
    .OrderByDescending(n => n)   // sortuj malejąco
    .Take(3)                     // weź pierwsze 3
    .ToList();                   // konwertuj do List

Console.WriteLine(string.Join(", ", result));
// 20, 16, 12

// Czytelność - każdy krok w nowej linii
var users = GetUsers()
    .Where(u => u.IsActive)
    .Where(u => u.Age >= 18)
    .OrderBy(u => u.LastName)
    .ThenBy(u => u.FirstName)
    .Select(u => new { u.Id, u.FullName, u.Email })
    .ToList();
💡 Praktyczny przykład - przetwarzanie danych
record Product(string Name, decimal Price, string Category, bool InStock);

var products = new List<Product>
{
    new("Laptop", 2999, "Electronics", true),
    new("Mouse", 49, "Electronics", true),
    new("Desk", 599, "Furniture", false),
    new("Chair", 399, "Furniture", true),
    new("Monitor", 899, "Electronics", true)
};

// Znajdź dostępne produkty elektroniczne, posortuj po cenie, weź 2 najtańsze
var cheapElectronics = products
    .Where(p => p.Category == "Electronics")
    .Where(p => p.InStock)
    .OrderBy(p => p.Price)
    .Take(2)
    .ToList();

// Mouse (49), Monitor (899)

// Średnia cena produktów w magazynie
var avgPrice = products
    .Where(p => p.InStock)
    .Average(p => p.Price);

// Grupowanie po kategorii
var byCategory = products
    .GroupBy(p => p.Category)
    .Select(g => new 
    { 
        Category = g.Key, 
        Count = g.Count(),
        AvgPrice = g.Average(p => p.Price)
    })
    .ToList();

// Electronics: Count=3, AvgPrice=1315.67
// Furniture: Count=2, AvgPrice=499
⚡ LINQ Performance - deferred execution

LINQ używa deferred execution (opóźnione wykonanie) - query NIE wykonuje się od razu!

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// To NIE wykonuje się jeszcze!
var query = numbers.Where(n => n > 2);

// Dodajemy więcej elementów
numbers.Add(6);
numbers.Add(7);

// Dopiero teraz wykonuje się (gdy iterujemy)
foreach (int n in query)
{
    Console.WriteLine(n);  // 3, 4, 5, 6, 7 (włącznie z nowymi!)
}

// Aby wykonać od razu - użyj ToList(), ToArray(), Count(), etc.
var executed = numbers.Where(n => n > 2).ToList();  // wykonane!
Podsumowanie

Mega wpis o kolekcjach! Nauczyłeś się wszystkiego o pracy z wieloma elementami w C# 14:

  • Arrays – stały rozmiar, szybki dostęp, range operator [..]
  • List<T> – dynamiczny rozmiar, najpopularniejsza kolekcja
  • 🔥 Collection Expressions (C# 12) – [1, 2, 3] dla wszystkich typów!
  • Spread operator (..) – [..list1, ..list2] łączenie kolekcji
  • Dictionary<K,V> – klucz-wartość, O(1) lookup
  • HashSet<T> – unikalne wartości, teoria zbiorów
  • LINQ basics – Where, Select, OrderBy, GroupBy
  • LINQ chaining – łańcuchowanie operacji
  • Deferred execution – jak działa LINQ pod spodem
  • Kiedy której kolekcji używać – praktyczne wskazówki

W kolejnym wpisie zagłębimy się w kontrolę przepływu programu – if/else z pattern matching, switch expressions, pętle!

Zadanie dla Ciebie 🎯

Stwórz program "Student Grade Manager":

  1. Stwórz klasę/record Student(string Name, List<int> Grades)
  2. Utwórz listę 5 studentów z różnymi ocenami (collection expressions!)
  3. Użyj LINQ do:
    • Znajdź studentów ze średnią > 80
    • Posortuj studentów po średniej ocen (malejąco)
    • Wyświetl top 3 studentów
    • Policz ile jest ocen 100 (wszystkich studentów razem)
    • Znajdź studenta z najwyższą pojedynczą oceną
  4. Użyj collection expressions i spread operator!
// Przykładowe dane:
var students = new List<Student>
{
    new("Jan", [85, 90, 78, 92]),
    new("Anna", [95, 88, 100, 91]),
    new("Piotr", [70, 75, 68, 72]),
    // ... więcej
};
🎯 BONUS: E-commerce Product Filter System

Stwórz system filtrowania produktów używając wszystkich poznanych kolekcji:

Klasy:

record Product(
    int Id, 
    string Name, 
    decimal Price, 
    string Category,
    HashSet Tags,  // np. "new", "sale", "featured"
    bool InStock
);

class ProductCatalog
{
    private List<Product> _products;
    private Dictionary<string, List<Product>> _categoryIndex;
    private HashSet<string> _allTags;

    // Twoje metody tutaj
}

Wymagane metody (wszystkie z LINQ!):

  1. GetProductsByCategory(string category)
  2. GetProductsByPriceRange(decimal min, decimal max)
  3. GetProductsByTags(params string[] tags) - produkty mające WSZYSTKIE tagi
  4. GetProductsByAnyTag(params string[] tags) - produkty mające KTÓRYKOLWIEK tag
  5. GetTopExpensive(int count) - n najdroższych produktów
  6. GetInStockCount() - ile produktów dostępnych
  7. GetAveragePriceByCategory() - Dictionary<string, decimal>
  8. SearchProducts(string query) - wyszukiwanie po nazwie (case-insensitive)

Użyj:

  • ✅ Collection expressions do inicjalizacji
  • ✅ Spread operator do łączenia wyników
  • ✅ LINQ dla wszystkich operacji
  • ✅ Dictionary do indeksowania
  • ✅ HashSet do tagów i deduplikacji
  • ✅ Pattern matching (podpowiedź dla SearchProducts)

Przykład użycia:

var catalog = new ProductCatalog();

// Dodaj produkty z collection expressions
var products = [
    new Product(1, "Laptop Pro", 2999, "Electronics", ["new", "featured"], true),
    new Product(2, "Gaming Mouse", 79, "Electronics", ["sale", "gaming"], true),
    // ... więcej
];

catalog.AddProducts(products);

// Filtrowanie
var saleItems = catalog.GetProductsByTags("sale");
var affordable = catalog.GetProductsByPriceRange(0, 100);
var electronics = catalog.GetProductsByCategory("Electronics");
var combined = catalog.GetProductsByAnyTag("new", "sale", "featured");

Console.WriteLine($"Średnia cena Electronics: {catalog.GetAveragePriceByCategory()["Electronics"]}");

To kompletny system używający List, Dictionary, HashSet, LINQ i collection expressions. Production-ready! 🚀✨