Menu

Dictionary в C#: Add, TryGetValue, перебор и сортировка

Dictionary<TKey, TValue> сопоставляет ключам значения с быстрым поиском. Как добавлять и обновлять элементы, безопасно читать через TryGetValue, перебирать элементы KeyValuePair, считать вхождения, игнорировать регистр ключей и хранить ключи отсортированными через SortedDictionary.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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 (после цикла или перебирая копию ключей).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ