Теория
Dictionary<TKey, TValue> съхранява двойки ключ → стойност: телефонен указател (име → номер), ценоразпис (продукт → цена), дневник (студент → оценка). Достъпът по ключ е почти мигновен, независимо колко записа има — за разлика от списък, който трябва да се обходи.
Синтаксисът наподобява масив, но в скобите стои ключ вместо индекс: prices["banana"] = 2.50; записва, prices["banana"] чете. Внимание: четене на несъществуващ ключ хвърля KeyNotFoundException — затова първо проверяваме с ContainsKey() или ползваме TryGetValue().
HashSet<T> е колекция, която пази само уникални стойности — повторното Add на същата стойност просто не прави нищо (и връща false). Идеален за "виждали ли сме това вече?" задачи, без ръчни проверки с Contains по списък.
Queue<T> (опашка) и Stack<T> (стек) определят реда на обработка: опашката е FIFO — първият влязъл излиза първи (като на каса в магазин), стекът е LIFO — последният влязъл излиза първи (като купчина чинии). Срещат се по-рядко в началото, но е добре да знаете, че съществуват.
Стойността в речника може да бъде каквото и да е — включително друга колекция: Dictionary<string, List<string>> е телефонен указател, в който едно име има няколко номера. Най-честият учебен шаблон обаче е броячът: Dictionary<string, int>, в който ключът е "какво броим", а стойността — "колко пъти сме го видели".
Златни правила
Ключовете са уникални
В Dictionary всеки ключ може да се среща само веднъж. Присвояване на съществуващ ключ (dict[key] = value) заменя старата стойност; Add() със съществуващ ключ хвърля грешка.
Проверявайте преди четене
Четенето на несъществуващ ключ (dict["липсващ"]) хвърля KeyNotFoundException. Преди четене проверявайте с ContainsKey() или използвайте TryGetValue().
Изберете правилната колекция
Наредени елементи по позиция → List. Търсене по ключ → Dictionary. Само уникални стойности → HashSet. Ред на обработка → Queue/Stack.
Обхождане с KeyValuePair
При foreach върху речник всеки елемент е KeyValuePair<TKey, TValue> — двойката се достъпва с .Key и .Value.
Бърз справочник
Основни операции с Dictionary
| Операция | Какво прави? | Пример |
|---|---|---|
| Деклариране | Празен речник ключ→стойност | var prices = new Dictionary<string, double>(); |
dict[key] = value | Добавя или заменя стойност по ключ | prices["banana"] = 2.50; |
dict[key] | Чете стойност (гърми при липсващ ключ!) | double p = prices["banana"]; |
ContainsKey() | Има ли такъв ключ (връща bool) | if (prices.ContainsKey("apple")) |
TryGetValue() | Безопасно четене без изключение | if (prices.TryGetValue("apple", out double p)) |
Remove() | Премахва запис по ключ | prices.Remove("banana"); |
Count | Брой записи | prices.Count |
.Keys / .Values | Колекция от всички ключове / стойности | foreach (string k in prices.Keys) |
Коя колекция кога?
| Колекция | Какво пази? | Типична употреба |
|---|---|---|
List<T> | Наредени елементи, дубликати са ОК | Списък с имена, оценки, продукти |
Dictionary<K, V> | Двойки ключ → стойност, уникални ключове | Ценоразпис, телефонен указател, броячи |
HashSet<T> | Само уникални стойности, без ред | Посетени елементи, премахване на дубликати |
Queue<T> | FIFO — пръв влязъл, пръв излязъл | Опашка от задачи, ред на обслужване |
Stack<T> | LIFO — последен влязъл, пръв излязъл | "Undo" история, обратен ред |
Код за анализ и пренаписване
Ценоразпис с Dictionary и уникални посетители с HashSet
using System;
using System.Collections.Generic;
namespace ModuleTen
{
class Program
{
static void Main(string[] args)
{
// Речник: продукт -> цена
Dictionary<string, double> prices = new Dictionary<string, double>();
prices["banana"] = 2.50;
prices["apple"] = 1.20;
prices["orange"] = 3.10;
Console.Write("Кой продукт търсиш? ");
string product = Console.ReadLine();
// Безопасно четене с TryGetValue
if (prices.TryGetValue(product, out double price))
{
Console.WriteLine($"{product} струва {price} лв.");
}
else
{
Console.WriteLine($"Нямаме '{product}' в ценоразписа.");
}
// Обхождане на целия речник
Console.WriteLine("--- Ценоразпис ---");
foreach (KeyValuePair<string, double> item in prices)
{
Console.WriteLine($"{item.Key}: {item.Value} лв.");
}
// HashSet: пази само уникални стойности
HashSet<string> visitors = new HashSet<string>();
visitors.Add("Иван");
visitors.Add("Мария");
visitors.Add("Иван"); // дубликат - игнорира се!
Console.WriteLine($"Уникални посетители: {visitors.Count}"); // 2
}
}
}Какво се случва тук?
-
prices["banana"] = 2.50;добавя запис — синтаксисът е като при масив, но "индексът" е низ-ключ. Ако ключът вече съществува, стойността просто се заменя. -
TryGetValue(product, out double price)опитва да прочете: ако ключът съществува, връщаtrueи записва стойността вprice; ако не — връщаfalse, без да гърми. Това е същият шаблон катоint.TryParseот Модул 1. - Директното
prices[product]при липсващ продукт щеше да хвърлиKeyNotFoundException— затова в реален код четем безопасно. - При
foreachвърху речника всеки елемент еKeyValuePair<string, double>— двойка с.Key(продукта) и.Value(цената). - Третото
visitors.Add("Иван")тихо се игнорира —HashSetне допуска дубликати, затоваCountе 2, а не 3. СъсListщеше да се наложи ръчна проверка сContains.
Шаблонът "брояч": колко пъти се среща всяка буква
using System;
using System.Collections.Generic;
namespace ModuleTenExtra
{
class Program
{
static void Main(string[] args)
{
Console.Write("Въведи текст: ");
string text = Console.ReadLine().ToLower();
Dictionary<char, int> counts = new Dictionary<char, int>();
foreach (char c in text)
{
if (c == ' ')
{
continue; // интервалите не ни интересуват
}
if (counts.ContainsKey(c))
{
counts[c]++; // виждали сме я -> увеличаваме
}
else
{
counts[c] = 1; // първа среща -> създаваме запис
}
}
Console.WriteLine("--- Резултат ---");
foreach (KeyValuePair<char, int> pair in counts)
{
Console.WriteLine($"'{pair.Key}' се среща {pair.Value} пъти");
}
}
}
}Какво се случва тук?
- Това е най-важният шаблон с речници — броене на срещания. Същата логика брои думи, гласове, продажби, оценки…
- Низът се обхожда с
foreach (char c in text)— низът е последователност от символи, затова това просто работи. -
ToLower()на входа гарантира, че "А" и "а" се броят заедно, аcontinueпрескача интервалите. - Сърцето: ако ключът съществува →
counts[c]++, ако не →counts[c] = 1. Без проверкатаContainsKeyредътcounts[c]++щеше да гръмне сKeyNotFoundExceptionпри първата среща на буквата. - Финалното обхождане с
KeyValuePair<char, int>показва и ключа (буквата), и стойността (броя) —.Keyи.Value.
Внимавай! Чести грешки
Грешките, които почти всеки прави в тази тема — виж разликата между грешния и правилния код.
Четене на липсващ ключ
Грешно
Dictionary<string, double> prices = new Dictionary<string, double>();
double p = prices["banana"];
// KeyNotFoundExceptionПравилно
if (prices.TryGetValue("banana", out double p))
{
Console.WriteLine(p);
}
else
{
Console.WriteLine("Няма такъв продукт.");
}Индексаторът за четене изисква ключът да съществува. TryGetValue проверява и чете в една стъпка — без изключение и без двойно търсене.
Add при вече съществуващ ключ
Грешно
dict.Add("ivan", 5);
dict.Add("ivan", 6); // ArgumentExceptionПравилно
dict["ivan"] = 5;
dict["ivan"] = 6; // просто заменя стойносттаAdd отказва дубликати с изключение. Когато замяната е очаквано поведение (обновяване на стойност), индексаторът е правилният инструмент.
Триене от речник по време на foreach
Грешно
foreach (var pair in counts)
{
if (pair.Value == 0)
{
counts.Remove(pair.Key); // InvalidOperationException
}
}Правилно
List<char> toRemove = new List<char>();
foreach (var pair in counts)
{
if (pair.Value == 0) toRemove.Add(pair.Key);
}
foreach (char key in toRemove)
{
counts.Remove(key);
}Както при списъците: колекция не се променя по време на обхождане. Съберете ключовете за триене в отделен списък и ги премахнете след това.
Задачи за самостоятелна работа
Опитай първо сам — подсказката е там само ако наистина закъсаш.
Задача 1: Брояч на думи
Потребителят въвежда изречение. Пребройте колко пъти се среща всяка дума и отпечатайте резултата във формат "дума: брой".
Задача 2: Телефонен указател
Направете програма-указател: потребителят добавя записи "име номер", а при въвеждане само на име програмата показва номера или "Няма такъв контакт". Командата "край" спира програмата.
Задача 3: Гласуване
Потребителите въвеждат име на кандидат (по един на ред), а командата "край" спира въвеждането. Отпечатайте колко гласа е получил всеки кандидат и обявете победителя.
Мини-тест за проверка
Отговори си наум (или на глас!) и чак тогава разкрий отговора.
1. Какво ще се случи при опит да прочетем несъществуващ ключ: prices["липсващ"]?
2. Каква е основната разлика между List<T> и Dictionary<TKey, TValue>?
3. Какво прави HashSet<T> при опит да се добави стойност, която вече съществува?
4. Каква е разликата между dict[key] = value и dict.Add(key, value)?
Речник на термините
Виж пълния речник на курсаDictionary<string, int>.pair.Key и pair.Value.true/false и подава стойността през out — без изключения.Add тихо се игнорират.Провери знанията
Бърз тест с избор от отговори върху термините на модула — с точки и моментална обратна връзка.
Провери знанията си
7 въпроса върху термините от модула. Показваме определение — ти избираш правилния термин.
Препоръки за този модул
- Мислете за речника като за истински речник: думата е ключът, дефиницията е стойността — и думите не се повтарят.
- Класическа задача за упражнение: преброяване на срещанията на думи в текст —
Dictionary<string, int>+Splitот Модул 9. - Когато се хванете да търсите в
Listс цикъл по "име", спрете и помислете: нямаше ли да е по-добреDictionary? - Нарочно прочетете несъществуващ ключ, за да видите
KeyNotFoundExceptionна живо — после го оправете сTryGetValue.