Почему ToDictionary может упасть на обычных данных?
Когда нужно быстро превратить список в словарь, часто используют ToDictionary. Это удобно, пока ключи уникальные.
Например, есть список пользователей и нужно быстро искать их по email.
var byEmail = users.ToDictionary(u => u.Email);
Код короткий и читаемый. Но он содержит скрытое предположение, что одинаковых email в списке точно нет.
Если из API, CSV или базы прилетят два пользователя с одним ключом, ToDictionary бросит исключение. Он не может сам решить, какой объект оставить.
Если дубли возможны, сначала нужно явно выбрать правило. Например, оставить первый элемент.
var byEmail = users
.GroupBy(u => u.Email)
.ToDictionary(g => g.Key, g => g.First());
Теперь поведение видно прямо в коде. Мы не случайно теряем данные, а осознанно выбираем первый объект для каждого email.
Если важнее оставить последний элемент, правило тоже должно быть явным.
var byEmail = users
.GroupBy(u => u.Email)
.ToDictionary(g => g.Key, g => g.Last());
А если нужно сохранить все значения одного ключа, лучше использовать ToLookup.
var byEmail = users.ToLookup(u => u.Email);
Lookup похож на словарь, но один ключ может содержать несколько элементов. Это хорошо подходит для тегов, ролей, заказов пользователя и результатов группировки.
Иногда правильнее вообще не чинить дубли в коде, а остановить импорт и показать проблему.
var duplicates = users
.GroupBy(u => u.Email)
.Where(g => g.Count() > 1);
Такой вариант полезен, если email должен быть уникальным по бизнес-правилу. Тогда исключение лучше заменить на понятную ошибку валидации.
Вывод простой. ToDictionary хорош, когда уникальность гарантирована. Если данные приходят извне, сначала реши, что делать с дублями.
В таком случае comparer лучше указать явно.
var byEmail = users
.GroupBy(u => u.Email, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
Без этого можно получить два разных ключа, хотя с точки зрения бизнеса это один и тот же пользователь.
Если данные приходят из внешнего источника, полезно сначала нормализовать ключи.
var email = user.Email.Trim().ToLowerInvariant();
Но нормализация тоже должна быть осознанной. Для одних полей это правильно, для других можно случайно сломать смысл значения.
➡️ C# Ready | #совет
Когда нужно быстро превратить список в словарь, часто используют ToDictionary. Это удобно, пока ключи уникальные.
Например, есть список пользователей и нужно быстро искать их по email.
var byEmail = users.ToDictionary(u => u.Email);
Код короткий и читаемый. Но он содержит скрытое предположение, что одинаковых email в списке точно нет.
Если из API, CSV или базы прилетят два пользователя с одним ключом, ToDictionary бросит исключение. Он не может сам решить, какой объект оставить.
Если дубли возможны, сначала нужно явно выбрать правило. Например, оставить первый элемент.
var byEmail = users
.GroupBy(u => u.Email)
.ToDictionary(g => g.Key, g => g.First());
Теперь поведение видно прямо в коде. Мы не случайно теряем данные, а осознанно выбираем первый объект для каждого email.
Если важнее оставить последний элемент, правило тоже должно быть явным.
var byEmail = users
.GroupBy(u => u.Email)
.ToDictionary(g => g.Key, g => g.Last());
А если нужно сохранить все значения одного ключа, лучше использовать ToLookup.
var byEmail = users.ToLookup(u => u.Email);
Lookup похож на словарь, но один ключ может содержать несколько элементов. Это хорошо подходит для тегов, ролей, заказов пользователя и результатов группировки.
Иногда правильнее вообще не чинить дубли в коде, а остановить импорт и показать проблему.
var duplicates = users
.GroupBy(u => u.Email)
.Where(g => g.Count() > 1);
Такой вариант полезен, если email должен быть уникальным по бизнес-правилу. Тогда исключение лучше заменить на понятную ошибку валидации.
Вывод простой. ToDictionary хорош, когда уникальность гарантирована. Если данные приходят извне, сначала реши, что делать с дублями.
В таком случае comparer лучше указать явно.
var byEmail = users
.GroupBy(u => u.Email, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
Без этого можно получить два разных ключа, хотя с точки зрения бизнеса это один и тот же пользователь.
Если данные приходят из внешнего источника, полезно сначала нормализовать ключи.
var email = user.Email.Trim().ToLowerInvariant();
Но нормализация тоже должна быть осознанной. Для одних полей это правильно, для других можно случайно сломать смысл значения.
➡️ C# Ready | #совет