Equals c что делает
Перейти к содержимому

Equals c что делает

  • автор:

Чем отличаются оператор == и вызов метода object.Equals в C#?

На самом деле, существует целый зоопарк методов сравнения объектов в C#.

    Сравнение через == . Это сравнение разрешается на этапе компиляции согласно заявленным типам левой и правой части сравнения. Оператор сравнения можно перегрузить. Он является статическим, настоящий тип объектов не играет роли при выборе того, какой из перегруженных операторов сравнения будет использован. Пример:

 object o1 = 5, o2 = 5; bool eq = (o1 == o2); // false 
 object o1 = 5, o2 = 5; bool eq = o1.Equals(o2); // true 
  • если все нестатические поля (включая приватные) типа-значения являются также типами-значениями рекурсивно вниз (то есть, ни одно из полей и подполей не является ссылочным типом), то значения сравниваются побитно.
  • в противном случае применяется рефлексия, и значения всех полей сравниваются попарно через Equals(object) .*

Затем, сравнение через рефлексию — штука медленная, поэтому если ваш тип-значение будет часто сравниваться, имеет смысл переопределить метод Equals(object) .

Кроме того, сравнение через Equals(object) не приводит к преобразованиям типов, поэтому 5.Equals(5.0) даст false .

Если вы переопределяете Equals(object) , скорее всего вам понадобится переопределить и GetHashCode() (о чём вам любезно напомнит компилятор).

 // для класса (по контракту, 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); 

Переопределяя какой-либо из методов сравнения объектов, не забывайте, что ваш метод должен быть рефлексивен, симметричен и транзитивен, а также по возможности не выбрасывать исключений (например, если операнды несовместимых типов, просто верните 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 данное поведение исправлено.

Equals c что делает

Все классы в .NET, даже те, которые мы сами создаем, а также базовые типы, такие как System.Int32 , являются неявно производными от класса Object. Даже если мы не указываем класс Object в качестве базового, по умолчанию неявно класс Object все равно стоит на вершине иерархии наследования. Поэтому все типы и классы могут реализовать те методы, которые определены в классе System.Object. Рассмотрим эти методы.

ToString

Метод ToString служит для получения строкового представления данного объекта. Для базовых типов просто будет выводиться их строковое значение:

int i = 5; Console.WriteLine(i.ToString()); // выведет число 5 double d = 3.5; Console.WriteLine(d.ToString()); // выведет число 3,5

Для классов же этот метод выводит полное название класса с указанием пространства имен, в котором определен этот класс. И мы можем переопределить данный метод. Посмотрим на примере:

Person person = new Person < Name = "Tom" >; Console.WriteLine(person.ToString()); // выведет название класса Person Clock clock = new Clock < Hours = 15, Minutes = 34, Seconds = 53 >; Console.WriteLine(clock.ToString()); // выведет 15:34:53 class Clock < public int Hours < get; set; >public int Minutes < get; set; >public int Seconds < get; set; >public override string ToString() < return $"::"; > > class Person < public string Name < get; set; >= ""; >

Для переопределения метода ToString() в классе Clock, который представляет часы, используется ключевое слово override (как и при обычном переопределении виртуальных или абстрактных методов). В данном случае метод ToString() выводит в строке значения свойств Hours, Minutes, Seconds.

Класс Person не переопределяет метод ToString, поэтому для этого класса срабатывает стандартная реализация этого метода, которая выводит просто название класса.

Кстати в данном случае мы могли задействовать обе реализации:

Person tom = new Person < Name = "Tom" >; Console.WriteLine(tom.ToString()); // Tom Person undefined = new Person(); Console.WriteLine(undefined.ToString()); // Person class Person < public string Name < get; set; >= ""; public override string? ToString() < if (string.IsNullOrEmpty(Name)) return base.ToString(); return Name; >>

То есть если имя — свойство Name не имеет значения, оно представляет пустую строку, то возвращается базовая реализация — название класса. Стоит отметить, что базовая реализация возвращает не просто строку, а объект string? — то есть это может быть строка string, либо значение null , которое указывает на отсутствие значения. И в реальности в качестве возвращаемого типа для метода мы можем использовать как string , так и string?

Если же имя у объекта Person установлено, то возвращается значение свойства Name. Для проверки строки на наличие значения применяется метод String.IsNullOrEmpty() .

Стоит отметить, что различные технологии на платформе .NET активно используют метод ToString для разных целей. В частности, тот же метод Console.WriteLine() по умолчанию выводит именно строковое представление объекта. Поэтому, если нам надо вывести строковое представление объекта на консоль, то при передаче объекта в метод Console.WriteLine необязательно использовать метод ToString() — он вызывается неявно:

Person person = new Person < Name = "Tom" >; Console.WriteLine(person); // Tom Clock clock = new Clock < Hours = 15, Minutes = 34, Seconds = 53 >; Console.WriteLine(clock); // выведет 15:34:53

Метод GetHashCode

Метод GetHashCode позволяет возвратить некоторое числовое значение, которое будет соответствовать данному объекту или его хэш-код. По данному числу, например, можно сравнивать объекты. Можно определять самые разные алгоритмы генерации подобного числа или взять реализацию базового типа:

class Person < public string Name < get; set; >= ""; public override int GetHashCode() < return Name.GetHashCode(); >>

В данном случае метод GetHashCode возвращает хеш-код для значения свойства Name. То есть два объекта Person, которые имеют одно и то же имя, будут возвращать один и тот же хеш-код. Однако в реальности алгоритм может быть самым различным.

Получение типа объекта и метод GetType

Метод GetType позволяет получить тип данного объекта:

Person person = new Person < Name = "Tom" >; Console.WriteLine(person.GetType()); // Person

Этот метод возвращает объект Type , то есть тип объекта.

С помощью ключевого слова typeof мы получаем тип класса и сравниваем его с типом объекта. И если этот объект представляет тип Person, то выполняем определенные действия.

object person = new Person < Name = "Tom" >; if (person.GetType() == typeof(Person)) Console.WriteLine("Это реально класс Person");

Причем поскольку класс Object является базовым типом для всех классов, то мы можем переменной типа object присвоить объект любого типа. Однако для этой переменной метод GetType все равно вернет тот тип, на объект которого ссылается переменная. То есть в данном случае объект типа Person.

Стоит отметить, что проверку типа в примере выше можно сократить с помощью оператора is :

object person = new Person < Name = "Tom" >; if (person is Person) Console.WriteLine("Это реально класс Person");

В отличие от методов ToString, Equals, GetHashCode метод GetType() не переопределяется.

Метод Equals

Метод Equals позволяет сравнить два объекта на равенство. В качестве параметра он принимает объект для сравнения в виде типа object и возврашает true , если оба объекта равны:

public override bool Equals(object? obj)

Например, реализуем данный метод в классе Person:

class Person < public string Name < get; set; >= ""; public override bool Equals(object? obj) < // если параметр метода представляет тип Person // то возвращаем true, если имена совпадают if (obj is Person person) return Name == person.Name; return false; >// вместе с методом Equals следует реализовать метод GetHashCode public override int GetHashCode() => Name.GetHashCode(); >

Метод Equals принимает в качестве параметра объект любого типа, который мы затем приводим к текущему классу — классу Person.

Если переданный объект представляет тип Person, то возвращаем результат сравнения имен двух объектов Person. Если же объект представляет другой тип, то возвращается false.

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

Стоит отметить, что вместе с методом Equals следует реализовать метод GetHashCode.

var person1 = new Person < Name = "Tom" >; var person2 = new Person < Name = "Bob" >; var person3 = new Person < Name = "Tom" >; bool person1EqualsPerson2 = person1.Equals(person2); // false bool person1EqualsPerson3 = person1.Equals(person3); // true Console.WriteLine(person1EqualsPerson2); // false Console.WriteLine(person1EqualsPerson3); // true

И если следует сравнивать два сложных объекта, как в данном случае, то лучше использовать метод Equals, а не стандартную операцию ==.

equals и == в Java

Оператор == в Java делает не совсем тоже самое что и в других языках. В Java он проверяет, являются ли переменные(или значения) одним и тем же объектом, в то время как в других языках(C++/php/etc.) этот оператор проверяет совпадает ли содержимое переменных.

Чтобы проверить равенство объектов на идентичность содержимого, нужно использовать функцию Object.equals(Object obj) для объекта, например mystring.equals(“Content”).

public static void main(String[] args)  String str1 = new String("MyName"); String str2 = new String("MyName"); System.out.print("str1 == str2 : "); if (str1 == str2)  System.out.print("true"); > else System.out.print("false"); > System.out.print("\nstr1.equals(str2) : "); if (str1.equals(str2))  System.out.print("true"); > else  System.out.print("false"); > String str3 = new String(str2); System.out.print("\nstr3.equals(str2) : "); if (str3.equals(str2))  System.out.print("true"); > else  System.out.print("false"); > System.out.print("\nstr3 == str2 : "); if (str3 == str2)  System.out.print("true"); > else System.out.print("false"); > System.out.print("\nstr3 == MyName : "); if (str3 == "MyName")  System.out.print("true"); > else System.out.print("false"); > >
str1 == str2 : false str1.equals(str2) : true str3.equals(str2) : true str3 == str2 : false str3 == MyName : false
  • [0] Java: ==, .equals(), compareTo(), and compare()
  • [1] Object (Java 2 Platform SE v1.4.2)
  • blog.. not another yet

Сравнение объектов в C#.NET

Также имеется возможность наследования IEquatable, IStructuralEquatable.

ReferenceEquals

Метод ReferenceEquals сравнивает две ссылки. Если ссылки на объекты идентичны, то возвращает true. Это значит, что данный метод проверяет экземпляры не на равенство, а на тождество. В случае передачи этому методу экземпляров значимого типа (даже если передать один и тот же экземпляр) всегда будет возвращать false. Так произойдёт потому, что при передаче произойдёт упаковка значимых типов и ссылки на них будут разные.
Здесь также хотелось бы упомянуть о сравнение двух строк этим методом. Например:

class Program < static void Main(string[] args) < string a = "Hello"; string b = "Hello"; if(object.ReferenceEquals(a,b)) Console.WriteLine("Same objects"); else Console.WriteLine("Not the same objects"); Console.ReadLine(); >> 

Такая программа, запросто может вывести «Same objects». Не стоит переживать, это связано с интернированием строк. Но это совсем другая история и здесь об этом речи идти не будет.

public static bool Equals(object objA, object objB)

Сначала этот метод проверяет экземпляры на тождество и если объекты не тождественны, то проверяет их на null и делегирует ответственность за компарацию переопределяемому экземплярному методу Equals.

public virtual bool Equals(object obj)

По умолчанию, этот метод ведёт себя точно также как ReferenceEquals. Однако для значимых типов он переопределён и в System.ValueType выглядит следующим образом:

public override bool Equals(object obj) < if (obj == null) < return false; >RuntimeType runtimeType = (RuntimeType)base.GetType(); RuntimeType left = (RuntimeType)obj.GetType(); if (left != runtimeType) < return false; >if (ValueType.CanCompareBits(this)) < return ValueType.FastEqualsCheck(this, obj); >FieldInfo[] fields = runtimeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); for (int i = 0; i < fields.Length; i++) < object obj2 = ((RtFieldInfo)fields[i]).InternalGetValue(this, false); object obj3 = ((RtFieldInfo)fields[i]).InternalGetValue(obj, false); if (obj2 == null) < if (obj3 != null) < return false; >> else < if (!obj2.Equals(obj3)) < return false; >> > return true; > 

Не дай бог никому пользоваться такой имплементацией на больших множествах. Разработчики BCL не могут знать какие значимые типы мы будем определять и проводят сравнение экземпляров значимых типов по их полям, используя рефлексию, заранее ничего не зная об этих полях. Разумеется, это не очень производительный способ сравнения. Поэтому, при использовании значимых типов, известных на этапе компиляции, необходимо переопределить этот метод, ибо кто лучше вас может знать как сравнить два разработанных вами объекта? Для ссылочных типов, без нужды сравнения двух экземпляров на манер значимых типов, переопределять этот метод необязательно.
Посмотрим на пример грамотного переопределения этого метода и сразу реализуем IEquatable:

class Vehicle:IEquatable  < protected int speed; public int Speed < get < return this.speed; >set < this.speed = value; >> protected string name; public string Name < get < return this.name; >set < this.name = value; >> public Vehicle()<> public Vehicle(int speed, string name) < this.speed = speed; this.name = name; >public override bool Equals(object other) < //Последовательность проверки должна быть именно такой. //Если не проверить на null объект other, то other.GetType() может выбросить //NullReferenceException. if (other == null) return false; //Если ссылки указывают на один и тот же адрес, то их идентичность гарантирована. if (object.ReferenceEquals(this, other)) return true; //Если класс находится на вершине иерархии или просто не имеет наследников, то можно просто //сделать Vehicle tmp = other as Vehicle; if(tmp==null) return false; //Затем вызвать экземплярный метод, сразу передав ему объект tmp. if (this.GetType() != other.GetType()) return false; return this.Equals(other as Vehicle); >public bool Equals(Vehicle other) < if (other == null) return false; //Здесь сравнение по ссылкам необязательно. //Если вы уверены, что многие проверки на идентичность будут отсекаться на проверке по ссылке - //можно имплементировать. if (object.ReferenceEquals(this, other)) return true; //Если по логике проверки, экземпляры родительского класса и класса потомка могут считаться равными, //то проверять на идентичность необязательно и можно переходить сразу к сравниванию полей. if (this.GetType() != other.GetType()) return false; if (string.Compare(this.Name, other.Name, StringComparison.CurrentCulture) == 0 && this.speed.Equals(other.speed)) return true; else return false; >> 

Комментарий про вершину иерархии в переопределении виртуального метода сделан не просто так. Если создать наследник класса Vehicle (например, Bike), который также будет иметь переопределённый виртуальный метод Equals, в котором не будет сравнения типов по GetType, а будет попытка приведения типа Bike tmp = other as Bike; if(tmp!=null) this.Equals(tmp); то в таком случае, следующий код может вызывать проблемы:

Vehicle vehicle = new Vehicle(); Bike bike = new Bike(); object vehicleObj = vehicle; object bikeObject = bike; bike.Equals(vehicleObj); //Базовый тип не сможет привестись к наследнику. Таким образом, может быть //нарушено свойство симметричности сравнения объектов 
public static bool operator == (Foo left, Foo right)

Для значимых типов всегда следует переопределять, как и виртуальный Equals(). Для ссылочных типов лучше не переопределять, ибо, по умолчанию, от == на ссылочных типах ожидается поведение как у метода ReferenceEquals(). Так что, здесь всё просто.

IStructuralEquatable

IStructuralEquatable идёт рука об руку с интерфейсом IEqualityComparer. Интерфейс IStructuralEquatable реализуют такие классы как System.Array или System.Tuple. Как пишет Билл Вагнер, IStructuralEquality декларирует то, что тип может составлять более крупные объекты, которые имплементируют семантику значимых типов и вряд ли когда-либо нам потребуется его самостоятельно реализовывать. Хотя, что сложного в его реализации? Достаточно посмотреть на его реализацию в System.Array:

bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer) < if (other == null) < return false; >if (object.ReferenceEquals(this, other)) < return true; >Array array = other as Array; if (array == null || array.Length != this.Length) < return false; >for (int i = 0; i < array.Length; i++) < object value = this.GetValue(i); object value2 = array.GetValue(i); if (!comparer.Equals(value, value2)) < return false; >> return true; > 

Собственно, сначала проверяется тождественность объектов, затем производится приведение к одному типу и сравнение по длине. Если длинна равна, то тогда начинается поэлементное сравнение через делегирование ответственности за это сравнение интерфейсному (IEqualityComparer) методу Equals.

Вот, по сути, и всё, что можно сказать о сравнении объектов в C#.NET, но осталась ещё одна маленькая, но важная деталь: метод GetHashCode().

public virtual int GetHashCode()

В общем и целом, стандартная реализация этого метода ведёт себя как генератор уникального идентификатора. Минус такого подхода состоит в том, что одинаковые семантически объекты, могут возвращать разные hash-значения. Рихтер жалуется на то, что стандартная реализация ещё и низкопроизводительна. Грамотная реализация этого метода весьма проблематична. Необходимо высчитывать hash быстро и иметь большой разброс в результате, чтобы не случалось повторений на достаточно больших множествах. На самом деле, в большей части случаев, имплементации GetHashCode() донельзя простые. Везде производятся сдвиги, «побитовые или», или «исключающие или». Сам Рихтер приводит пример со структурой, имеющей два поля типа int. GetHashCode() он предлагает имплементировать примерно так:

internal sealed class Point < private int a; private int b; public override int GetHashCode() < return a ^ b; >> 

А вот как переопределён GetHasCode() в System.Char:

public override int GetHashCode()

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

При написании статьи были использованы небезызвестные источники:
J.Richter, CLR via C#
B. Wagner Effective C#
Также использовался свой опыт и источники в интернете, которые приводить особого смысла не имеет.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *