Чем отличаются оператор == и вызов метода object.Equals в C#?
1,00
р.
р.
Чем отличаются оператор == и вызов метода object.Equals в C#?
Ответ На самом деле, существует целый зоопарк методов сравнения объектов в C#. Сравнение через ==. Это сравнение разрешается на этапе компиляции согласно заявленным типам левой и правой части сравнения. Оператор сравнения можно перегрузить. Он является статическим, настоящий тип объектов не играет роли при выборе того, какой из перегруженных операторов сравнения будет использован. Пример: object o1 = 5, o2 = 5 bool eq = (o1 == o2) // false Неперегруженный оператор == проверяет для ссылочных типов равенство ссылок. Допускается значение null, null равен только другому null. Строка имеет перегруженный оператор ==, который сравнивает не ссылки, а содержимое строк. В перегруженном операторе для вашего объекта вы можете, разумеется, реализовать любую логику. Оператор == для предопределённых типов-значений проверяет равенство этих значений. Для чисел, например, это может привести к приведению типов: если один из операндов int, а второй — double, то первый операнд тоже будет приведён к типу double (то есть 5 == 5.0 даёт true). Оператор == для пользовательских структур не определён, вы должны написать его сами. Наличие статического оператора == для текущей версии языка не выражается при помощи генерик-ограничения, поэтому этот оператор часто бесполезен в обобщённых методах для аргументов обобщённого типа. Сравнение через виртуальный метод Equals(object). Это сравнение разрешается вызовом виртуальной функции Equals и основывается на динамическом типе левого аргумента. Пример: object o1 = 5, o2 = 5 bool eq = o1.Equals(o2) // true Для непереопределённой функции, для ссылочных типов проверяется также равенство ссылок. В отличие от предыдущего случая, для вызова a.Equals(b) значение null в a, как и во всяком вызове метода, приводит к NullReferenceException. ValueType, базовый класс всех типов-значений, переопределяет метод Equals. Таким образом, если вы не переопределите этот метод, для типов-значений он будет вести себя следующим образом: если все нестатические поля (включая приватные) типа-значения являются также типами-значениями рекурсивно вниз (то есть, ни одно из полей и подполей не является ссылочным типом), то значения сравниваются побитно. в противном случае применяется рефлексия, и значения всех полей сравниваются попарно через Equals(object).* Затем, сравнение через рефлексию — штука медленная, поэтому если ваш тип-значение будет часто сравниваться, имеет смысл переопределить метод Equals(object). Кроме того, сравнение через Equals(object) не приводит к преобразованиям типов, поэтому 5.Equals(5.0) даст false. Если вы переопределяете Equals(object), скорее всего вам понадобится переопределить и GetHashCode() (о чём вам любезно напомнит компилятор). Сравнение через object.Equals(object o1, object o2). Это по сути удобная обёртка над Equals(object): метод проверяет, не пришёл ли к нему на вход один и тот же объект или null, и только если обе эти проверки не дали результат, вызывает Equals(object) у первого объекта. Имеет смысл использовать этот метод вместо Equals(object), чтобы избежать обвязочного кода с проверками на null. Следующий метод — реализация интерфейса IEquatable. Это типизированный интерфейс, так что вам не придётся проверять, какого типа ваш операнд. Как и Equals(object), это виртуальный метод. Впрочем, он не особо осмыслен в сложных иерархиях классов, поэтому скорее всего будет вызываться с известным runtime-типом обоих аргументов — ведь тип правой части задан параметром генерика! Этот метод предпочтительнее для использования, чем перегрузки Equals(object), так как у вас отпадают расходы на проверку типов и упаковку [boxing] (для типов-значений). Но если вы уж реализовали этот интерфейс, имеет смысл реализовать и Equals(object) совместимым образом: // для класса (по контракту, Equals(null) должно возвращать false) public override bool Equals(object o) => Equals(o as T) // для структуры public override bool Equals(object o) => o is T t && Equals(t) Затем, если вам нужен специальный метод сравнения объектов для конкретной, а не общей ситуации (или вы хотите сравнивать специальным образом объекты, которые вам не принадлежат), вы можете делегировать сравнение специальному объекту, реализующему интерфейс IEqualityComparer (нетипизированный) или IEqualityComparer (типизированный). Сравнение при помощи таких сравнивающих объектов применяют, например, Hashtable и Dictionary, а также некоторые LINQ-методы. В дополнение к нему есть вспомогательный класс EqualityComparer. EqualityComparer предоставляет IEqualityComparer, который проверяет, реализует ли тип T интерфейс IEquatable, и в противном случае выполняет сравнение через Equal(object) (который всегда присутствует). (Также для реализации IEqualityComparer советуют наследоваться от EqualityComparer.) И наконец, равенство/неравенство может быть получено из более общего сравнения на больше/меньше/равно. Для этого используются операторы </> (которые аналогичны оператору ==, но определены для более узкой группы типов), интерфейсы IComparable (аналог метода Equals(object)), IComparable (аналог интерфейса IEquatable), IComparer (аналог IEqualityComparer) и IComparer (аналог IEqualityComparer). А также делегат Comparison (например, для перегрузки List.Sort(Comparison)).
Переопределяя какой-либо из методов сравнения объектов, не забывайте, что ваш метод должен быть рефлексивен, симметричен и транзитивен, а также по возможности не выбрасывать исключений (например, если операнды несовместимых типов, просто верните false).
*Также в .NET Core побитное сравнение не используется для структур, содержащих double/float. В .NET Framework это дополнительное правило не используется, что приводит к следующей проблеме. Побитовое равенство и равенство значений — немного разные вещи. Например, минус ноль не равен побитово плюс нулю. Поэтому такой код: struct BitComparable { double d public BitComparable(double d) { this.d = d } } struct NonBitComparable { double d object o public NonBitComparable(double d, object o) { this.d = d this.o = o } } var x1 = new BitComparable(1 / double.PositiveInfinity) // +0 var y1 = new BitComparable(1 / double.NegativeInfinity) // -0 Console.WriteLine(x1.Equals(y1)) var x2 = new NonBitComparable(1 / double.PositiveInfinity, null) var y2 = new NonBitComparable(1 / double.NegativeInfinity, null) Console.WriteLine(x2.Equals(y2)) выводит False и True соответственно. В .NET Core данное поведение исправлено.