Всегда ли нужно перегружать метод Equals и оператор == в C#?

Ответ

Нет, это не всегда требуется, но крайне рекомендуется для пользовательских типов, где важна семантика сравнения по значению. Поведение по умолчанию часто не соответствует ожиданиям.

Поведение по умолчанию:

  • Для ссылочных типов (class): == и Equals() (без переопределения) сравнивают ссылки, а не содержимое объектов.
  • Для типов значений (struct): Equals() использует побитовое сравнение полей через рефлексию (медленно), а == вообще не определен для пользовательских struct.

Когда перегружать обязательно:

  1. Для struct — чтобы обеспечить эффективное сравнение и определить оператор ==.
  2. Для class, когда логика приложения требует сравнения объектов по их данным (например, Person с одинаковым Id считаются одним человеком).

Пример корректной реализации:

public class Product : IEquatable<Product>
{
    public int Id { get; init; }
    public string Name { get; init; }

    // Реализация для производительности
    public bool Equals(Product other) => other != null && Id == other.Id;

    // Стандартное переопределение
    public override bool Equals(object obj) => Equals(obj as Product);

    // GetHashCode ОБЯЗАТЕЛЬНО должен быть переопределен вместе с Equals
    public override int GetHashCode() => Id.GetHashCode();

    // Перегрузка операторов для удобства
    public static bool operator ==(Product left, Product right) => Equals(left, right);
    public static bool operator !=(Product left, Product right) => !Equals(left, right);
}

Важно: Начиная с C# 9.0, для record типов (record class, record struct) компилятор автоматически генерирует корректные реализации Equals, GetHashCode() и операторов ==/!=, основанные на сравнении всех свойств.

Ответ 18+ 🔞

А, ну вот, опять про эти ваши сравнения объектов. Слушай, тут такая история, что если ты не переопределишь Equals и == для своих классов, то потом сам себя ненавидеть будешь, когда начнётся эта пиздобратия с багами.

Представь себе: у тебя есть два объекта Product с одинаковым Id = 5. Ты такой думаешь: "Ну, один и тот же товар, чё тут думать". А C# тебе такой: "А пошёл ты нахуй, это два разных объекта в памяти, ссылки разные, значит — нихуя не равны". И всё, приехали. product1 == product2 вернёт false, хотя по смыслу-то это одно и то же.

Как оно работает по дефолту, чтобы ты понимал, насколько всё плохо:

  • Для классов (class): И ==, и Equals (если его не трогали) сравнивают ссылки на объекты. То есть, один ли это кусок памяти или два разных. Про содержимое — нихуя.
  • Для структур (struct): Тут вообще пиздец. Equals по умолчанию лезет через рефлексию, сравнивает каждое поле — медленно, как черепаха в сиропе. А оператор == для твоей кастомной структуры вообще не определён, компилятор на тебя посмотрит как на идиота и скажет "чё, бля?".

Так когда же это надо делать? Да почти всегда, если объекты у тебя смысловые:

  1. Для структур (struct) — ОБЯЗАТЕЛЬНО. Иначе нихуя не работать будет, да ещё и тормозить.
  2. Для классов (class) — когда тебе важны данные, а не адрес в памяти. Типа, два пользователя с одинаковым UserId — это один и тот же пользователь, даже если объекты созданы в разных концах программы.

Вот смотри, как это делается без всякого геморроя. Главное — не забыть про GetHashCode, а то у тебя в Dictionary или HashSet всё полетит к ебеням.

public class Product : IEquatable<Product> // Интерфейс для красоты и скорости
{
    public int Id { get; init; }
    public string Name { get; init; }

    // Самый важный метод, по сути
    public bool Equals(Product other) => other != null && Id == other.Id;

    // Стандартная обёртка, без неё никуда
    public override bool Equals(object obj) => Equals(obj as Product);

    // БЛЯДЬ, НЕ ЗАБУДЬ ПРО ЭТОТ МЕТОД! Иначе все твои хеш-таблицы сломаются.
    public override int GetHashCode() => Id.GetHashCode();

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

И да, чтобы ты не писал эту хуйню каждый раз, в новых C# (9.0 и выше) придумали record. Объявил record class Product(int Id, string Name); и всё, компилятор сам за тебя сгенерирует все эти сравнения, основанные на значениях всех свойств. Красота, а не жизнь. Но старый код, блядь, ещё везде есть, так что понимать, как это работает изнутри, всё равно надо.