Dictionary<TKey, TValue> хранит значения под уникальными ключами и находит значение по ключу примерно за постоянное время, сколько бы ни было элементов. Это аналог хеш-таблицы в C#: телефонная книга от имени к номеру, кэш от идентификатора к записи, счётчик для каждого слова.
Создание словаря и чтение значений
Вывод:
12
2.50
2
True
False
Обе формы инициализатора делают одно и то же. Форма ["key"] = value (C# 6) использует индексатор, поэтому повторный ключ перезаписывается; форма { key, value } вызывает Add, поэтому повторный ключ выбрасывает исключение при выполнении строки.
ContainsKey это поиск по хешу, и он быстрый. ContainsValue приходится просматривать все элементы, потому что значения не индексируются.
Add, индексатор и TryAdd
Есть три способа добавить элемент, и они различаются только тем, что происходит, когда ключ уже существует:
Вывод:
26
Caught ArgumentException
True
False
31
То, что Add выбрасывает исключение на дубликате, это полезная особенность: так проявляются данные, которые должны были быть уникальными, но не оказались. Используйте индексатор, когда вы имеете в виду «вставить или обновить», и TryAdd (.NET Core 2.0 и новее), когда должно победить первое значение.
Ключи не могут быть null. Add(null, ...) или dict[null] выбрасывает ArgumentNullException. Значения могут быть null, если это позволяет тип значения.
KeyNotFoundException и TryGetValue
Чтение отсутствующего ключа через индексатор выбрасывает KeyNotFoundException. Это самая частая ошибка со словарём, и исправление почти всегда TryGetValue.
Вывод:
Caught KeyNotFoundException
Found ana@example.com
Missing, value is null: True
no email
TryGetValue делает один поиск по хешу и сообщает об успехе через bool. Шаблон if (dict.ContainsKey(k)) { var v = dict[k]; } работает, но ищет ключ дважды. Когда ключа нет, переменная out получает значение по умолчанию для типа (null, 0, false).
В .NET Core 2.0 и новее есть также GetValueOrDefault(key, fallback), который возвращает запасное значение, когда ключа нет: emails.GetValueOrDefault(103, "no email").
Обновление и удаление элементов
Вывод:
2
True
False
1
0
cart["milk"] += 1 выбрасывает KeyNotFoundException, если milk ещё нет в словаре, потому что сначала выполняется чтение, а затем запись. Remove для отсутствующего ключа возвращает false, а не выбрасывает исключение, поэтому сначала проверять ContainsKey не нужно.
Перебор: KeyValuePair, Keys и Values
foreach по словарю выдаёт элементы KeyValuePair<TKey, TValue>, у каждого из которых есть Key и Value.
Вывод:
Ana: 88
Ben: 72
Chloe: 95
Ana Ben Chloe
Total 255
77
pair.Value доступно только для чтения, поэтому значения обновляют через индексатор. Последний цикл перебирает копию ключей типа List<string>, что всегда безопасно; перебор scores.Keys напрямую с перезаписью существующих значений разрешён в .NET Core 3.0 и новее, но выбрасывает InvalidOperationException в .NET Framework.
Добавление нового ключа внутри foreach по тому же словарю выбрасывает InvalidOperationException в любой версии. Удаление во время перебора выбрасывает исключение в .NET Framework и разрешено начиная с .NET Core 3.0. Код, который должен работать везде, сначала собирает ключи для удаления, а удаляет их после цикла.
В C# 7 и .NET Core 2.0 или новее KeyValuePair можно деконструировать в заголовке цикла:
foreach (var (name, score) in scores)
{
Console.WriteLine($"{name}: {score}");
}
Подсчёт с помощью словаря
Подсчёт вхождений это хрестоматийное применение. Прочитайте текущее значение через TryGetValue (отсутствующий ключ даёт 0), затем запишите обратно.
Вывод:
the 3
cat 1
and 2
dog 1
bird 1
По той же схеме элементы группируют: Dictionary<string, List<Order>>, где вы получаете список через TryGetValue, создаёте и сохраняете его при отсутствии, а затем вызываете у него Add. Для разового подсчёта и группировки LINQ делает это одним выражением: words.GroupBy(w => w).ToDictionary(g => g.Key, g => g.Count()). См. LINQ.
Ключи без учёта регистра через компаратор
Строковые ключи по умолчанию сравниваются точно: "Apple" и "apple" это два ключа. Чтобы это изменить, передайте в конструктор IEqualityComparer<string>.
Вывод:
False
text/html
1
StringComparer.OrdinalIgnoreCase это правильный выбор для идентификаторов вроде HTTP-заголовков, расширений файлов и имён пользователей. Вызов .ToLower() для каждого ключа перед сохранением тоже работает, но его легко забыть в каком-нибудь месте.
Для ключей вашего собственного класса словарь вызывает у ключа GetHashCode и Equals. Класс, который их не переопределяет, сравнивается по ссылке, поэтому два отдельных объекта с одинаковыми полями это разные ключи. Как написать эту пару методов, показано на странице HashSet.
Порядок, сортировка и SortedDictionary
Dictionary ничего не обещает о порядке перебора. На практике словарь, в который только добавляли элементы, перебирается в порядке вставки, но после Remove последующий Add может занять освободившуюся ячейку и оказаться раньше. Код никогда не должен на это полагаться.
Когда нужен порядок, сортируйте в месте использования или используйте отсортированную коллекцию:
Вывод:
Cairo 210
Lima 340
Oslo 520
By value, highest first:
Oslo 520
Lima 340
Cairo 210
Berlin, Cairo, Lima, Oslo
SortedDictionary<TKey, TValue> всегда держит ключи отсортированными (это сбалансированное дерево), поэтому поиск и вставка занимают O(log n) вместо O(1). Используйте его, когда часто перебираете в порядке ключей; сортируйте обычный словарь через LINQ, когда порядок нужен один раз. Третий вариант это SortedList<TKey, TValue>, который занимает меньше памяти, но медленно вставляет, когда становится большим.
Краткая справка
| Задача | Код |
|---|---|
| Создать | new Dictionary<string, int>() |
| Вставить или перезаписать | d[k] = v |
| Вставить, исключение при дубликате | d.Add(k, v) |
| Вставить, только если ключ новый | d.TryAdd(k, v) |
| Прочитать, исключение при отсутствии | d[k] |
| Безопасно прочитать | d.TryGetValue(k, out var v) |
| Ключ существует | d.ContainsKey(k) |
| Удалить | d.Remove(k) (возвращает bool) |
| Размер | d.Count |
| Ключи, значения | d.Keys, d.Values |
| Отсортировать по ключу | d.OrderBy(p => p.Key) или SortedDictionary |
| Без учёта регистра | new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase) |
Частые ошибки
- Чтение отсутствующего ключа через
d[k]. ВыбрасываетKeyNotFoundException; используйтеTryGetValue. - Вызов
Addдля ключа, который может существовать. ВыбрасываетArgumentException; используйте индексатор илиTryAdd. - Добавление ключей внутри
foreachпо словарю. ВыбрасываетInvalidOperationException; собирайте изменения и применяйте их после цикла. - Опора на порядок перебора. Сортируйте или используйте
SortedDictionary. - Изменение полей объекта-ключа после вставки. Его хеш-код меняется, и элемент больше нельзя найти.
ContainsKey, а затем индексатор. Два поиска;TryGetValueделает один.
Часто задаваемые вопросы
Чем Dictionary.Add отличается от индексатора в C#?
dict.Add(key, value) вставляет новый элемент и выбрасывает ArgumentException, если ключ уже существует. dict[key] = value вставляет элемент, если ключ новый, и перезаписывает значение, если он существует, и никогда не выбрасывает исключение из-за дубликата. TryAdd(key, value) вставляет, только когда ключ новый, и иначе возвращает false.
Как работает TryGetValue в C#?
dict.TryGetValue(key, out var value) возвращает true и задаёт value, когда ключ существует, и возвращает false, задавая value значением по умолчанию для его типа, когда ключа нет. Он делает один поиск, тогда как ContainsKey с последующим dict[key] делают два, и никогда не выбрасывает KeyNotFoundException.
Как перебрать Dictionary в C#?
foreach (KeyValuePair<string, int> pair in dict) даёт каждый элемент с pair.Key и pair.Value. Чтобы перебрать только ключи или только значения, используйте dict.Keys или dict.Values. Не добавляйте ключи в словарь внутри такого цикла: это выбрасывает InvalidOperationException.
Упорядочен ли Dictionary в C#?
Никакой порядок не гарантируется. Словарь, в который только добавляли элементы, обычно перебирается в порядке вставки, но после Remove новые элементы могут занять освободившуюся ячейку, и порядок меняется. Когда нужен порядок, сортируйте: dict.OrderBy(p => p.Key) или используйте SortedDictionary<TKey, TValue>, который всегда перебирается по ключу.
Как сделать ключи Dictionary нечувствительными к регистру?
Передайте компаратор в конструктор: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). Тогда "Apple" и "apple" будут одним ключом для поиска, Add и ContainsKey. Компаратор фиксируется при создании словаря.
Что такое KeyValuePair в C#?
KeyValuePair<TKey, TValue> это структура, которую словарь выдаёт для каждого элемента при переборе. У неё свойства Key и Value только для чтения, поэтому изменить элемент через неё нельзя; пишите dict[pair.Key] = newValue (после цикла или перебирая копию ключей).