О Сравнении Объектов По Значению – 2, Или Особенности Реализации Метода Equals

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

NET. Эти улучшения включают перекрывающиеся методы.

Объект.Равно(Объект) И Объект.GetHashCode() .

Остановимся подробнее на особенностях реализации метода.

Объект.Равно(Объект) обеспечить соответствие следующему требованию документации:

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  x.Equals(y) returns the same value as y.Equals(x).

Класс Person, созданный в предыдущая публикация , содержит следующую реализацию метода Равно(Объект) : Person.Equals(Объект) public override bool Equals(object obj) { if ((object)this == obj) return true; var other = obj as Person; if ((object)other == null) return false; return EqualsHelper(this, other); } После проверки ссылочного равенства текущего и входящего объектов, в случае отрицательного результата проверки, входящий объект приводится к типу Person, чтобы иметь возможность сравнивать объекты по значению.

Согласно примеру, приведенному в документация , приведение выполняется с помощью оператора как .

Давайте проверим, дает ли это правильный результат. Давайте реализуем класс PersonEx, унаследовав класс Person, добавив свойство Middle Name к личным данным и переопределив методы Person.Equals(Object) и Person.GetHashCode() соответственно.

Класс PersonEx: класс PersonEx using System;

namespace HelloEquatable

{

public class PersonEx : Person

{

public string MiddleName { get; }

public PersonEx(

string firstName, string middleName, string lastName, DateTime? birthDate

) : base(firstName, lastName, birthDate)

{

this.MiddleName = NormalizeName(middleName);

}

public override int GetHashCode() =>

base.GetHashCode() ^

this.MiddleName.GetHashCode();

protected static bool EqualsHelper(PersonEx first, PersonEx second) =>

EqualsHelper((Person)first, (Person)second) &&

first.MiddleName == second.MiddleName;

public override bool Equals(object obj)

{

if ((object)this == obj)

return true;

var other = obj as PersonEx;

if ((object)other == null)

return false;

return EqualsHelper(this, other);

}

}

}
Легко увидеть, что если вызвать метод Equals(Object) для объекта класса Person и передать ему объект класса PersonEx, то если эти объекты (персоны) будут иметь одинаковые имя, фамилию и дату рождения, метод Equals вернет истинный , в противном случае метод вернет ЛОЖЬ .

(При выполнении метода Equals входной объект, имеющий тип времени выполнения PersonEx, будет успешно приведен к типу Person с помощью оператора как , а затем объекты будут сравниваться на основе значений полей, найденных только в классе Person, и будет возвращен соответствующий результат.)

Очевидно, что с предметной точки зрения это неправильное поведение:

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

(Это разные типы сущностей.

) Если, наоборот, вызвать метод Equals(Object) на объекте класса PersonEx и передать ему объект класса Person, то метод Equals в любом случае вернет ЛОЖЬ , независимо от значений свойств объектов.

(При выполнении метода Equals входной объект типа Person во время выполнения не будет успешно приведен к типу PersonEx с помощью оператора как - результат приведения будет нулевой , и метод вернет ЛОЖЬ .

) Здесь мы наблюдаем правильное с субъектной точки зрения поведение, в отличие от предыдущего случая.

Такое поведение можно легко протестировать, запустив следующий код: Код var person = new Person("John", "Smith", new DateTime(1990, 1, 1)); var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); bool isSamePerson = person.Equals(personEx); bool isSamePerson2 = personEx.Equals(person); Однако в контексте данной публикации нас больше интересует соответствие реализованного поведения Equals(Object) требованиям в документация , а не правильность логики с предметной точки зрения.

А именно, соблюдение требования: x.Equals(y) returns the same value as y.Equals(x).

Это требование не выполняется.



(А с точки зрения здравого смысла, какие проблемы могут возникнуть в текущей реализации Equals(Object)?

Разработчик типа данных не имеет информации о том, как именно будут сравниваться объекты — x.Equals(y) или y.Equals(x) — как в клиентском коде (при явном вызове Equals), так и при размещении объектов в хеш-наборы (хеш-карты) И словари (внутри самих наборов/словарей).

В этом случае поведение программы будет недетерминированным и будет зависеть от деталей реализации.

)



Давайте посмотрим, как именно можно реализовать метод Equals(Object), чтобы обеспечить ожидаемое поведение.



На данный момент представляется правильным использовать метод, предложенный Джеффри Рихтером в книге CLR через C# (Часть II «Проектирование типов», Глава 5 «Примитивные, ссылочные и значащие типы», подраздел «Равенство и идентичность объектов»), когда перед сравнением объектов напрямую по значению, типы объекты времени выполнения, полученные с помощью метода Объект.ПолучитьТип() проверяются на равенство (вместо односторонней проверки совместимости и приведения типов объектов с помощью оператора как ): if (this.GetType() != obj.GetType()) return false; Следует отметить, что использование этого метода не является однозначным, т.к.

существует три разных способа проверки равенства экземпляров класса.

Тип , с теоретически разными результатами для одних и тех же операндов: 1. Согласно документация к методу Объект.ПолучитьТип() : For two objects x and y that have identical runtime types, Object.ReferenceEquals(x.GetType(),y.GetType()) returns true. Таким образом, объекты класса Тип Проверить равенство можно с помощью сравнения по ссылке: bool isSameType = (object)obj1.GetType() == (object)obj2.GetType(); или bool isSameType = Object.ReferenceEquals(obj1.GetType(), obj2.GetType()); 2. Класс Тип имеет методы Равно(Объект) И Равно (Тип) , поведение которого определяется следующим образом:

Определяет, совпадает ли базовый тип системы текущего объекта Type с базовым типом системы указанного объекта.

Возвращаемое значение Тип: System.Boolean true, если базовый тип системы o такой же, как базовый тип системы текущего Type; в противном случае ложь.

Этот метод также возвращает false, если: о равно нулю.

o нельзя привести или преобразовать в объект Type. Примечания Этот метод переопределяет Object.Equals. Он приводит o к объекту типа Type и вызывает метод Type.Equals(Type).

И
Определяет, совпадает ли базовый тип системы текущего типа с базовым типом системы указанного типа.

Возвращаемое значение Тип: System.Boolean true, если базовый тип системы o такой же, как базовый тип системы текущего Type; в противном случае ложь.

Внутренне эти методы реализованы следующим образом: public override bool Equals(Object o) { if (o == null) return false; return Equals(o as Type); } И public virtual bool Equals(Type o) { if ((object)o == null) return false; return (Object.ReferenceEquals(this.UnderlyingSystemType, o.UnderlyingSystemType)); } Как видите, результат выполнения обоих методов Equals для объектов класса Тип в целом оно может отличаться от сравнения объектов по ссылке, поскольку в случае использования методов Equals по ссылке сравниваются не сами объекты класса Тип и их свойства Базовый тип системы принадлежность к одному и тому же классу.

Однако из описания методов класса Equals Тип.

Равно(Объект) Похоже, они не предназначены для прямого сравнения объектов класса Type. Примечание: Для метода Тип.

Равно(Объект) проблема невыполнения требования (как следствие использования оператора как ) x.Equals(y) returns the same value as y.Equals(x).

не произойдет, если только у потомков класса Тип метод не будет переопределен неправильно.

Чтобы предотвратить эту потенциальную проблему, разработчикам было бы целесообразно объявить метод как запечатанный .

3. Класс Тип начиная с .

NET Framework 4.0, имеет перегрузки операторов == или != , поведение которого описано просто, без описания деталей реализации:

Указывает, равны ли два объекта Type. Возвращаемое значение Тип: System.Boolean true, если лево равно правому; в противном случае ложь.

И
Указывает, не равны ли два объекта Type. Возвращаемое значение Тип: System.Boolean true, если левое не равно правому; в противном случае ложь.

Изучение исходных кодов также не дает информации о деталях реализации для выяснения внутренней логики операторов: public static extern bool operator ==(Type left, Type right); public static extern bool operator !=(Type left, Type right); На основе анализа трех документированных способов сравнения объектов классов.

Тип , кажется, что наиболее правильным способом сравнения объектов будет использование операторов "==" и "!=".

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

== И != .

Давайте реализуем классы Person и PersonEx соответственно: класс Person (с новым методом Equals) using System; namespace HelloEquatable { public class Person { protected static string NormalizeName(string name) => name?.

Trim() ?? string.Empty; protected static DateTime? NormalizeDate(DateTime? date) => date?.



Date;

public string FirstName { get; }

public string LastName { get; }

public DateTime? BirthDate { get; }

public Person(string firstName, string lastName, DateTime? birthDate)

{

this.FirstName = NormalizeName(firstName);

this.LastName = NormalizeName(lastName);

this.BirthDate = NormalizeDate(birthDate);

}

public override int GetHashCode() =>

this.FirstName.GetHashCode() ^

this.LastName.GetHashCode() ^

this.BirthDate.GetHashCode();

protected static bool EqualsHelper(Person first, Person second) =>

first.BirthDate == second.BirthDate &&

first.FirstName == second.FirstName &&

first.LastName == second.LastName;

public override bool Equals(object obj)

{

if ((object)this == obj)

return true;

if (obj == null)

return false;

if (this.GetType() != obj.GetType())

return false;

return EqualsHelper(this, (Person)obj);

}

}

}

класс PersonEx (с новым методом Equals) using System;

namespace HelloEquatable

{

public class PersonEx : Person

{

public string MiddleName { get; }

public PersonEx(

string firstName, string middleName, string lastName, DateTime? birthDate

) : base(firstName, lastName, birthDate)

{

this.MiddleName = NormalizeName(middleName);

}

public override int GetHashCode() =>

base.GetHashCode() ^

this.MiddleName.GetHashCode();

protected static bool EqualsHelper(PersonEx first, PersonEx second) =>

EqualsHelper((Person)first, (Person)second) &&

first.MiddleName == second.MiddleName;

public override bool Equals(object obj)

{

if ((object)this == obj)

return true;

if (obj == null)

return false;

if (this.GetType() != obj.GetType())

return false;

return EqualsHelper(this, (PersonEx)obj);

}

}

}

Теперь следующим требованием для реализации метода является Равно(Объект) будет наблюдаться: x.Equals(y) returns the same value as y.Equals(x).

что можно легко проверить, запустив код: Код var person = new Person("John", "Smith", new DateTime(1990, 1, 1)); var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); bool isSamePerson = person.Equals(personEx); bool isSamePerson2 = personEx.Equals(person); Замечания по реализации метода Equals(Object):

  1. сначала ссылки, указывающие на текущий и входящий объекты, проверяются на равенство и, если ссылки совпадают, возвращается истинный ;
  2. затем проверил нулевой ссылка на входящий объект и, если проверка положительна, она возвращается ЛОЖЬ ;
  3. затем проверяется идентичность типов текущего и входящего объекта и, если проверка отрицательная, он возвращается ЛОЖЬ ;
  4. на последнем этапе входящий объект приводится к типу этого класса и объекты напрямую сравниваются по значению.



Таким образом, мы нашли оптимальный способ реализации ожидаемого поведения метода.

Равно(Объект) .

На десерт проверим корректность реализации Равно(Объект) в стандартной библиотеке.

Метод Uri.Equals(Объект) :

Сравнивает два экземпляра Uri на предмет равенства.

Синтаксис публичное переопределение bool Equals (сравниваемый объект) Параметры сравнивать Тип: Системный.

Объект Экземпляр Uri или идентификатор URI для сравнения с текущим экземпляром.

Возвращаемое значение Тип: System.Boolean Логическое значение, имеющее значение true, если два экземпляра представляют один и тот же URI; в противном случае ложь.

Uri.Equals(Объект) public override bool Equals(object comparand)

{

if ((object)comparand == null)

{

return false;

}

if ((object)this == (object)comparand)

{

return true;

}

Uri obj = comparand as Uri;

//

// we allow comparisons of Uri and String objects only. If a string

// is passed, convert to Uri. This is inefficient, but allows us to

// canonicalize the comparand, making comparison possible

//

if ((object)obj == null)

{

string s = comparand as string;

if ((object)s == null)

return false;

if (!TryCreate(s, UriKind.RelativeOrAbsolute, out obj))

return false;

}

// method code .

}

Логично предположить, что следующее требование к реализации метода Равно(Объект) не выполнено: x.Equals(y) returns the same value as y.Equals(x).

Потому что класс Нить и метод String.Equals(Объект) , в свою очередь, не «знают» о существовании класса Ури .

В этом можно легко убедиться на практике, запустив код: Код const string uriString = " https://www.habrahabr.ru "; Uri uri = new Uri(uriString); bool isSameUri = uri.Equals(uriString); bool isSameUri2 = uriString.Equals(uri);



В продолжение мы посмотрим на реализацию интерфейса IEquatable(Of T) и типоспецифичный метод IEquatable(Of T).

Равно(T)



, перегрузив операторы равенства и неравенства для сравнения объектов по значению, и найти способ наиболее компактно, последовательно и эффективно реализовать все виды проверок по значению в одном классе.

Теги: #C++ #.

NET #равенство объектов #GetHashCode #GetHashCode #GetHashCode #equals #IEquatable #Equals(T) #операторы равенства #программирование #Совершенный код #.

NET #проектирование и рефакторинг #C++

Вместе с данным постом часто просматривают: