Представьте: вы пишете бэкенд для интернет-магазина. Нужно хранить статусы заказов — по каждому 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 при одновременном доступе из нескольких потоков может повредить данные.

Обсуждение урока

0
Комментарии видны всем. Чтобы участвовать в обсуждении, войдите или зарегистрируйтесь.
Модерация сообщества

Пожаловаться на комментарий

Расскажите модераторам, что именно требует внимания.