Статья #2: Сказ о том, как Dictionary дырявым стал, или Почему Пол «потерял» данные
Действующие лица:
Пол (МП): Год в индустрии, MacBook в наклейках, верит в магию фреймворков и то, что .NET сам всё порешает за его спиной.
Дядя Паша (ДП): 47 лет, архитектор старой закалки. Помнит времена, когда память выделяли дескрипторами, а за неэффективный алгоритм могли и из проекта попросить. Пьет Мальбек, смотрит на Пола как на жертву современного маркетинга.
Диалог
Пол: — Дядь Паш, ну это издевательство! .NET точно дырявый. У меня Dictionary<UserContext, string>, я туда записываю данные, а через секунду стучусь по точно такому же ключу — и KeyNotFoundException! Я дебажил три часа: поля в объекте идентичны, ID совпадает до бита. Где мои данные? Они что, протухли?
ДП: (медленно отрезает кусок эмпанады и смотрит на Пола с плохо скрываемой жалостью) — Эх, Пол... Жертва ты «быстрых курсов за 30 дней». Тебя там научили кнопки нажимать, а как шестеренки внутри хрустят — забыли. Ты мне скажи, соколик, как твой Dictionary поймет, что это один и тот же ключ, если ты ему каждый раз подсовываешь новый адрес в памяти?
Пол: — Ну как... Поля же одинаковые! UserId, TenantId. Разве он не внутрь объекта смотрит?
ДП: — Внутрь он посмотрит, когда ты его заставишь. А пока твой class UserContext — это ссылочный тип. Для рантайма твои два объекта — это как две одинаковые бутылки вина: этикетки одни, а пробки разные. Ты один объект в словарь положил, адрес его запомнил, а потом пришел с другим адресом. Для Dictionary это разные ключи. Он идет искать в другую «корзину» (bucket) и, естественно, находит там только дырку от бублика.
Пол: — И что теперь? Опять писать эту бесконечную портянку: Equals, GetHashCode, проверять на null, комбинировать поля? Это же прошлый век!
ДП: (прищуривается, делает глоток Мальбека) — Слушай сюда, инженер «счастливого будущего». В C# есть record. И это не просто «синтаксический сахар» для ленивых.
Запомни, Пол: Record — это архитектурный контракт на Value-based equality.
Когда ты пишешь public record UserContext(int Id), компилятор сам, за тебя, пишет правильный GetHashCode, который считается по значениям полей, а не по адресу. И Equals он пишет такой же. Для Dictionary два разных инстанса одного рекорда с одинаковыми данными будут одним и тем же ключом. Понимаешь? Одна строчка кода закрывает проблему, на которой вы жжете тысячи долларов облачного бюджета.
Пол: — Ладно, с «исчезновением» понятно. Но это же просто удобство, так? На производительность-то это как влияет?
ДП: (отставляет бокал и смотрит на Пола, как на человека, утверждающего, что старый фермерский пикап и болид Формулы-1 едут одинаково, потому что у обоих по четыре колеса)
— «Просто удобство»? Эх, Пол... Ты хоть раз заглядывал под капот? В реальном проекте, когда ты меняешь свой тяжелый класс на компактный record, скорость поиска в Dictionary взлетает в три раза. Минимум!
Пол: — В три раза?! Да ну, за счет чего? Это же тот же самый поиск в хэш-таблице!
ДП: — За счет инженерной точности. Компилятор генерирует для рекорда максимально эффективный код: без лишних кастов и с идеально подогнанным вычислением хэша. Твой рекорд залетает в пазы Dictionary как деталь от швейцарских часов, а твой класс — как ржавый болт, который нужно еще полчаса подпиливать напильником.
Пол: — Слушай, дядь Паш... Я ведь реально думал, что Dictionary — это просто. А тут — магия адресов, хэш-коллизии, контракты рекордов... Откуда ты всё это знаешь?
ДП: — Я это не помню, я это знаю. Потому что в наше время учили строить фундамент, а не клеить обои на гнилые стены. Современные курсы тебе этого не скажут — им выгодно, чтобы ты бесконечно покупал их «продвинутые уровни».
— Хочешь знать, где еще у тебя «дыры»? Я систему собрал. Там 15+ тысяч вопросов, которые вытрясут из тебя всю дурь и покажут, где ты реально профи, а где просто заголовки на Medium читал.
— Называется «Я хочу знать .NET». Ссылка у тебя есть — iwanttoknow.net. Пройдешь раздел по коллекциям без единого мата — налью тебе Мальбека. А пока — иди, переделывай.