Денис Суворов 14 августа 2022

👨‍🎓️ Учебник по C#: работа с коллекциями Dictionary<K, V>

Разбор и примеры работы методов коллекции Dictionary<K, V>: манипуляции со значениями, перебор словаря, конструкторы, реализации интерфейса и методы расширения.
👨‍🎓️ Учебник по C#: работа с коллекциями Dictionary<K, V>

Коллекция Dictionary<K, V>

Dictionary<K, V> — класс пространства имён System.Collections.Generic, который представляет из себя коллекцию ключей и значений, также называемый Словарем. Коллекция типизируется двумя типами: K (key) — тип ключа и V (value) — тип значения. Коллекция позволяет получать значения со скоростью близкой к O(1). Скорость зависит от качества алгоритма хеширования типа, заданного для ключа.

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

Рассмотрим способы создания и инициализации класса Dictionary<K, V>.

Например, пустой словарь:

        Dictionary<string, string> dict = new Dictionary<string, string>();

    

В примере выше тип ключа и значения указан string (строки).

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

        Dictionary<string, string> dict = new Dictionary<string, string>
{
    { "Hello", "Привет" },
    { "How are you?", "Как дела?" },
    { "Bye", "Пока" }
};

    

Каждое новое значение берётся в фигурные скобки: первое значение — ключ, второе значение — значение, которое будет доступно по ключу.

Рассмотрим ещё один способ наполнения словаря:

        Dictionary<string, string> dict = new Dictionary<string, string>
{
    ["Hello"]= "Привет",
    ["How are you?"] = "Как дела?",
    ["Bye"] = "Пока",
};

    

KeyValuePair

По сути, словарь — это коллекция, т. е. набор элементов, тип элементов — KeyValuePair<TKey, TValue>, где TKey — ключ, а TValue — значение. Данная структура предоставляет свойства Key и Value, с помощью которых можно получить ключ и значение. Один из конструкторов Dictionary<TKey, TValue> принимает на входе список элементов типа KeyValuePair<TKey, TValue>. Рассмотрим пример:

        var hello = new KeyValuePair<string, string>("Hello", "Привет");
var listForDict = new List<KeyValuePair<string, string>>() { hello };
Dictionary<string, string> dict = new Dictionary<string, string>(listForDict);

    

В примере выше Hello — ключ, Привет — значение, которые мы передаём в конструктор KeyValuePair<string, string>, который, в свою очередь, передает в список **listForDict**при инициализации в фигурных скобках и далее передает в конструктор словаря dict.

Также мы можем совместить два способа инициализации:

        var hello = new KeyValuePair<string, string>("Sorry", "Извиняюсь");
var listForDict = new List<KeyValuePair<string, string>>() { hello };
Dictionary<string, string> dict = new Dictionary<string, string>(listForDict)
{
    ["Hello"]= "Привет",
    ["How are you?"] = "Как дела?",
    ["Bye"] = "Пока",
};


    
👨‍🎓️ Учебник по C#: работа с коллекциями Dictionary<K, V>
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека шарписта»

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

Рассмотрим пример из предыдущего раздела и убедимся с помощью цикла foreach, что в словаре действительно четыре элемента:

        //Инициализируем словарь
var hello = new KeyValuePair<string, string>("Sorry", "Извиняюсь");
var listForDict = new List<KeyValuePair<string, string>>() { hello };
Dictionary<string, string> dict = new Dictionary<string, string>(listForDict)
{
    ["Hello"]= "Привет",
    ["How are you?"] = "Как дела?",
    ["Bye"] = "Пока",
};
//Переберём значения словаря и выведем их
foreach (var kvp in dict)
    Console.WriteLine($"Key:[{kvp.Key}] Value:[{kvp.Value}]");

    
👨‍🎓️ Учебник по C#: работа с коллекциями Dictionary<K, V>

Также можно перебрать словарь классическим циклом for:

        for (int i = 0; i < dict.Count; i++)
    Console.WriteLine($"Key:[{dict.ElementAt(i).Key}] Value:[{dict.ElementAt(i).Value}]");

    

При попытке обратиться к элементу словаря с помощью [], мы получим ошибку (для текущего словаря). Если ключи — тип int, то можно получить исключение KeyNotFoundException, если бы в словаре не было соответствующего ключа.

Получение элементов

Для получения значения по ключу необходимо использовать квадратные скобки:

        словарь[Ключ элемента]

    

Рассмотрим пример работы с элементами словаря:

        Dictionary<string, string> dict = new Dictionary<string, string>
{
    ["Hello"]= "Привет",
    ["How are you?"] = "Как дела?",
    ["Bye"] = "Пока",
};
//Получим элемент по ключу
Console.WriteLine(dict["Hello"]);
//Изменим значение для ключа
dict["Hello"] = "Здравствуйте";
Console.WriteLine(dict["Hello"]);
//Добавим новое значение
dict["I am fine"] = "Я в порядке";
Console.WriteLine(dict["I am fine"]);

    
👨‍🎓️ Учебник по C#: работа с коллекциями Dictionary<K, V>

Как можно заметить в примере выше, значение по ключу можно получить, заменить и даже создать новое.

Методы и свойства Dictionary

Методы Dictionary

Добавление:

  • void Add(TKey key, TValue value) — Добавляет элемент в коллекцию с ключом key и значением value
  • bool TryAdd(TKey key, TValue value) — Метод пытается добавить новый элемент в коллекцию. Если ключ в словаре не найден, то метод ничего не сделает и вернёт false.

Удаление:

  • bool Remove(TKey key) — Удаляет элемент по ключу, при успехе возвращает — true
  • bool Remove(TKey key, out TValue value) — Аналогично примеру выше, но ещё помещает значение удалённого элемента в выходной параметр value.
  • void Clear() — Очищает словарь.

Получение:

  • bool TryGetValue(TKey key, out TValue value) — Пытается получить значение по ключу, при успехе возвращает true и записывает полученное значение в переменную value

Прочее:

  • bool ContainsKey(TKey key) — Проверяет наличие ключа в словаре.
  • bool ContainsValue(TValue value) — Проверяет наличие значения в словаре.
  • int EnsureCapacity(int capacity) — Обеспечивает возможность хранения указанного количества записей в словаре без дальнейшего увеличения его резервного хранилища. Возвращает текущее количество элементов в словаре.
  • void TrimExcess() — Устанавливает ёмкость словаря такой, какой бы она была, если словарь был изначально инициализирован со всеми записями.
  • void TrimExcess(int capacity) — Устанавливает ёмкость словаря такой, чтобы в нём помещалось указанное количество записей без дальнейшего увеличения его резервного хранилища. Если capacity меньше текущей ёмкости словаря, то генерирует исключение ArgumentOutOfRangeException.

Свойства Dictionary

  • int Count { get; } — Возвращает число элементов в словаре.
  • IEqualityComparer<TKey> Comparer { get; } — Возвращает интерфейс IEqualityComparer<T>, используемый для установления равенства ключей словаря.
  • Dictionary<TKey, TValue>.KeyCollection Keys { get; } — Коллекция ключей.
  • Dictionary<TKey, TValue>.ValueCollection Values { get; } — Коллекция значений.
  • TValue this[TKey key] — Индексатор возвращает значение по заданному ключу.

Конструкторы

Рассмотрим доступные конструкторы:

  • public Dictionary(int capacity, IEqualityComparer<TKey>? comparer) — Основной конструктор, задающий размер текущего словаря и задающий объект, реализующий интерфейс IEqualityComparer, используемый для сравнения ключей. Входная переменная capacity, которую мы передаём, на самом деле не является реальным размером словаря. Начальный размер словаря выбирается из набора простых чисел (до 7199369) и выставляется равным или больше заданного числа, если же число элементов больше, чем максимальное число из набора, то дальше поиск размера идёт перебором с проверкой на простое значение до максимального значения int(0x7fffffff — 2147483647). Инициализируются массивы выбранного ранее размера под наши ключи и значения и назначается comparer.
  • public Dictionary() — Обычный конструктор без параметров, создаёт словарь, а при вызове вызывает другой конструктор с параметрами this(0, null).
  • public Dictionary(int capacity) — Аналогично конструкторам выше вызывает основной конструктор с параметрами this(capacity, null).
  • public Dictionary(IEqualityComparer<TKey>? comparer) — Аналогично конструктору выше вызывает основной конструктор с параметрами this(0, comparer).
  • public Dictionary(IDictionary<TKey, TValue> dictionary) — конструктор, принимающий один параметр: объект, реализующий интерфейс IDictionary, вызывающий другой конструктор this(dictionary, null), который, вызывает конструктор с передачей количества элементов и null для comparer. Также сохраняет все элементы из переданного объекта с помощью метода Add.
  • public Dictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey>? comparer) — вызывает основной конструктор с передачей количества записей и comparerthis((collection as ICollection<KeyValuePair<TKey, TValue>>)?.Count ?? 0, comparer), проверяет коллекцию на null. Если null, то выбрасывает исключение ArgumentNullException и заносит все значения с помощью метода AddRange(collection).
  • public Dictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey>? comparer) — вызывает основной конструктор с параметрами this(dictionary != null ? dictionary.Count : 0, comparer) и проверяет словарь на null, если null, то выбрасывает исключение ArgumentNullException и заносит все значения с помощью метода AddRange(dictionary).
  • public Dictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection) — вызывает другой конструктор с параметрами this(collection, null).

Явные реализации интерфейса

  • ICollection.CopyTo(Array, Int32) — Копирует элементы коллекции ICollection<T> в массив, начиная с указанного индекса массива.
  • ICollection.IsSynchronized — Получает значение, определяющее, является ли доступ к коллекции ICollection синхронизированным (потокобезопасным).
  • ICollection.SyncRoot — Получает объект, с помощью которого можно синхронизировать доступ к коллекции ICollection.
  • IDictionary.Add(Object, Object) — Добавляет указанные ключ и значение в словарь.
  • IDictionary.Contains(Object) — Определяет, содержится ли элемент с указанным ключом в IDictionary.
  • IDictionary.Keys — Возвращает интерфейс ICollection, содержащий ключи IDictionary.
  • IDictionary.Values — Возвращает интерфейс ICollection, содержащий значения из IDictionary.

Методы расширения

Рассмотрим методы расширения, которые имеют отношение непосредственно к словарю. В реальности их гораздо больше, а, так как словарь реализовывает такие интерфейсы как ICollection, IEnumerable, IDeserializationCallback их ещё больше (больше информации).

  • Remove<TKey,TValue>(IDictionary<TKey,TValue>, TKey, TValue) — Пытается удалить значение с указанным key из dictionary.
  • GetValueOrDefault<TKey,TValue>(IReadOnlyDictionary<TKey,TValue>, TKey) — Пытается получить значение, связанное с указанным key в dictionary.
  • TryAdd<TKey,TValue>(IDictionary<TKey,TValue>, TKey, TValue) — Пытается добавить указанные элементы key и value в dictionary.

Примеры

Самый наглядный вариант реализации словаря — англо-русский словарь, но мы рассмотрим менее классические примеры:

        // Допустим используем словарь для хранения номеров, но помним, 
// что ключ должен быть уникальным, иначе мы получим ошибку уникальных
// значений.
Dictionary<string, long> phones = new Dictionary<string, long> {
	{ "Вася", 81112223344 },
  { "Петя", 82224445566 },
  { "Стёпа", 83335556677 }
};

// Допустим у нас есть метод отправки смс по номеру
void SendSMS(long phone, string msg)
{
	// todo
}

// Допустим мы хотим стёпе отправить смс, получаем его номер по имени
SendSMS(phones["Стёпа"], "Привет, как дела?");
// Допустим мы получили ответное смс
var sms = GetSMS(phones["Стёпа"], "Привет, всё ок, а у тебя?");
// Воспользуемся методом, который позволит определить от кого смс и что пишет
Console.WriteLine($"{WhoSendSMS(sms.phone)} - {sms.msg}" );
Console.WriteLine();
sms = GetSMS(81111111111, "Здравствуйте вам одобрена...");
// Получим смс от кого-то ещё)
Console.WriteLine($"{WhoSendSMS(sms.phone)} - {sms.msg}");
Console.WriteLine();

// Допустим нам не понравился этот номер и мы не хотим больше от него получать смс
// создадим ещё один словарь, с помощью него будем блокировать номера телефонов
// и добавим туда наш новый номер с флагом false
Dictionary<long, bool> accessNumbers = new Dictionary<long, bool>
{
	{ 81111111111, false }
};

// Также добавим туда все номера из нашей записной книжки с флагом true
foreach (var phone in phones)
	accessNumbers[phone.Value] = true;
// Попробуем снова получить СМС
var newSms = GetSMSWithAccess(81111111111, "Здравствуйте вам одобрена...");
// Поскольку теперь нам может прийти null нужно проверить есть ли действительно 
// смс
if (newSms != null)
	Console.WriteLine($"{WhoSendSMS(newSms.phone)} - {newSms.msg}");
else
	Console.WriteLine($"Пришла СМС от заблокированного номера.");
Console.WriteLine();
// Ну и проверим сообщение от Васи например
newSms = GetSMSWithAccess(phones["Вася"], "Привет");
if (newSms != null)
	Console.WriteLine($"{WhoSendSMS(newSms.phone)} - {newSms.msg}");
Console.WriteLine();
// Допустим хотим посмотреть все номера в нашей записной книжке
Console.WriteLine($"Всего номеров в записной книжке: {phones.Count}");
foreach (var phone in phones)
	Console.WriteLine($"{phone.Key} - {phone.Value}");
// Допустим мы видим, что у нас записан Петя, с которым давно
// не общались и хотим удалить его номер
Console.WriteLine();
phones.Remove("Петя");
Console.WriteLine($"Всего номеров в записной книжке: {phones.Count}");
// Проверим снова телефонную книгу
foreach (var phone in phones)
	Console.WriteLine($"{phone.Key} - {phone.Value}");
Console.WriteLine();
// Как насчёт информации о доступных номерах?
Console.WriteLine($"Всего записей в коллекции с доступными номерами: {accessNumbers.Count}");
foreach (var phone in accessNumbers)
    Console.WriteLine($"От номера {phone.Key} получать смс {(phone.Value ? "можно" : "нельзя")}.");

// Допустим есть метод, который возвращает нам текст сообщения и номер телефона
SMSInfo GetSMS(long phone, string msg)
{
	return new SMSInfo { phone = phone, msg = msg };
}
SMSInfo? GetSMSWithAccess(long phone, string msg)
{
	if (accessNumbers[phone])
		return new SMSInfo { phone = phone, msg = msg };
	return null;
}

// А теперь попробуем найти обратно, кому же принадлежит номер
string WhoSendSMS(long phone)
{
	// Переберём все значения в словаре и найдём имя отправителя
	foreach (var phoneInfo in phones)
	{
		if (phoneInfo.Value == phone)
			return phoneInfo.Key;
	}
	return "Номер отсутствует в записной книжке!";
}

// Для следующего примера создадим специальный класс
class SMSInfo
{
	public long phone { get; set; }
	public string msg { get; set; }
}

    

Комментарии

Как было описано выше, скорость доступа к значениям словаря близка к O(1). Скорость зависит от качества алгоритма хеширования указанного типа TKey. Значения ключей должны быть уникальными. Dictionary<TKey,TValue> требует реализации равенства, чтобы определить, равны ли ключи. Можно указать реализацию универсального интерфейса с помощью конструктора, принимающего **IEqualityComparer<T>** **comparer** параметр.

Например, можно использовать компараторы строк без учёта регистра, предоставляемые классом StringComparer, для создания словарей с нечувствительными к регистру строковыми ключами.

Ёмкость a Dictionary<TKey,TValue> — это количество элементов, которые Dictionary<TKey,TValue> могут храниться. При добавлении элементов к объекту Dictionary<TKey,TValue> ёмкость автоматически увеличивается, так как требуется перераспределить внутренний массив.

Итог

Словарь используется для быстрого доступа к значениям по ключу и содержит значения, которые можно получить по уникальным ключам. Имеет смысл использовать словарь, если есть большая коллекция данных, в которой часто производится поиск значений по какому-то ключу.

***

Материалы по теме

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ