В предыдущей публикации мы рассмотрели общие принципы реализации минимально необходимых доработок класса для возможности сравнения объектов класса по значению с помощью стандартной инфраструктуры платформы .NET.
Эти доработки включают перекрытие методов Object.Equals(Object) и Object.GetHashCode().
Остановимся подробнее на особенностях реализации метода Object.Equals(Object) для соответствия следующему требованию в документации:
x.Equals(y) returns the same value as y.Equals(x).
// и, как следствие, следующему:
If (x.Equals(y) && y.Equals(z)) returns true, then x.Equals(z) returns true.
Класс Person, созданный в предыдущей публикации, содержит следующую реализацию метода Equals(Object):
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 для возможности сравнения объектов по значению.
В соответствии с примером, приведенным в документации, приведение производится с помощью оператора as. Проверим, дает ли это корректный результат.
Реализуем класс PersonEx, унаследовав класс Person, добавив в персональные данные свойство Middle Name, и перекрыв соответствующим образом методы Person.Equals(Object) и Person.GetHashCode().
Класс 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); } } }
Легко заметить, что если у объекта класса Person вызвать метод Equals(Object) и передать в него объект класса PersonEx, то, если у этих объектов (персон) совпадают имя, фамилия и дата рождения, метод Equals возвратит true, в противном случае метод возвратит false.
(При выполнении метода Equals, входящий объект, имеющий во время выполнения (runtime) тип PersonEx, будет успешно приведен к типу Person с помощью оператора as, и далее будет произведено сравнение объектов по значениям полей, имеющихся только в классе Person, и будет возвращен соответствующий результат.)
Очевидно, что с предметной точки зрения это неверное поведение:
Совпадение имени, фамилии и даты рождения не означает, что это одна и та же персона, т.к. у одной персоны отсутствует атрибут middle name (речь не о неопределенном значении атрибута, а об отсутствии самого атрибута), а у другой имеется атрибут middle name.
(Это разные типы сущностей.)
Если же, напротив, у объекта класса PersonEx вызвать метод Equals(Object) и передать в него объект класса Person, то метод Equals в любом случае возвратит false, независимо от значений свойств объектов.
(При выполнении метода Equals, входящий объект, имеющий во время выполнения (runtime) тип Person, не будет успешно приведен к типу PersonEx с помощью оператора as — результатом приведения будет null, и метод возвратит false.)
Здесь мы наблюдаем верное с предметной точки зрения поведение, в отличие от предыдущего случая.
Эти виды поведения можно легко проверить, выполнив следующий код:
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), обеспечив ожидаемое поведение.
На текущий момент представляется корректным способ, предложенный Джеффри Рихтером (Jeffrey Richter) в книге CLR via C# (Part II: Designing Types, Chapter 5: Primitive, Reference, and Value Types, Subchapter «Object Equality and Identity»), когда перед сравнением объектов непосредственно по значению, типы объектов во время выполнения (runtime), полученные с помощью метода Object.GetType() проверяются на равенство (вместо односторонних проверки/приведения типов объектов на совместимость с помощью оператора as):
if (this.GetType() != obj.GetType()) return false;
Следует отметить, что данный способ не является однозначным, т.к. существует три различных способа проверки на равенство экземпляров класса Type, с теоретически различными результатами для одних и тех же операндов:
1. Согласно документации к методу Object.GetType():
For two objects x and y that have identical runtime types, Object.ReferenceEquals(x.GetType(),y.GetType()) returns true.
Таким образом, объекты класса Type можно проверить на равенство с помощью сравнения по ссылке:
bool isEqualTypes = (object)obj1.GetType() == (object)obj2.GetType();
или
bool isEqualTypes = Object.ReferenceEquals(obj1.GetType(), obj2.GetType());
2. Класс Type имеет методы Equals(Object) и Equals(Type), поведение которых определено следующим образом:
Determines if the underlying system type of the current Type object is the same as the underlying system type of the specified Object.
Return Value
Type: System.Boolean
true if the underlying system type of o is the same as the underlying system type of the current Type; otherwise, false. This method also returns false if:
o is null.
o cannot be cast or converted to a Type object.Remarks
This method overrides Object.Equals. It casts o to an object of type Type and calls the Type.Equals(Type) method.
и
Determines if the underlying system type of the current Type is the same as the underlying system type of the specified Type.
Return Value
Type: System.Boolean
true if the underlying system type of o is the same as the underlying system type of the current Type; otherwise, false.
Внутри эти методы реализованы следующим образом:
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 для объектов класса Type в общем случае может отличаться от сравнения объектов по ссылке, т.к. в случае использования методов Equals, сравниваются по ссылке не сами объекты класса Type, а их свойства UnderlyingSystemType, относящиеся к тому же классу.
Однако, из описания методов Equals класса Type.Equals(Object) представляется, что они не предназначены для сравнения непосредственно объектов класса Type.
Примечание:
Для метода Type.Equals(Object) проблема несоответствия требованию (как следствие использования оператора as)
x.Equals(y) returns the same value as y.Equals(x).
не возникнет, т.к. класс Type — абстрактный, если только в потомках класса метод не будет перекрыт некорректным образом.
Для предотвращения этой потенциальной проблемы, возможно, стоило объявить метод как sealed.
3. Класс Type, начиная с .NET Framework 4.0, имеет перегруженные операторы == или !=, поведение которых описывается простым образом, без описания деталей реализации:
Indicates whether two Type objects are equal.
Return Value
Type: System.Boolean
true if left is equal to right; otherwise, false.
и
Indicates whether two Type objects are not equal.
Return Value
Type: System.Boolean
true if left is not equal to right; otherwise, false.
Изучение исходных кодов тоже не дает информации по деталям реализации, для выяснения внутренней логики операторов:
public static extern bool operator ==(Type left, Type right);
public static extern bool operator !=(Type left, Type right);
Исходя из анализа трех документированных способов сравнения объектов класса Type, представляется, что наиболее корректным способом сравнения объектов будет использование операторов "==" и "!=", и, в зависимости от используемой версии .NET при сборке, исходный код будет собран либо с использованием сравнение по ссылке (идентично первому варианту), либо с использованием перегруженных операторов "==" и "!=".
Реализуем классы Person и PersonEx соответствующим образом:
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); } } }
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); } } }
Теперь следующее требование к реализации метода Equals(Object) будет соблюдаться:
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):
- вначале проверяются на равенство ссылки, указывающие на текущий и входящий объекты, и, в случае совпадения ссылок, возвращается true;
- затем проверяется на null ссылка на входящий объект, и, в случае положительного результата проверки, возвращается false;
- затем проверяется идентичность типов текущего и входящего объекта, и, в случае отрицательного результата проверки, возвращается false;
- на последнем этапе производятся приведение входящего объекта к типу данного класса и непосредственно сравнение объектов по значению.
Таким образом, мы нашли оптимальный способ реализации ожидаемого поведения метода Equals(Object).
В продолжении мы рассмотрим реализацию интерфейса IEquatable(Of T) и type-specific метода IEquatable(Of T).Equals(T), перегрузку операторов равенства и неравенства для сравнения объектов по значению, и найдем способ наиболее компактно, согласованно и производительно реализовать в одном классе все виды проверок по значению.
P.S. А на десерт проверим корректность реализации Equals(Object) в стандартной библиотеке.
Метод Uri.Equals(Object):
Compares two Uri instances for equality.
Syntax
public override bool Equals(object comparand)Parameters
comparand
Type: System.Object
The Uri instance or a URI identifier to compare with the current instance.Return Value
Type: System.Boolean
A Boolean value that is true if the two instances represent the same URI; otherwise, false.
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 ... }
Логично предположить, что следующее требование к реализации метода Equals(Object) не выполняется:
x.Equals(y) returns the same value as y.Equals(x).
т.к. класс String и метод String.Equals(Object), в свою очередь, не «знают» о существовании класса Uri.
Это легко проверить на практике, выполнив код:
const string uriString = "https://www.habrahabr.ru"; Uri uri = new Uri(uriString); bool isSameUri = uri.Equals(uriString); bool isSameUri2 = uriString.Equals(uri);
ссылка на оригинал статьи https://habrahabr.ru/post/314500/
Добавить комментарий