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:
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 };
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 [..]