👨🎓️ Учебник по 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и значениемvaluebool TryAdd(TKey key, TValue value)— Метод пытается добавить новый элемент в коллекцию. Если ключ в словаре не найден, то метод ничего не сделает и вернётfalse.
Удаление:
bool Remove(TKey key)— Удаляет элемент по ключу, при успехе возвращает —truebool 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> ёмкость автоматически увеличивается, так как требуется перераспределить внутренний массив.
Итог
Словарь используется для быстрого доступа к значениям по ключу и содержит значения, которые можно получить по уникальным ключам. Имеет смысл использовать словарь, если есть большая коллекция данных, в которой часто производится поиск значений по какому-то ключу.