Для чего в C# переопределяют метод Equals?

«Для чего в C# переопределяют метод Equals?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Метод Equals переопределяют, чтобы изменить логику сравнения объектов по умолчанию (сравнение ссылок для ссылочных типов) на сравнение по значению (по содержимому полей). Это необходимо, когда важна семантическая эквивалентность объектов, а не их идентичность в памяти.

Когда это нужно:

  1. Для типов-значений (struct): По умолчанию Equals для struct использует побитовое сравнение (рефлексию), что может быть неэффективно. Переопределение позволяет задать явную и быструю логику.
  2. Для ссылочных типов (class), которые должны вести себя как значения: Например, классы Money, Point, Person (в контексте сравнения по ID и имени).
  3. Для корректной работы с коллекциями: Методы Contains, IndexOf в List<T>, а также Dictionary<TKey, TValue> и HashSet<T> полагаются на Equals для проверки наличия элементов.

Обязательное правило: При переопределении Equals всегда переопределяйте GetHashCode(), чтобы объекты, считающиеся равными по Equals, возвращали одинаковый хэш-код. Нарушение этого правила приведет к некорректной работе хэш-таблиц.

Пример правильной реализации для класса:

public class Person : IEquatable<Person> // Реализация IEquatable<T> для избежания боксинга
{
    public string Name { get; init; }
    public int Age { get; init; }

    // Реализация для избежания боксинга при сравнении Person с Person
    public bool Equals(Person other)
    {
        if (other is null) return false;
        // Используем сравнение строк с учетом/без учета регистра в зависимости от логики
        return String.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) 
               && Age == other.Age;
    }

    // Стандартная перегрузка Equals(object)
    public override bool Equals(object obj) => Equals(obj as Person);

    // КРИТИЧНО: Согласованный GetHashCode
    public override int GetHashCode() => HashCode.Combine(
        Name?.ToUpperInvariant(), // Учитываем ту же логику сравнения, что и в Equals
        Age
    );

    // Для удобства можно перегрузить операторы == и !=
    public static bool operator ==(Person left, Person right) => Equals(left, right);
    public static bool operator !=(Person left, Person right) => !Equals(left, right);
}

// Использование:
var person1 = new Person { Name = "Alice", Age = 30 };
var person2 = new Person { Name = "alice", Age = 30 }; // Та же семантическая сущность

Console.WriteLine(person1.Equals(person2)); // True (благодаря OrdinalIgnoreCase)
Console.WriteLine(person1 == person2);      // True (если перегружены операторы)

var set = new HashSet<Person> { person1 };
Console.WriteLine(set.Contains(person2));   // True (работает благодаря корректным Equals и GetHashCode)