$ sudo teach IT
Модуль 5 · Коллекции

Урок 5.3 — Словарь (Dictionary)

Пары ключ-значение для быстрого доступа к данным по уникальному ключу.

Представь себе телефонную книгу. Ты не ищешь номер по порядковому номеру записи — ты ищешь по имени контакта. Имя — это ключ, номер телефона — значение. Dictionary<TKey, TValue> в C# работает точно так же: ты передаёшь ключ и мгновенно получаешь значение. Внутри словарь использует хэш-таблицу, что обеспечивает очень быстрый доступ — O(1) в среднем случае.

🏗️

Создание словаря

Словарь требует указать два типа: TKey (тип ключа) и TValue (тип значения). Ключи должны быть уникальными — ты не можешь добавить два элемента с одинаковым ключом.

C# · Создание словаря
using System.Collections.Generic;

// Пустой словарь (ключ — string, значение — int)
Dictionary<string, int> ages = new Dictionary<string, int>();

// Словарь с начальной ёмкостью
Dictionary<string, string> capitals = new Dictionary<string, string>(50);

// Инициализация через коллекцию пар
Dictionary<string, double> products = new Dictionary<string, double>
{
    { "молоко", 89.9 },
    { "хлеб", 45.0 },
    { "сыр", 299.5 }
};

// Инициализация через индекс (C# 6+)
Dictionary<int, string> employees = new Dictionary<int, string>
{
    [101] = "Анна Смирнова",
    [102] = "Борис Иванов",
    [103] = "Вика Петрова"
};

// Target-typed new (C# 9+)
Dictionary<string, bool> settings = new();

Что можно использовать в качестве ключа? Любой тип, который реализует GetHashCode() и Equals(). Встроенные типы (int, string, double, DateTime и т.д.) работают из коробки. Для своих классов нужно переопределять GetHashCode и Equals — это мы разберём в уроках про ООП.

💡 Самое частое использование — ключ типа string или int. Избегай double как ключа из-за погрешностей вычислений: 0.1 + 0.2 != 0.3 в двоичной арифметике.
🎯

Доступ по ключу: dict[key]

Получить значение по ключу можно через квадратные скобки, как при индексации массива, только вместо индекса — ключ:

C# · Доступ по ключу
Dictionary<string, int> ages = new Dictionary<string, int>
{
    { "Анна", 25 },
    { "Борис", 32 },
    { "Вика", 28 }
};

int borisAge = ages["Борис"];
Console.WriteLine(borisAge);            // 32

// ❌ Если ключа нет — KeyNotFoundException!
// int unknown = ages["Ольга"]; // Ошибка!

⚠️ KeyNotFoundException — ещё одна частая ошибка. Если обратиться к словарю по ключу, которого нет, программа упадёт. Всегда проверяй наличие ключа через ContainsKey() или используй TryGetValue().

Через индексатор можно также добавлять и обновлять элементы:

C# · Добавление и обновление
// Добавление через индексатор (если ключа нет)
ages["Ольга"] = 30;  // добавится новая пара

// Обновление через индексатор (если ключ есть)
ages["Анна"] = 26;   // обновится значение для ключа "Анна"

// Индексатор работает и для добавления, и для обновления:
// - Если ключа нет — добавляет
// - Если ключ есть — перезаписывает
🛡️

Безопасный доступ: TryGetValue()

TryGetValue() — это безопасный способ получить значение. Он не выбрасывает исключение, а возвращает bool — найден ключ или нет:

C# · TryGetValue
Dictionary<string, int> ages = new()
{
    { "Анна", 25 },
    { "Борис", 32 }
};

if (ages.TryGetValue("Борис", out int borisAge))
{
    Console.WriteLine($"Возраст Бориса: {borisAge}");
}
else
{
    Console.WriteLine("Борис не найден");
}

if (ages.TryGetValue("Ольга", out int olgaAge))
{
    Console.WriteLine($"Возраст Ольги: {olgaAge}");
}
else
{
    Console.WriteLine("Ольга не найдена");  // это выполнится
}

// Можно объявить out-переменную прямо в условии
if (ages.TryGetValue("Анна", out int age))
{
    Console.WriteLine($"Возраст: {age}");
}

Это гораздо лучше, чем ловить KeyNotFoundException! TryGetValue — идиоматический способ работы со словарями в C#.

📝

Add, Remove, ContainsKey

Add() — добавить с проверкой

В отличие от индексатора, Add() выбрасывает ArgumentException, если ключ уже существует:

C# · Add
Dictionary<string, int> ages = new();

ages.Add("Анна", 25);  // OK
ages.Add("Борис", 32); // OK
// ages.Add("Анна", 30);  // ❌ ArgumentException! Ключ уже есть

// Лучше сначала проверить:
if (!ages.ContainsKey("Анна"))
{
    ages.Add("Анна", 30);
}
else
{
    ages["Анна"] = 30;  // обновить существующее
}

Remove() — удалить по ключу

Удаляет пару по ключу. Возвращает true, если ключ был найден и удалён:

C# · Remove
bool removed = ages.Remove("Борис");  // true — удалён
bool removedAgain = ages.Remove("Борис");  // false — уже нет

// Remove возвращает bool, можно использовать в условиях
if (ages.Remove("Анна"))
{
    Console.WriteLine("Анна удалена из словаря");
}

ContainsKey() и ContainsValue()

Проверяют наличие ключа или значения:

C# · ContainsKey / ContainsValue
bool hasAnna = ages.ContainsKey("Анна");    // true — по ключу
bool hasAge25 = ages.ContainsValue(25);     // true — по значению (медленно!)
bool hasValue99 = ages.ContainsValue(99);   // false
💡 ContainsKey() — O(1) (быстро). ContainsValue() — O(n) (медленно, перебирает все значения). Используй ContainsValue только для маленьких словарей.
🔄

Перебор словаря

У словаря нет индекса, но его можно перебирать разными способами:

Перебор пар KeyValuePair

Самый частый способ — через foreach по KeyValuePair<TKey, TValue>:

C# · Перебор пар
Dictionary<string, int> ages = new()
{
    { "Анна", 25 },
    { "Борис", 32 },
    { "Вика", 28 }
};

foreach (KeyValuePair<string, int> kvp in ages)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value} лет");
}
// Вывод:
// Анна: 25 лет
// Борис: 32 лет
// Вика: 28 лет

// Более краткая запись с var:
foreach (var kvp in ages)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

// Ещё короче — деконструкция (C# 7+):
foreach (var (name, age) in ages)
{
    Console.WriteLine($"{name}: {age}");
}

Перебор ключей и значений отдельно

Свойства Keys и Values возвращают коллекции ключей и значений:

C# · Keys и Values
// Только ключи
Console.WriteLine("Имена:");
foreach (string name in ages.Keys)
{
    Console.WriteLine($"  {name}");
}

// Только значения
Console.WriteLine("Возраст:");
foreach (int age in ages.Values)
{
    Console.WriteLine($"  {age}");
}

// KeyValuePair — это структура (value type)
// Свойства Key и Value доступны только для чтения
🪆

Вложенные словари

Значением словаря может быть другой словарь. Это мощный приём для моделирования сложных структур данных:

C# · Вложенный словарь
// Словарь: отдел → (сотрудник → зарплата)
Dictionary<string, Dictionary<string, double>> company = new()
{
    ["IT"] = new()
    {
        { "Анна", 150000 },
        { "Борис", 180000 },
        { "Вика", 160000 }
    },
    ["HR"] = new()
    {
        { "Глеб", 95000 },
        { "Дарья", 110000 }
    },
    ["Sales"] = new()
    {
        { "Егор", 85000 },
        { "Жанна", 92000 }
    }
};

// Доступ к элементу
double annasSalary = company["IT"]["Анна"];  // 150000

// Проверка — безопасный доступ
if (company.TryGetValue("IT", out var itDept) &&
    itDept.TryGetValue("Борис", out double borisSalary))
{
    Console.WriteLine($"Зарплата Бориса: {borisSalary}");
}

// Перебор
foreach (var (department, employees) in company)
{
    Console.WriteLine($"Отдел: {department}");
    foreach (var (name, salary) in employees)
    {
        Console.WriteLine($"  {name}: {salary} руб.");
    }
}

Ещё пример — каталог товаров по категориям:

C# · Каталог товаров
Dictionary<string, Dictionary<string, double>> catalog = new()
{
    ["Электроника"] = new()
    {
        ["Наушники"] = 2990,
        ["Клавиатура"] = 1890,
        ["Мышь"] = 990
    },
    ["Продукты"] = new()
    {
        ["Молоко"] = 89.9,
        ["Хлеб"] = 45,
        ["Яйца (10 шт)"] = 120
    }
};

// Достать цену наушников:
double headphonePrice = catalog["Электроника"]["Наушники"];
📚

Другие полезные методы

Метод/Свойство
Описание
dict[key]
Получить/установить значение по ключу
TryGetValue(key, out val)
Безопасно получить значение
Add(key, value)
Добавить (исключение, если ключ есть)
Remove(key)
Удалить по ключу (возвращает bool)
ContainsKey(key)
Есть ли ключ (O(1))
ContainsValue(val)
Есть ли значение (O(n))
Clear()
Очистить словарь
Count
Количество пар
Keys
Коллекция ключей
Values
Коллекция значений
🧩

Большой пример — подсчёт слов

Классическая задача: дан текст, нужно подсчитать, сколько раз встречается каждое слово. Словарь идеально подходит для этого:

C# · Подсчёт слов
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        string text = "привет мир привет C# мир привет C# программирование";
        string[] words = text.Split(' ');
        
        Dictionary<string, int> wordCount = new Dictionary<string, int>();
        
        foreach (string word in words)
        {
            if (wordCount.ContainsKey(word))
            {
                wordCount[word]++;  // увеличиваем счётчик
            }
            else
            {
                wordCount[word] = 1;  // первое упоминание
            }
        }
        
        // Вывод результатов
        foreach (var (word, count) in wordCount)
        {
            Console.WriteLine($"Слово '{word}' встречается {count} раз(а)");
        }
        
        // Поиск самого частого слова
        string mostFrequent = "";
        int maxCount = 0;
        foreach (var (word, count) in wordCount)
        {
            if (count > maxCount)
            {
                mostFrequent = word;
                maxCount = count;
            }
        }
        Console.WriteLine($"\nСамое частое слово: '{mostFrequent}' ({maxCount} раз)");
    }
}
// Вывод:
// Слово 'привет' встречается 3 раз(а)
// Слово 'мир' встречается 2 раз(а)
// Слово 'C#' встречается 2 раз(а)
// Слово 'программирование' встречается 1 раз(а)
// Самое частое слово: 'привет' (3 раз)
📌

Что важно запомнить

1️⃣
Dictionary<TKey, TValue> — это хэш-таблица. Хранит пары ключ-значение. Доступ по ключу — O(1). Ключи должны быть уникальными.
2️⃣
dict[key] — быстро, но выбрасывает KeyNotFoundException. TryGetValue() — безопасный способ без исключений.
3️⃣
Add() — добавляет или выбрасывает исключение (если ключ есть). Индексатор dict[key] = value — добавляет или обновляет.
4️⃣
ContainsKey(key) — проверка наличия (O(1)). ContainsValue(val) — медленный перебор (O(n)).
5️⃣
Перебор: foreach (var kvp in dict) или foreach (var (key, value) in dict). Свойства Keys и Values для отдельных коллекций.
6️⃣
Вложенные словари — мощный приём: Dictionary<string, Dictionary<string, int>>. Удобно для каталогов и иерархических данных.

Тест: 5.3: Словарь (Dictionary<TKey, TValue>)

3 вопроса

Dictionary

Premium