Dictionary i HashSet — wyszukuj po kluczu, pilnuj unikalności
Wypożyczalnia szkolna sprawdza numer urządzenia, a lista obecności nie powinna liczyć tej samej osoby dwa razy. Dwa problemy, których lista nie rozwiązuje dobrze — i dwie kolekcje stworzone właśnie do tego.
Czego się dziś nauczysz
- Utworzysz słownik i dodasz do niego wpisy dwoma sposobami.
- Odczytasz wartość bezpiecznie, metodą
TryGetValue. - Wyjaśnisz, dlaczego klucz musi być unikalny.
- Użyjesz
HashSetdo usuwania powtórzeń. - Dobierzesz listę, słownik albo zbiór do konkretnego zadania.
Przygotowanie: lekcje 01–31. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.
Szukanie po numerze zamiast po kolei
Wypożyczalnia szkolna ma sto urządzeń. Uczeń podaje numer, a program ma powiedzieć, co to za sprzęt. Z listą wygląda to tak:
List<string> numery = new List<string> { "LT-01", "LT-02", "PR-07" };
List<string> nazwy = new List<string> { "laptop Dell", "laptop Lenovo", "drukarka" };
string szukany = "PR-07";
for (int i = 0; i < numery.Count; i++)
{
if (numery[i] == szukany)
{
Console.WriteLine(nazwy[i]);
break;
}
}
Dwa problemy naraz. Po pierwsze: przy stu urządzeniach program przegląda listę element po elemencie. Po drugie, gorsze: dwie listy trzeba trzymać w tej samej kolejności — jedno usunięcie w niewłaściwym miejscu i numery przestają pasować do nazw.
Słownik łączy jedno z drugim w jednej kolekcji:
Dictionary<string, string> sprzet = new Dictionary<string, string>();
sprzet["LT-01"] = "laptop Dell";
sprzet["LT-02"] = "laptop Lenovo";
sprzet["PR-07"] = "drukarka";
Console.WriteLine(sprzet["PR-07"]); // drukarka
Dlaczego słownik jest szybki
Słownik nie przegląda wpisów po kolei. Z klucza wylicza skrót, który wskazuje miejsce w pamięci — i sięga tam od razu. Dlatego wyszukanie w słowniku o stu wpisach trwa tyle samo, co w słowniku o stu tysiącach. Przy liście czas rośnie razem z liczbą elementów.
Klucz jest jeden, wartości mogą się powtarzać
Zapis Dictionary<string, int> czytamy: „słownik, w którym kluczem jest napis, a wartością liczba”. Pierwszy typ to klucz, drugi to wartość.
Dictionary<string, int> punkty = new Dictionary<string, int>();
punkty["Ala"] = 8;
punkty["Bartek"] = 5;
punkty["Cezary"] = 8; // ta sama WARTOSC co Ala - to w porzadku
punkty["Ala"] = 10; // ten sam KLUCZ - zastepuje poprzednia wartosc
Console.WriteLine(punkty["Ala"]); // 10
Console.WriteLine(punkty.Count); // 3
| Klucz | Wartość | |
|---|---|---|
| Czy może się powtórzyć | nie — drugi taki sam zastępuje pierwszy | tak, dowolnie wiele razy |
| Po czym szukamy | po kluczu | — |
| Typowy przykład | numer urządzenia, imię, kod przedmiotu | nazwa, liczba punktów, cena |
Dwa sposoby dodawania — i różnica między nimi
Dictionary<string, int> punkty = new Dictionary<string, int>();
punkty["Ala"] = 8; // dodaje albo NADPISUJE, nigdy nie zglasza bledu
punkty.Add("Bartek", 5); // dodaje; przy istniejacym kluczu -> ArgumentException
// punkty.Add("Ala", 9); // WYJATEK: klucz "Ala" juz istnieje
Który wybrać
Add wybieraj tam, gdzie powtórzony klucz oznacza błąd w danych i chcesz się o nim dowiedzieć. Indeksatora słownik[klucz] = wartość — tam, gdzie nadpisanie jest normalne, na przykład przy aktualizacji wyniku. Świadomy wybór jednego z nich to połowa poprawnego programu na słownikach.
Odczyt, który nie wysadzi programu
Odczyt przez indeksator wygląda niewinnie, ale dla nieistniejącego klucza kończy się wyjątkiem:
Dictionary<string, int> punkty = new Dictionary<string, int> { ["Ala"] = 8 };
Console.WriteLine(punkty["Ala"]); // 8
// Console.WriteLine(punkty["Zenon"]); // KeyNotFoundException
Są trzy bezpieczne drogi. Najważniejsza jest pierwsza.
// 1. TryGetValue - sprawdza i oddaje wartosc w jednym ruchu
if (punkty.TryGetValue("Zenon", out int wynik))
{
Console.WriteLine($"Zenon ma {wynik} pkt");
}
else
{
Console.WriteLine("Nie ma takiej osoby");
}
// 2. ContainsKey - czytelne, ale przeszukuje slownik dwa razy
if (punkty.ContainsKey("Ala"))
{
Console.WriteLine(punkty["Ala"]);
}
// 3. wartosc zastepcza, gdy brak klucza nie jest bledem
int ile = punkty.GetValueOrDefault("Zenon", 0);
Wartość z out tylko w gałęzi powodzenia
Gdy TryGetValue zwróci false, zmienna wynik dostaje wartość domyślną typu — dla int jest to zero. Program się skompiluje i wypisze „Zenon ma 0 pkt”, co wygląda jak prawdziwy wynik. Zera z nieudanego odczytu nie wolno używać poza gałęzią if.
Przechodzenie po słowniku
foreach (KeyValuePair<string, int> wpis in punkty)
{
Console.WriteLine($"{wpis.Key}: {wpis.Value}");
}
foreach (string imie in punkty.Keys) { }
foreach (int wartosc in punkty.Values) { }
Kolejność nie jest gwarantowana
Słownik nie obiecuje żadnej kolejności wpisów i może ją zmienić po dodaniu elementu. Jeśli wynik ma być uporządkowany, posortuj go jawnie przy wypisywaniu — albo użyj SortedDictionary, który trzyma klucze w porządku rosnącym kosztem wolniejszego dodawania.
HashSet — zbiór bez powtórzeń
Drugi problem: lista obecności, na której nikt nie może figurować dwa razy. Z listą trzeba przed każdym dodaniem sprawdzać Contains. HashSet<T> robi to sam.
HashSet<string> obecni = new HashSet<string>();
Console.WriteLine(obecni.Add("Ala")); // True - dodano
Console.WriteLine(obecni.Add("Bartek")); // True
Console.WriteLine(obecni.Add("Ala")); // False - juz byl, nic sie nie zmienilo
Console.WriteLine(obecni.Count); // 2
Console.WriteLine(obecni.Contains("Ala")); // True
Wynik Add to gotowa odpowiedź na pytanie „czy to pierwsze zgłoszenie tej osoby”. Nie trzeba niczego sprawdzać wcześniej.
Usuwanie powtórzeń jednym ruchem
List<string> zgloszenia = new List<string> { "Ala", "Bartek", "Ala", "Cezary", "Bartek" };
HashSet<string> unikalne = new HashSet<string>(zgloszenia);
Console.WriteLine(zgloszenia.Count); // 5
Console.WriteLine(unikalne.Count); // 3
| Kolekcja | Czym się wyróżnia | Kiedy wybrać |
|---|---|---|
List<T> | kolejność i indeksy, powtórzenia dozwolone | gdy liczy się kolejność albo numer pozycji |
Dictionary<K, V> | szybkie szukanie po kluczu | gdy masz identyfikator i chcesz po nim sięgać |
HashSet<T> | brak powtórzeń, szybkie Contains | gdy interesuje cię tylko „jest czy nie ma” |
Zbiór nie ma indeksów
Zapis obecni[0] nie skompiluje się — HashSet nie jest listą i nie ma pojęcia „pierwszy element”. Kolejność przy wypisywaniu też nie jest gwarantowana. Gdy potrzebujesz jednocześnie unikalności i kolejności, trzymaj listę i zbiór obok siebie: lista przechowuje kolejność, zbiór pilnuje powtórzeń.
Przykład: wypożyczalnia i lista obecności
using System.Collections.Generic;
class Program
{
static void Sprawdz(Dictionary<string, string> sprzet, string numer)
{
if (sprzet.TryGetValue(numer, out string? nazwa))
{
Console.WriteLine($"{numer}: {nazwa}");
}
else
{
Console.WriteLine($"{numer}: nie znam takiego numeru");
}
}
static void Main()
{
Dictionary<string, string> sprzet = new Dictionary<string, string>
{
["LT-01"] = "laptop Dell",
["LT-02"] = "laptop Lenovo",
["PR-07"] = "drukarka"
};
Sprawdz(sprzet, "LT-02");
Sprawdz(sprzet, "XX-99");
sprzet["LT-02"] = "laptop Lenovo (naprawiony)"; // nadpisanie
Sprawdz(sprzet, "LT-02");
Console.WriteLine($"\nUrzadzen w bazie: {sprzet.Count}");
foreach (KeyValuePair<string, string> wpis in sprzet)
{
Console.WriteLine($" {wpis.Key} -> {wpis.Value}");
}
Console.WriteLine("\nLISTA OBECNOSCI");
string[] zgloszenia = { "Ala", "Bartek", "Ala", "Cezary", "Bartek", "Ala" };
HashSet<string> obecni = new HashSet<string>();
foreach (string imie in zgloszenia)
{
if (!obecni.Add(imie))
{
Console.WriteLine($" {imie} juz sie zapisal - pomijam");
}
}
Console.WriteLine($"Zgloszen: {zgloszenia.Length}, osob: {obecni.Count}");
}
}
LT-02: laptop Lenovo
XX-99: nie znam takiego numeru
LT-02: laptop Lenovo (naprawiony)
Urzadzen w bazie: 3
LT-01 -> laptop Dell
LT-02 -> laptop Lenovo (naprawiony)
PR-07 -> drukarka
LISTA OBECNOSCI
Ala juz sie zapisal - pomijam
Bartek juz sie zapisal - pomijam
Ala juz sie zapisal - pomijam
Zgloszen: 6, osob: 3
Co dzieje się po kolei
TryGetValueobsługuje oba przypadki w jednym miejscu — nieznany numer nie przerywa programu, tylko daje komunikat.- Typ
string?przyoutto typ dopuszczający brak wartości z lekcji 34. Gdy odczyt się nie uda, zmienna dostajenull. - Przypisanie do istniejącego klucza
LT-02nadpisuje wartość i nie zwiększaCount— to wciąż trzy urządzenia. - Warunek
if (!obecni.Add(imie))czyta się „jeśli nie udało się dodać”, czyli „jeśli już był”. Jedno wywołanie robi dwie rzeczy: sprawdza i dodaje. - Sześć zgłoszeń, trzy osoby — zbiór odrzucił trzy powtórzenia, nie zgłaszając żadnego błędu.
Zestawienie elementów
| Element | Zwraca | Działanie i ograniczenia |
|---|---|---|
slownik[klucz] = wartosc | — | Dodaje albo nadpisuje. Nigdy nie zgłasza błędu przy zapisie. |
slownik[klucz] (odczyt) | wartość | Dla nieistniejącego klucza KeyNotFoundException. |
Add(klucz, wartosc) | — | Dodaje; przy istniejącym kluczu ArgumentException. |
TryGetValue(klucz, out x) | bool | Najbezpieczniejszy odczyt. Przy false zmienna ma wartość domyślną — nie używaj jej. |
ContainsKey(klucz) | bool | Sprawdza obecność klucza. Razem z odczytem przeszukuje słownik dwa razy. |
GetValueOrDefault(klucz, x) | wartość | Wartość albo podana wartość zastępcza. Wygodne przy zliczaniu. |
Remove(klucz) | bool | Usuwa wpis; false, gdy klucza nie było. |
Keys / Values | kolekcje | Same klucze albo same wartości; do przejścia pętlą foreach. |
KeyValuePair<K, V> | typ | Para klucz-wartość z pól Key i Value; element słownika w foreach. |
HashSet<T>.Add(x) | bool | true przy nowym elemencie, false przy powtórzeniu. |
HashSet<T>.Contains(x) | bool | Sprawdzenie obecności, szybkie niezależnie od rozmiaru zbioru. |
new HashSet<T>(kolekcja) | HashSet<T> | Kopia bez powtórzeń — najkrótszy sposób na usunięcie duplikatów. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
Odczyt slownik[klucz] bez sprawdzenia | KeyNotFoundException przy pierwszym nieznanym kluczu. Użyj TryGetValue. |
Add tam, gdzie klucz może się powtórzyć | ArgumentException. Do aktualizacji służy indeksator. |
Użycie zmiennej out poza gałęzią powodzenia | Dla int dostaniesz zero, dla napisu null — i to wygląda jak prawdziwy wynik. |
| Poleganie na kolejności wpisów | Słownik i zbiór nie gwarantują kolejności. Sortuj jawnie przy wypisywaniu. |
zbior[0] | HashSet nie ma indeksatora — to nie lista. |
Dodawanie do słownika w trakcie foreach po nim | InvalidOperationException, tak samo jak przy liście. Zbierz zmiany obok i zastosuj po pętli. |
| Dwie listy zamiast słownika | Nic nie pilnuje, żeby pozostały zgodne. Jeśli jedna wartość opisuje drugą, to jest słownik. |
Zadania
Utwórz Dictionary<string, string> z pięcioma kodami przedmiotów i ich nazwami. Wypisz wszystkie pary, a potem odczytaj jedną wybraną po kluczu.
Rozbuduj poprzedni program tak, żeby wczytywał kod od użytkownika i wypisywał nazwę przedmiotu albo komunikat o nieznanym kodzie. Użyj TryGetValue, nie indeksatora.
Mając tablicę imion z powtórzeniami, wypisz liczbę wszystkich zgłoszeń i liczbę różnych osób. Skorzystaj z HashSet utworzonego z tej tablicy.
Wczytaj zdanie i policz, ile razy występuje każda litera. Wynik trzymaj w Dictionary<char, int> i wypisz tylko litery występujące więcej niż raz. Podpowiedź: GetValueOrDefault upraszcza zwiększanie licznika dla nowego klucza.
Zbuduj Dictionary<string, List<int>> — imię ucznia i lista jego ocen. Napisz metody dodające ocenę oraz liczące średnią jednej osoby. Dodanie oceny osobie, której jeszcze nie ma w dzienniku, ma utworzyć dla niej nową listę, a nie zgłosić błąd.
Napisz program wypożyczalni z menu: dodaj sprzęt, wypożycz po numerze, zwróć, pokaż dostępne. Numery trzymaj w słowniku, a zbiór wypożyczonych numerów w HashSet. Program ma odrzucać wypożyczenie sprzętu już wypożyczonego oraz zwrot sprzętu, którego nikt nie wypożyczył — w obu przypadkach z czytelnym komunikatem, bez wyjątku.
Co trzeba zapamiętać
- Słownik łączy klucz z wartością i wyszukuje po kluczu niezależnie od liczby wpisów.
- Klucz musi być unikalny; wartości mogą się powtarzać.
- Indeksator dodaje albo nadpisuje,
Addzgłasza wyjątek przy istniejącym kluczu — wybieraj świadomie. - Odczyt nieznanego klucza przez indeksator kończy się wyjątkiem;
TryGetValuejest bezpieczne. - Wartości z parametru
outużywamy tylko wtedy, gdy odczyt się powiódł. HashSetnie przechowuje powtórzeń, a jegoAddod razu mówi, czy element był nowy.- Ani słownik, ani zbiór nie gwarantują kolejności elementów.
Dokumentacja: Microsoft Learn — temat tej lekcji.