В предыдущая публикация Мы рассмотрели общие принципы реализации минимально необходимых модификаций класса, чтобы иметь возможность сравнивать объекты класса по значению, используя стандартную инфраструктуру платформы .
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;
Легко увидеть, что если вызвать метод Equals(Object) для объекта класса Person и передать ему объект класса PersonEx, то если эти объекты (персоны) будут иметь одинаковые имя, фамилию и дату рождения, метод Equals вернет истинный , в противном случае метод вернет ЛОЖЬ .
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 входной объект, имеющий тип времени выполнения 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):
- сначала ссылки, указывающие на текущий и входящий объекты, проверяются на равенство и, если ссылки совпадают, возвращается истинный ;
- затем проверил нулевой ссылка на входящий объект и, если проверка положительна, она возвращается ЛОЖЬ ;
- затем проверяется идентичность типов текущего и входящего объекта и, если проверка отрицательная, он возвращается ЛОЖЬ ;
- на последнем этапе входящий объект приводится к типу этого класса и объекты напрямую сравниваются по значению.
Таким образом, мы нашли оптимальный способ реализации ожидаемого поведения метода.
На десерт проверим корректность реализации Равно(Объект) в стандартной библиотеке.
Метод Uri.Equals(Объект) :
Сравнивает два экземпляра Uri на предмет равенства.Uri.Equals(Объект)Синтаксис публичное переопределение bool Equals (сравниваемый объект) Параметры сравнивать Тип: Системный.
Объект Экземпляр Uri или идентификатор URI для сравнения с текущим экземпляром.
Возвращаемое значение Тип: System.Boolean Логическое значение, имеющее значение true, если два экземпляра представляют один и тот же URI; в противном случае ложь.
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).
, перегрузив операторы равенства и неравенства для сравнения объектов по значению, и найти способ наиболее компактно, последовательно и эффективно реализовать все виды проверок по значению в одном классе.
Теги: #C++ #.
NET #равенство объектов #GetHashCode #GetHashCode #GetHashCode #equals #IEquatable #Equals(T) #операторы равенства #программирование #Совершенный код #.
NET #проектирование и рефакторинг #C++
-
Скорость Флешек (Usb-Флешка)
19 Dec, 24 -
Зам-С — Выпуск №52
19 Dec, 24 -
Распознавание Лиц Для Удовольствия
19 Dec, 24