Co to jest kolekcja?

Każda poważna aplikacja w pewnym momencie musi zarządzać nie jednym obiektem, ale wieloma — listą zamówień, koszem klientów, kolejką zadań do przetworzenia, cache’em produktów po ID. Jak je przechowujesz, decyduje o tym, czy Twój kod będzie szybki, czytelny i skalowalny — czy będziesz debugował dziwne bugi o 2 w nocy.

Ten wpis to mapa kolekcji w .NET — punkt startowy, z którego przejdziesz do szczegółowego materiału dla każdego typu. Jeśli dopiero zaczynasz, zacznij tu. Jeśli szukasz konkretnej kolekcji — przejdź od razu do linków na końcu.

Czym jest kolekcja?

Kolekcja to typ zaprojektowany do grupowania powiązanych obiektów i zarządzania nimi jako jedną całością. Zamiast tworzyć sto zmiennych order1, order2order100, przechowujesz je w jednej kolekcji i operujesz na nich: dodajesz, usuwasz, przeszukujesz, sortujesz, iterujesz.

W .NET kolekcje żyją głównie w przestrzeni nazw System.Collections.Generic — to generyczne (typowane) odpowiedniki starszych, niegeneryckach klas z System.Collections. W nowym kodzie zawsze używasz wariantów generycznych: List<T>, Dictionary<TKey, TValue> itd.

Mapa kolekcji w .NET — szybki przegląd

Pięć podstawowych typów, które pokrywają 95% przypadków w typowej aplikacji biznesowej:

1. Tablica — T[]

Stały rozmiar, maksymalna wydajność. Element wewnętrzny niemal wszystkich innych kolekcji. Używasz jej gdy znasz rozmiar z góry i nie będziesz dodawać ani usuwać elementów.

string[] months = { "Styczeń", "Luty", "Marzec", /* ... */ "Grudzień" };

Console.WriteLine(months[0]);      // Styczeń
Console.WriteLine(months.Length);  // 12

foreach (var month in months)
    Console.WriteLine(month);

👉 Tablica w C# — szczegółowy wpis · Array vs List — kiedy co wybrać?

2. Lista — List<T>

Dynamiczny rozmiar, elastyczność. Domyślny wybór gdy nie wiesz ile elementów będziesz przechowywać. Wewnętrznie to tablica, która automatycznie rośnie.

var orders = new List<string>();

orders.Add("Zamówienie #001");
orders.Add("Zamówienie #002");
orders.Add("Zamówienie #003");

orders.Remove("Zamówienie #002");

Console.WriteLine(orders.Count);   // 2

// Wyszukiwanie z LINQ
var found = orders.FirstOrDefault(o => o.Contains("#003"));
Console.WriteLine(found);          // Zamówienie #003

👉 List w C# — praktyczny przewodnik · Lista — wydajność i pułapki

3. Słownik — Dictionary<TKey, TValue>

Błyskawiczne wyszukiwanie po kluczu — O(1). Zamiast przeszukiwać całą listę, podajesz klucz i od razu dostajesz wartość. Używasz gdy masz unikalne identyfikatory: ID użytkownika, kod produktu, nazwa konfiguracji.

var productCache = new Dictionary<int, string>
{
    [1001] = "Klawiatura mechaniczna",
    [1002] = "Monitor 27 cali",
    [1003] = "Mysz bezprzewodowa"
};

// Szybki dostęp po kluczu
Console.WriteLine(productCache[1002]);  // Monitor 27 cali

// Bezpieczny odczyt — TryGetValue zamiast [key], żeby nie rzucić wyjątku
if (productCache.TryGetValue(9999, out string? name))
    Console.WriteLine(name);
else
    Console.WriteLine("Produkt nie istnieje");

👉 Dictionary w C# — pełny przewodnik

4. HashSet — HashSet<T>

Unikalne elementy, szybkie sprawdzanie przynależności. Idealne gdy chcesz wyeliminować duplikaty albo błyskawicznie sprawdzić, czy element już istnieje.

var visitedPages = new HashSet<string>();

visitedPages.Add("/home");
visitedPages.Add("/products");
visitedPages.Add("/home");     // duplikat — cicho zignorowany

Console.WriteLine(visitedPages.Count);              // 2
Console.WriteLine(visitedPages.Contains("/home"));  // True

// Operacje zbiorowe
var premiumUsers = new HashSet<int> { 1, 2, 3 };
var activeUsers  = new HashSet<int> { 2, 3, 4, 5 };

premiumUsers.IntersectWith(activeUsers);
// premiumUsers = { 2, 3 } — aktywni premium

👉 HashSet w C# — szczegółowy wpis · SortedSet — unikalne i posortowane

5. Kolejka i Stos — Queue<T> i Stack<T>

Kolejność dostępu jest kluczowa. Kolejka (FIFO: first-in, first-out) przetwarza elementy w kolejności dodania. Stos (LIFO: last-in, first-out) — od końca.

// Queue — FIFO: przetwarzaj w kolejności napływania
var emailQueue = new Queue<string>();
emailQueue.Enqueue("jan@example.com");
emailQueue.Enqueue("anna@example.com");

string next = emailQueue.Dequeue();   // jan@example.com (pierwszy wyszedł)
string peek = emailQueue.Peek();      // anna@example.com (podgląd bez usuwania)

// Stack — LIFO: cofanie operacji, historia nawigacji
var history = new Stack<string>();
history.Push("/home");
history.Push("/products");
history.Push("/cart");

string current = history.Pop();   // /cart (ostatni wyszedł)
string prev    = history.Peek();  // /products

👉 Queue w C# — szczegółowy wpis · Stack w C# — szczegółowy wpis

Tabela decyzyjna — co wybrać?

Potrzebujesz…WybierzDlaczego
Stały zestaw, znany rozmiar, max wydajnośćT[]Minimalny narzut, ciągły blok pamięci
Lista, rozmiar zmienia się dynamicznieList<T>Domyślny wybór — elastyczna, bogata w metody
Szybkie wyszukiwanie po unikalnym kluczuDictionary<TKey, TValue>Haszowanie → O(1) lookup
Unikalne elementy, sprawdzanie przynależnościHashSet<T>O(1) Contains, eliminacja duplikatów
Unikalne elementy i zawsze posortowaneSortedSet<T>BST w środku → O(log n), automatyczna kolejność
Przetwarzaj w kolejności FIFO (zadania, eventy)Queue<T>Dequeue od początku — gwarantuje kolejność
Cofanie operacji, stos wywołań (LIFO)Stack<T>Pop z wierzchołka — ostatni wszedł, pierwszy wyszedł
Dostęp z wielu wątków jednocześnieConcurrentBag<T> / ConcurrentQueue<T>Thread-safe bez ręcznych locków
Kolekcja tylko do odczytu (niezmienialność)ImmutableList<T> / IReadOnlyList<T>Bezpieczna do zwrócenia z API, bez ryzyka mutacji

Złożoność operacji — skrótowo

Wybór kolekcji to decyzja o wydajności. Kilka kluczowych liczb:

OperacjaT[]List<T>DictionaryHashSet
Dostęp po indeksieO(1)O(1)
Dodaj na końcuO(1)*O(1)*O(1)*
Usuń ze środkaO(n)O(1)*O(1)*
Wyszukaj (Contains)O(n)O(n)O(1)*O(1)*
Wyszukaj po kluczuO(1)*

* amortyzowane O(1) — sporadyczne realokacje mogą być droższe, ale uśredniony koszt jest stały.

💡 Jeśli masz wiele operacji Contains na dużej kolekcji — różnica między List<T> (O(n)) a HashSet<T> / Dictionary (O(1)) może być przepaść między setkami ms a mikrosekundami.

Wspólna cecha wszystkich kolekcji — IEnumerable

Wszystkie kolekcje w .NET implementują interfejs IEnumerable<T>. To oznacza, że możesz po każdej iterować pętlą foreach i stosować LINQ:

// Działa dla T[], List<T>, Dictionary, HashSet, Queue, Stack...
IEnumerable<int> source = GetAnyCollection();

var filtered = source
    .Where(x => x > 10)
    .OrderByDescending(x => x)
    .Take(5)
    .ToList();

To właśnie dlatego możesz pisać jedną metodę akceptującą IEnumerable<T> i przekazywać do niej dowolną kolekcję — jest to najszerszy, najmniej restrykcyjny kontrakt, gdy potrzebujesz tylko iteracji.

👉 Pętla foreach — jak efektywnie iterować przez kolekcje

Co dalej?

Ten wpis to mapa. Teraz wybierz kolekcję, której potrzebujesz, i przejdź do dedykowanego materiału:

Chcesz opanować kolekcje od zera do produkcji?

Jeśli wolisz uczyć się w ustrukturyzowany sposób — z ćwiczeniami, quizami i przykładami z realnych projektów — sprawdź kurs:

👉 C# Wprowadzenie do Kolekcji — tablica, lista, stos, kolejka, słownik, kiedy co stosować i jak nie popełniać typowych błędów wydajnościowych.

A jeśli dopiero zaczynasz przygodę z C# — ten kurs postaw jako punkt startowy:

👉 C# Podstawy Programowania

Dodaj komentarz