Какие виртуальные методы предоставляет базовый класс Object в C# и для чего они используются?

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

Ответ

Класс System.Object является корнем иерархии типов в C#. Все типы (как ссылочные, так и значимые через boxing) неявно наследуются от Object. Он предоставляет несколько виртуальных методов, которые можно и часто нужно переопределять в пользовательских классах для корректного поведения.

Виртуальные методы класса Object:

  1. public virtual bool Equals(object? obj)

    • Назначение: Определяет семантическое равенство объектов (равенство значений), в отличие от равенства ссылок (== по умолчанию для ссылочных типов).
    • Когда переопределять: Для классов, где два разных экземпляра могут считаться логически равными (например, Money, DateTime, Person с одинаковым Id). Всегда переопределяйте GetHashCode() вместе с Equals().
    • Пример:

      public class Product
      {
      public int Id { get; set; }
      public string Name { get; set; }
      
      public override bool Equals(object obj)
      {
          return obj is Product other && this.Id == other.Id;
      }
      // GetHashCode также должен быть переопределен (см. ниже)
      }
  2. public virtual int GetHashCode()

    • Назначение: Возвращает числовой хеш-код объекта, используемый в хеш-таблицах (Dictionary<TKey, TValue>, HashSet<T>).
    • Критически важные контракты при переопределении:
      1. Если два объекта равны по Equals(), они должны возвращать одинаковый хеш-код.
      2. Хеш-код должен быть стабильным на протяжении жизненного цикла объекта (пока он находится в коллекции).
      3. Желательно, чтобы разные объекты возвращали разные хеш-коды для производительности.
    • Пример (сопряженный с Equals выше):
      public override int GetHashCode() => Id.GetHashCode();
  3. public virtual string? ToString()

    • Назначение: Возвращает строковое представление объекта. Реализация по умолчанию возвращает полное имя типа.
    • Когда переопределять: Для предоставления удобочитаемой отладочной информации. Широко используется при интерполяции строк ($"{obj}") и вызове Console.WriteLine(obj).
    • Пример:
      public override string ToString() => $"Product {{ Id = {Id}, Name = {Name} }}";

Невиртуальные (статические) методы:

  • public static bool ReferenceEquals(object? objA, object? objB)
    • Всегда проверяет равенство ссылок, даже для типов, перегрузивших оператор ==. Полезен, когда нужно гарантированно проверить, указывают ли две переменные на один экземпляр в памяти.
  • public static bool Equals(object? objA, object? objB)
    • Статический помощник, который безопасно обрабатывает null и вызывает виртуальный instance.Equals().

Защищенный метод:

  • protected object MemberwiseClone()
    • Создает поверхностную копию (shallow copy) текущего объекта. Копируются значения полей значимых типов и ссылки для ссылочных типов (сами объекты не клонируются).

Пример использования ReferenceEquals и GetType:

var list1 = new List<int>() { 1, 2, 3 };
var list2 = list1; // Копируется ссылка
var list3 = new List<int>() { 1, 2, 3 }; // Новый объект с теми же данными

Console.WriteLine(Object.ReferenceEquals(list1, list2)); // True (одна ссылка)
Console.WriteLine(Object.ReferenceEquals(list1, list3)); // False (разные объекты)
Console.WriteLine(list1.Equals(list3)); // Для List<T> переопределен - может быть True (сравнение содержимого)

Console.WriteLine(list1.GetType().Name); // "List`1"
Console.WriteLine(list1.GetType().FullName); // "System.Collections.Generic.List`1[[System.Int32...]]"

Итог: Переопределение Equals, GetHashCode и ToString — стандартная практика для создания полноценных, предсказуемых и удобных в использовании типов данных в C#.

Видео-ответы