Представьте: вы пишете бэкенд для интернет-магазина. Нужно хранить статусы заказов — по каждому orderId знать, на каком он этапе: «В обработке», «Отправлен», «Доставлен». Заказов тысячи, и статус нужно проверять мгновенно.
Задача: Создайте словарь Dictionary<string, string> с именем orderStatus, добавьте три заказа с идентификаторами "ORD-001", "ORD-002", "ORD-003" и статусами на ваш выбор. Затем выведите статус заказа "ORD-002" в консоль.
Подсказка: Синтаксис создания — new Dictionary<string, string>(). Добавить элемент можно через dict["ключ"] = "значение" или через dict.Add("ключ", "значение").
В прошлых уроках вы разобрались, как устроены хеш-функции и что происходит при коллизиях. Теперь — инструменты, которыми вы будете пользоваться каждый день. Dictionary<TKey, TValue> в C# — это готовая хеш-таблица с открытой адресацией под капотом. Вам не нужно думать о размере массива, коэффициенте загрузки или цепочках — всё уже сделано за вас.
Начнём с самого частого сценария. В системе управления доступом нужно быстро узнать роль пользователя по его username. Массив или список здесь не подойдут — придётся перебирать всех пользователей линейно. Словарь решает это за одно обращение:
var userRole = new Dictionary<string, string>();
// Добавляем записи через индексатор
userRole["alice"] = "admin";
userRole["bob"] = "editor";
userRole["carol"] = "viewer";
// Или через метод Add — он бросит исключение, если ключ уже есть
userRole.Add("dave", "editor");
Console.WriteLine(userRole["alice"]); // admin
Console.WriteLine(userRole.Count); // 4
Есть два способа добавить запись: индексатор [] и метод Add. Индексатор перезапишет значение, если ключ уже существует. Add — бросит ArgumentException. Выбирайте Add, когда дубликат — это ошибка, и индексатор — когда хотите «создать или обновить».
Безопасный способ читать из словаря — метод TryGetValue. Он возвращает false и не бросает исключений, если ключ не найден:
string username = "eve";
if (userRole.TryGetValue(username, out string role))
{
Console.WriteLine($"{username} имеет роль: {role}");
}
else
{
Console.WriteLine($"Пользователь {username} не найден");
}
Переменная role объявляется прямо в вызове через out string role — это современный синтаксис C# 7+. Если ключ нашёлся, role получает значение, и метод возвращает true. Нет ключа — возвращает false, а role остаётся null.
Есть ещё ContainsKey — он только проверяет существование ключа без возврата значения. Используйте его, когда само значение вам не нужно, только факт наличия записи.
Итерация по словарю идёт через foreach. Каждый элемент — это структура KeyValuePair<TKey, TValue> с полями Key и Value:
foreach (KeyValuePair<string, string> entry in userRole)
{
Console.WriteLine($"{entry.Key}: {entry.Value}");
}
// Современный вариант — деструктуризация через var
foreach (var (user, r) in userRole)
{
Console.WriteLine($"{user}: {r}");
}
// Если нужны только ключи или только значения:
foreach (string key in userRole.Keys) { /* ... */ }
foreach (string val in userRole.Values) { /* ... */ }
Порядок итерации не определён и может меняться. Если вам важен порядок — это сигнал либо сортировать на лету, либо использовать SortedDictionary, о котором поговорим чуть позже.
Удалить запись — метод Remove. Он возвращает bool: true если ключ был и удалился, false если ключа не было. Это удобно: не нужно сначала проверять ContainsKey.
bool removed = userRole.Remove("bob");
Console.WriteLine(removed ? "Удалён" : "Не найден"); // Удалён
Теперь про HashSet<T>. Это хеш-таблица без значений — только ключи. Используется там, где важна уникальность или нужно быстро проверить принадлежность элемента к множеству.
Типичный кейс — у вас список productCode-ов из разных источников, и нужно убрать дубликаты. Или нужно быстро проверить, входит ли товар в «запрещённый» список:
// Дедупликация
var rawCodes = new List<string> { "SKU-101", "SKU-202", "SKU-101", "SKU-303", "SKU-202" };
var uniqueCodes = new HashSet<string>(rawCodes);
Console.WriteLine(uniqueCodes.Count); // 3 — дубликаты удалены
// Проверка принадлежности
var blockedProducts = new HashSet<string> { "SKU-999", "SKU-888", "SKU-777" };
string incoming = "SKU-888";
if (blockedProducts.Contains(incoming))
{
Console.WriteLine($"Товар {incoming} заблокирован");
}
HashSet умеет выполнять операции над множествами — те самые из теории вероятностей:
var setA = new HashSet<int> { 1, 2, 3, 4, 5 };
var setB = new HashSet<int> { 3, 4, 5, 6, 7 };
// Объединение: setA теперь { 1, 2, 3, 4, 5, 6, 7 }
setA.UnionWith(setB);
// Пересечение (сбросим setA)
setA = new HashSet<int> { 1, 2, 3, 4, 5 };
setA.IntersectWith(setB); // setA: { 3, 4, 5 }
// Разность
setA = new HashSet<int> { 1, 2, 3, 4, 5 };
setA.ExceptWith(setB); // setA: { 1, 2 } — только то, чего нет в setB
Обратите внимание: все три метода модифицируют вызывающий объект (setA). Если нужно сохранить оригинал — сначала скопируйте: var result = new HashSet<int>(setA).
Реальный пример из жизни: найти сотрудников employeeId, которые прошли обучение А, но ещё не прошли обучение Б:
var completedTrainingA = new HashSet<int> { 101, 102, 103, 104, 105 };
var completedTrainingB = new HashSet<int> { 102, 104, 106 };
var needsTrainingB = new HashSet<int>(completedTrainingA);
needsTrainingB.ExceptWith(completedTrainingB);
// needsTrainingB: { 101, 103, 105 }
foreach (int empId in needsTrainingB)
Console.WriteLine($"Сотрудник {empId} ещё не прошёл обучение Б");
Теперь о выборе между Dictionary<K,V> и SortedDictionary<K,V>. Это часто спрашивают на собеседованиях, и ответ простой, если понимать внутреннее устройство.
// Dictionary — порядок не определён
var dict = new Dictionary<string, int>
{
["gamma"] = 3,
["alpha"] = 1,
["beta"] = 2
};
// При итерации порядок непредсказуем
// SortedDictionary — всегда по алфавиту
var sorted = new SortedDictionary<string, int>
{
["gamma"] = 3,
["alpha"] = 1,
["beta"] = 2
};
foreach (var pair in sorted)
Console.WriteLine(pair.Key); // alpha, beta, gamma
Когда что выбирать? Dictionary — когда порядок не важен и важна скорость: O(1) против O(log n). SortedDictionary — когда нужно часто итерировать в порядке ключей, или выполнять операции типа «найди все записи с ключом от А до Б». Если данные меняются редко, а сортировка нужна только при выводе — лучше взять Dictionary и отсортировать ключи через LINQ один раз.
Теперь два паттерна, которые встречаются в реальном коде постоянно.
Подсчёт частот. Нужно узнать, сколько раз каждое значение встречается в наборе данных. Классический пример — статистика по категориям заказов:
var orders = new List<string>
{
"Electronics", "Clothing", "Electronics", "Books",
"Clothing", "Electronics", "Books", "Books"
};
var frequency = new Dictionary<string, int>();
foreach (string category in orders)
{
if (frequency.TryGetValue(category, out int count))
frequency[category] = count + 1;
else
frequency[category] = 1;
}
// Вывод: Electronics=3, Clothing=2, Books=3
foreach (var (cat, cnt) in frequency)
Console.WriteLine($"{cat}: {cnt}");
Мемоизация. Если функция с одними и теми же аргументами всегда возвращает одинаковый результат, зачем считать дважды? Сохраняем результат в словарь и при следующем вызове просто отдаём кешированное значение:
var cache = new Dictionary<int, long>();
long Fibonacci(int n)
{
if (n <= 1) return n;
if (cache.TryGetValue(n, out long cached)) return cached;
long result = Fibonacci(n - 1) + Fibonacci(n - 2);
cache[n] = result;
return result;
}
Console.WriteLine(Fibonacci(50)); // Быстро, без повторных вычислений
Без кеша Fibonacci(50) потребовало бы триллионы рекурсивных вызовов. С Dictionary каждое значение считается ровно один раз.
Dictionary<TKey, TValue> — хеш-таблица в C#. Хранит пары «ключ → значение», поиск и вставка за O(1) в среднем.
Индексатор [] при чтении бросает KeyNotFoundException для отсутствующего ключа. Используйте TryGetValue для безопасного чтения — он возвращает false вместо исключения.
HashSet<T> — хеш-таблица только с ключами. Гарантирует уникальность, операции Add / Contains / Remove за O(1). Поддерживает операции над множествами: UnionWith, IntersectWith, ExceptWith.
SortedDictionary<K,V> основан на красно-чёрном дереве, хранит ключи в отсортированном порядке, операции за O(log n). Выбирайте его только когда порядок ключей при итерации действительно нужен.
Подсчёт частот — паттерн с TryGetValue: если ключ есть — увеличить счётчик, нет — создать с 1.
Мемоизация — кеширование результатов функции в Dictionary, чтобы не пересчитывать одно и то же дважды.
ConcurrentDictionary — потокобезопасная версия словаря для многопоточных сценариев. Обычный Dictionary при одновременном доступе из нескольких потоков может повредить данные.