- PVSM.RU - https://www.pvsm.ru -
Эти доработки включают перекрытие методов Object.Equals(Object) [2] и Object.GetHashCode() [3].
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, созданный в предыдущей публикации [1], содержит следующую реализацию метода Equals(Object) [2]:
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 для возможности сравнения объектов по значению.
В соответствии с примером, приведенным в документации [2], приведение производится с помощью оператора as [4]. Проверим, дает ли это корректный результат.
Реализуем класс 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 [5], в противном случае метод возвратит false [6].
(При выполнении метода Equals, входящий объект, имеющий во время выполнения (runtime) тип PersonEx, будет успешно приведен к типу Person с помощью оператора as [4], и далее будет произведено сравнение объектов по значениям полей, имеющихся только в классе Person, и будет возвращен соответствующий результат.)
Очевидно, что с предметной точки зрения это неверное поведение:
Совпадение имени, фамилии и даты рождения не означает, что это одна и та же персона, т.к. у одной персоны отсутствует атрибут middle name (речь не о неопределенном значении атрибута, а об отсутствии самого атрибута), а у другой имеется атрибут middle name.
(Это разные типы сущностей.)
Если же, напротив, у объекта класса PersonEx вызвать метод Equals(Object) и передать в него объект класса Person, то метод Equals в любом случае возвратит false [6], независимо от значений свойств объектов.
(При выполнении метода Equals, входящий объект, имеющий во время выполнения (runtime) тип Person, не будет успешно приведен к типу PersonEx с помощью оператора as [4] — результатом приведения будет null [7], и метод возвратит false [6].)
Здесь мы наблюдаем верное с предметной точки зрения поведение, в отличие от предыдущего случая.
Эти виды поведения можно легко проверить, выполнив следующий код:
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) требованиям в документации [2], нежели корректность логики с предметной точки зрения.
А именно соответствие требованию:
x.Equals(y) returns the same value as y.Equals(x).
Это требование не выполняется.
(А с точки зрения здравого смысла, какие могут быть проблемы при текущей реализации Equals(Object)?
У разработчика типа данных нет информации, каким именно способом будут сравниваться объекты — x.Equals(y) или y.Equals(x) — как в клиентском коде (при явном вызове Equals), так и при помещении объектов в хеш-наборы (хеш-карты) [8] и словари [9] (внутри самих наборов/словарей).
В этом случае поведение программы будет недетерминировано, и зависеть от деталей реализации.)
На текущий момент представляется корректным способ, предложенный Джеффри Рихтером (Jeffrey Richter) в книге CLR via C# (Part II: Designing Types, Chapter 5: Primitive, Reference, and Value Types, Subchapter «Object Equality and Identity»), когда перед сравнением объектов непосредственно по значению, типы [10] объектов во время выполнения (runtime), полученные с помощью метода Object.GetType() [11] проверяются на равенство (вместо односторонних проверки/приведения типов объектов на совместимость с помощью оператора as [4]):
if (this.GetType() != obj.GetType())
return false;
Следует отметить, что данный способ не является однозначным, т.к. существует три различных способа проверки на равенство экземпляров класса Type [10], с теоретически различными результатами для одних и тех же операндов:
1. Согласно документации [11] к методу Object.GetType() [11]:
For two objects x and y that have identical runtime types, Object.ReferenceEquals(x.GetType(),y.GetType()) returns true.
Таким образом, объекты класса Type [10] можно проверить на равенство с помощью сравнения по ссылке:
bool isEqualTypes = (object)obj1.GetType() == (object)obj2.GetType();
или
bool isEqualTypes = Object.ReferenceEquals(obj1.GetType(), obj2.GetType());
2. Класс Type [10] имеет методы Equals(Object) [12] и Equals(Type) [13], поведение которых определено следующим образом:
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 [10] в общем случае может отличаться от сравнения объектов по ссылке, т.к. в случае использования методов Equals, сравниваются по ссылке не сами объекты класса Type [10], а их свойства UnderlyingSystemType [14], относящиеся к тому же классу.
Однако, из описания методов Equals класса Type.Equals(Object) [12] представляется, что они не предназначены для сравнения непосредственно объектов класса Type.
Примечание:
Для метода Type.Equals(Object) [12] проблема несоответствия требованию (как следствие использования оператора as [4])
x.Equals(y) returns the same value as y.Equals(x).
не возникнет, т.к. класс Type [10] — абстрактный, если только в потомках класса метод не будет перекрыт некорректным образом.
Для предотвращения этой потенциальной проблемы, возможно, стоило объявить метод как sealed [15].
3. Класс Type [10], начиная с .NET Framework 4.0, имеет перегруженные операторы == [16] или != [17], поведение которых описывается простым образом, без описания деталей реализации:
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 [10], представляется, что наиболее корректным способом сравнения объектов будет использование операторов "==" и "!=", и, в зависимости от используемой версии .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) [2] будет соблюдаться:
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):
В продолжении мы рассмотрим реализацию интерфейса IEquatable(Of T) [18] и type-specific метода IEquatable(Of T).Equals(T) [19], перегрузку операторов равенства и неравенства для сравнения объектов по значению, и найдем способ наиболее компактно, согласованно и производительно реализовать в одном классе все виды проверок по значению.
Метод Uri.Equals(Object) [20]:
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) [2] не выполняется:
x.Equals(y) returns the same value as y.Equals(x).
т.к. класс String [21] и метод String.Equals(Object) [22], в свою очередь, не «знают» о существовании класса Uri [23].
Это легко проверить на практике, выполнив код:
const string uriString = "https://www.habrahabr.ru";
Uri uri = new Uri(uriString);
bool isSameUri = uri.Equals(uriString);
bool isSameUri2 = uriString.Equals(uri);
Автор: sand14
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/206851
Ссылки в тексте:
[1] предыдущей публикации: https://habrahabr.ru/post/314328/
[2] Object.Equals(Object): https://msdn.microsoft.com/library/bsc2ak47.aspx
[3] Object.GetHashCode(): https://msdn.microsoft.com/library/system.object.gethashcode.aspx
[4] as: https://msdn.microsoft.com/library/cscsdfbt.aspx
[5] true: https://msdn.microsoft.com/library/6x6y6z4d.aspx
[6] false: https://msdn.microsoft.com/library/67bxt5ee.aspx
[7] null: https://msdn.microsoft.com/library/edakx9da.aspx
[8] хеш-наборы (хеш-карты): https://msdn.microsoft.com/library/bb359438.aspx
[9] словари: https://msdn.microsoft.com/library/xfhwa508.aspx
[10] типы: https://msdn.microsoft.com/library/system.type.aspx
[11] Object.GetType(): https://msdn.microsoft.com/library/system.object.gettype.aspx
[12] Equals(Object): https://msdn.microsoft.com/library/hh32dc75.aspx
[13] Equals(Type): https://msdn.microsoft.com/library/3ahwab82.aspx
[14] UnderlyingSystemType: https://msdn.microsoft.com/library/system.type.underlyingsystemtype.aspx
[15] sealed: https://msdn.microsoft.com/library/88c54tsw.aspx
[16] ==: https://msdn.microsoft.com/library/system.type.op_equality.aspx
[17] !=: https://msdn.microsoft.com/library/system.type.op_inequality.aspx
[18] IEquatable(Of T): https://msdn.microsoft.com/library/ms131187.aspx
[19] IEquatable(Of T).Equals(T): https://msdn.microsoft.com/library/ms131190.aspx
[20] Uri.Equals(Object): https://msdn.microsoft.com/library/f83xtf15.aspx
[21] String: https://msdn.microsoft.com/library/system.string.aspx
[22] String.Equals(Object): https://msdn.microsoft.com/library/fkfd9eh8.aspx
[23] Uri: https://msdn.microsoft.com/library/system.uri.aspx
[24] Источник: https://habrahabr.ru/post/314500/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.