👨🎓️ Учебник по C#: работа с коллекциями Dictionary<K, V>
Разбор и примеры работы методов коллекции 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"] = "Пока", };
Перебор словаря
Рассмотрим пример из предыдущего раздела и убедимся с помощью цикла 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}]");
Также можно перебрать словарь классическим циклом 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"]);
Как можно заметить в примере выше, значение по ключу можно получить, заменить и даже создать новое.
Методы и свойства 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)
— вызывает основной конструктор с передачей количества записей иcomparer
—this((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>
ёмкость автоматически увеличивается, так как требуется перераспределить внутренний массив.
Итог
Словарь используется для быстрого доступа к значениям по ключу и содержит значения, которые можно получить по уникальным ключам. Имеет смысл использовать словарь, если есть большая коллекция данных, в которой часто производится поиск значений по какому-то ключу.