Ответ
Класс System.Object является корнем иерархии типов в C#. Все типы (как ссылочные, так и значимые через boxing) неявно наследуются от Object. Он предоставляет несколько виртуальных методов, которые можно и часто нужно переопределять в пользовательских классах для корректного поведения.
Виртуальные методы класса Object:
-
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 также должен быть переопределен (см. ниже) }
- Назначение: Определяет семантическое равенство объектов (равенство значений), в отличие от равенства ссылок (
-
public virtual int GetHashCode()- Назначение: Возвращает числовой хеш-код объекта, используемый в хеш-таблицах (
Dictionary<TKey, TValue>,HashSet<T>). - Критически важные контракты при переопределении:
- Если два объекта равны по
Equals(), они должны возвращать одинаковый хеш-код. - Хеш-код должен быть стабильным на протяжении жизненного цикла объекта (пока он находится в коллекции).
- Желательно, чтобы разные объекты возвращали разные хеш-коды для производительности.
- Если два объекта равны по
- Пример (сопряженный с
Equalsвыше):public override int GetHashCode() => Id.GetHashCode();
- Назначение: Возвращает числовой хеш-код объекта, используемый в хеш-таблицах (
-
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#.
Ответ 18+ 🔞
А, ну вот, System.Object, мать его! Корень всей этой нашей иерархии в C#, как дед на печи — все от него пляшут, и ссылочные, и значимые, если их в бокс запихнуть. А внутри у него, у этого деда, есть несколько виртуальных методов, которые он как бы предлагает: «Внучата, перепишите на свой лад, а то у меня реализация так себе».
Вот эти самые виртуальные методы, на которые надо глаз положить:
-
public virtual bool Equals(object? obj)- Зачем нужен: Чтобы понять, равны ли два объекта по смыслу, а не просто потому что это одна и та же ссылка в памяти. По умолчанию для классов
==сравнивает именно ссылки, а это часто не то, что надо. - Когда в него лезть: Когда у тебя есть свой класс, и два разных экземпляра могут считаться одинаковыми. Например, объект
Деньгис суммой 100 рублей и валютой USD должен быть равен другому такому же объекту, даже если они в памяти в разных местах. Важное правило, которое все хуярят: если переопределилEquals, тут же, блядь, переопределяй иGetHashCode(), иначе потом будешь искать, почему вDictionaryвсё ебется. -
Пример, чтобы было понятнее:
public class Товар { public int Айди { get; set; } public string Название { get; set; } public override bool Equals(object obj) { // Если obj — это Товар, и айдишники совпали, то это один и тот же товар по нашей логике return obj is Товар другой && this.Айди == другой.Айди; } // GetHashCode тоже надо! Смотри ниже. }
- Зачем нужен: Чтобы понять, равны ли два объекта по смыслу, а не просто потому что это одна и та же ссылка в памяти. По умолчанию для классов
-
public virtual int GetHashCode()- Зачем нужен: Чтобы быстро находить объекты в хеш-таблицах, типа
DictionaryилиHashSet. Это как номер ячейки в гигантском шкафу. - Главные правила, которые нарушать — себя не уважать:
- Если два объекта по
Equalsравны, то их хеш-коды обязаны быть одинаковыми. Иначе в коллекциях начнётся пиздец, объекты потеряются. - Хеш-код должен быть стабильным, пока объект живёт. Нельзя, чтобы он менялся, пока объект лежит, например, в словаре.
- Для разных объектов хорошо бы возвращать разные коды, чтобы не было коллизий и поиск был быстрым.
- Если два объекта по
- Пример (продолжаем издеваться над
Товаром):public override int GetHashCode() => Айди.GetHashCode(); // Всё просто, хеш-код товара — это хеш его айди.
- Зачем нужен: Чтобы быстро находить объекты в хеш-таблицах, типа
-
public virtual string? ToString()- Зачем нужен: Чтобы получить человекочитаемую строку из объекта. По умолчанию он выдаёт полное имя типа, что полезно, как паровоз в огороде.
- Когда переопределять: Почти всегда для своих классов! Когда ты в отладке смотришь на значение переменной или пишешь
Console.WriteLine(myObject), хочется видеть осмысленные данные, а неYourNamespace.SomeClass. Широко используется в интерполяции строк. - Пример:
public override string ToString() => $"Товар {{ Айди = {Айди}, Название = {Название} }}";
А ещё есть методы невиртуальные, статические:
public static bool ReferenceEquals(object? objA, object? objB)- Этот товарищ всегда, при любых раскладах, проверяет, одна ли это ссылка в памяти. Даже если ты перегрузил оператор
==. Полезно, когда нужно на 100% убедиться, что это один и тот же физический объект.
- Этот товарищ всегда, при любых раскладах, проверяет, одна ли это ссылка в памяти. Даже если ты перегрузил оператор
public static bool Equals(object? objA, object? objB)- Статический помощник, который не паникует от
nullи внутри вызывает уже виртуальныйEquals. Удобная обёртка.
- Статический помощник, который не паникует от
И один защищённый метод, как семейная реликвия:
protected object MemberwiseClone()- Делает поверхностную копию объекта. То есть скопирует все поля: для чисел — значения, для ссылок на другие объекты — скопирует только ссылку (сам объект-прицеп клонирован не будет). Иногда полезно, но глубоко не копает.
Ну и примерчик в студию, чтобы закрепить:
var список1 = new List<int>() { 1, 2, 3 };
var список2 = список1; // Скопировали ссылку, это один и тот же объект
var список3 = new List<int>() { 1, 2, 3 }; // Создали новый объект, но с такими же данными
Console.WriteLine(Object.ReferenceEquals(список1, список2)); // True (одна и та же банка сгущёнки)
Console.WriteLine(Object.ReferenceEquals(список1, список3)); // False (две разные банки)
Console.WriteLine(список1.Equals(список3)); // У List<T> Equals переопределён — может вернуть True (сравнивает содержимое)
Console.WriteLine(список1.GetType().Name); // "List`1"
Console.WriteLine(список1.GetType().FullName); // "System.Collections.Generic.List`1[[System.Int32...]]" — вот это уже полное имя с деталями
Итог, блядь: Если пишешь свой класс, который представляет какую-то сущность (не просто служебный класс на один вызов), то переопределение Equals, GetHashCode и ToString — это не прихоть, а признак того, что ты не мудак и думаешь о том, как твоим классом будут пользоваться. Без этого он будет вести себя тупо и непредсказуемо в стандартных коллекциях.
Видео-ответы
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶