Какой контракт между методами Equals и GetHashCode в C# и что будет, если GetHashCode всегда возвращает одну и ту же константу
Что проверяют
Понимаете ли вы, как устроены Dictionary<TKey, TValue> и HashSet<T>, и отличаете ли нарушение контракта от просто плохой реализации. Во второй части вопроса подвох: многие уверенно говорят, что поиск сломается. Следом обычно спрашивают, что будет, если изменить поле ключа после добавления в словарь.
Как отвечать
Контракт в трёх пунктах:
- равные объекты дают равный хэш: если
a.Equals(b)возвращаетtrue, тоa.GetHashCode() == b.GetHashCode(). Обратное не требуется, коллизии допустимы; - стабильность: пока объект лежит в хэш-коллекции, его хэш не должен меняться, поэтому считать его стоит по неизменяемым полям;
- не для хранения: значение не гарантировано между запусками, у строк хэш рандомизирован, поэтому сохранять его в базу нельзя.
Теперь константа. Контракт она не нарушает: равные объекты получают равные хэши. Dictionary найдёт ключ правильно, ContainsKey вернёт true для равного объекта. Ломается производительность: все элементы попадают в одну корзину, и каждая операция сравнивает ключ через Equals со всей цепочкой. Поиск деградирует с O(1) до O(n), заполнение коллекции из n элементов до O(n²). На десятках элементов это незаметно, на сотне тысяч сервис встаёт.
Как делать правильно: переопределять Equals и GetHashCode вместе (иначе будет предупреждение CS0659), реализовать IEquatable<T>, хэш собирать через HashCode.Combine(Id, Name). Для классов, которые просто хранят данные, проще взять record: компилятор сгенерирует оба метода по всем свойствам. Для особого равенства в одной коллекции есть свой IEqualityComparer<T>.
Где спотыкаются
Говорят, что при константном хэше ContainsKey вернёт false. Вернёт true, просто медленно. false получится при обратной ошибке, когда Equals переопределён, а GetHashCode остался стандартным.
Считают хэш по изменяемому полю. После key.Id = 5 объект остаётся в старой корзине, Contains его не находит, а Remove не удаляет.
Включают в GetHashCode поле, которого нет в Equals, например временную метку. Равные объекты получают разные хэши, и словарь хранит их как разные ключи: это уже настоящее нарушение контракта.