Equals, GetHashCode, сравнение объектов
↑ 1.3 C♯: основы языка · ← Предыдущая
Зачем это на собесе
контракт
Equals/GetHashCode— любимый вопрос: «что будет, если переопределить Equals, но не GetHashCode». От этого зависит работа Dictionary, HashSet, LINQDistinctи сравнений сущностей.
Объяснение
Виды равенства
- Ссылочное — один и тот же объект:
ReferenceEquals(a, b). По умолчанию для классов. - По значению — одинаковые данные. По умолчанию для struct (через рефлексию, медленно), для record — сгенерировано компилятором.
==для классов по умолчанию ссылочное, но может быть перегружено (string,record).
Контракт Equals
Рефлексивность (a.Equals(a)), симметричность, транзитивность, согласованность (одинаковый результат при повторах), a.Equals(null) == false.
Контракт GetHashCode
- Если
a.Equals(b), тоa.GetHashCode() == b.GetHashCode()— обязательно. - Обратное не требуется: одинаковый хэш ≠ равенство (коллизии допустимы).
- Хэш не должен меняться, пока объект лежит в хэш-коллекции.
Почему это важно: как работает Dictionary
- По
GetHashCode()ключа вычисляется «корзина» (bucket). - Внутри корзины элементы сравниваются через
Equals. ЕслиEqualsпереопределён, аGetHashCodeнет — равные объекты попадут в разные корзины, иdict.ContainsKey(equalKey)вернёт false.
Правильная реализация
public sealed class Snils : IEquatable<Snils>
{
public string Value { get; }
public Snils(string value) => Value = Normalize(value);
public bool Equals(Snils? other) => other is not null && Value == other.Value;
public override bool Equals(object? obj) => obj is Snils s && Equals(s);
public override int GetHashCode() => Value.GetHashCode(); // по тем же полям, что Equals
public static bool operator ==(Snils? a, Snils? b) => Equals(a, b);
public static bool operator !=(Snils? a, Snils? b) => !Equals(a, b);
}
// для нескольких полей
public override int GetHashCode() => HashCode.Combine(System, Code, Version);Проще всего — record/record struct: всё генерирует компилятор.
public sealed record CodingKey(string System, string Code, string? Version); // ключ справочника терминологии
var cache = new Dictionary<CodingKey, Concept>();IEqualityComparer — равенство «снаружи»
Когда нельзя или не нужно менять тип:
var byCode = new Dictionary<string, Concept>(StringComparer.OrdinalIgnoreCase);
var unique = patients.DistinctBy(p => p.Snils); // .NET 6+
public sealed class PatientBySnilsComparer : IEqualityComparer<Patient>
{
public bool Equals(Patient? x, Patient? y) => x?.Snils == y?.Snils;
public int GetHashCode(Patient p) => p.Snils.GetHashCode();
}Сущности (Entity) vs Value Objects
- Entity — равенство по идентификатору (Id), данные могут меняться.
- Value object — равенство по всем значениям, иммутабелен (деньги, адрес, код справочника).
Нюансы и подводные камни
- Изменяемые поля в хэше: положили объект в HashSet, изменили поле, участвующее в
GetHashCode, — объект «потерялся» (Contains вернёт false). GetHashCodeстроки в .NET Core рандомизирован между запусками процесса: не сохраняйте его в БД и не используйте для шардирования. Для стабильного хэша —XxHash64или SHA.- Переопределение
==безEquals(и наоборот) приводит к непоследовательному поведению. - EF Core сравнивает сущности по ключу через change tracker. Переопределённый
Equalsу сущностей может мешать, делайте это аккуратно. - Struct без переопределения
Equalsсравнивается рефлексией и боксится — медленно в словарях.
Тестирование
[Fact]
public void Equal_objects_have_equal_hash_codes()
{
var a = new CodingKey("icd10", "J06.9", null);
var b = new CodingKey("icd10", "J06.9", null);
a.Should().Be(b);
a.GetHashCode().Should().Be(b.GetHashCode());
new HashSet<CodingKey> { a }.Contains(b).Should().BeTrue();
}Вопросы с ответами
Что будет, если переопределить Equals, но не GetHashCode?
Равные объекты будут иметь разные хэши, поэтому Dictionary, HashSet и Distinct перестанут их находить и считать одинаковыми. Компилятор выдаёт предупреждение.
Каков контракт GetHashCode?
Равные по Equals объекты обязаны иметь равные хэши. Хэш стабилен, пока объект не изменён. Разные объекты могут иметь одинаковый хэш (коллизии).
Как работает Dictionary внутри?
Хэш-таблица: GetHashCode ключа выбирает корзину, внутри корзины ключи сравниваются через Equals. Поиск в среднем O(1). При росте таблица перестраивается.
Чем равенство сущности отличается от равенства value object?
Сущность равна другой, если совпадает идентификатор. Value object равен другому, если совпадают все значения. Value object обычно иммутабелен — удобно делать через record.
Можно ли сохранять string.GetHashCode() в БД?
Нет. В .NET Core хэш строк рандомизирован для каждого процесса (защита от hash flooding) и меняется между запусками.
Связанные темы
- Предыдущая: Pattern matching и switch expressions · Следующий раздел (ООП): 1.4 ООП в C♯
- Коллекции: Массивы, List, Dictionary, HashSet, Queue, Stack
- Хэширование в задачах: Хэширование: Dictionary и HashSet в задачах
- Records: Records, init, with, required, primary constructors
- DDD: entity и value object: DDD: тактический — entity, value object, aggregate, domain events
- Сравнение в JS: Сравнение: и =, Object.is, NaN