О сравнении объектов по значению — 4, или Inheritance & Equality operators

от автора

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

  • перекрытие методов Object.GetHashCode(), Object.Equals(Object);
  • реализацию интерфейса IEquatable (Of T);
  • реализацию Type-specific статических метода Equals(Person, Person) и операторов ==(Person, Person), !=(Person, Person).

Каждый из способов сравнения для любой одной и той же пары объектов возвращает один и тот же результат:

Пример кода

Person p1 = new Person("John", "Smith", new DateTime(1990, 1, 1)); Person p2 = new Person("John", "Smith", new DateTime(1990, 1, 1)); //Person p2 = new Person("Robert", "Smith", new DateTime(1991, 1, 1));  object o1 = p1; object o2 = p2;  bool isSamePerson;  isSamePerson = o1.Equals(o2); isSamePerson = p1.Equals(p2); isSamePerson = object.Equals(o1, o2); isSamePerson = Person.Equals(p1, p2); isSamePerson = p1 == p2; isSamePerson = !(p1 == p2);

При этом, каждый из способов сравнения является коммуникативным:
x.Equals(y) возвращает тот же результат, что и y.Equals(x), и т.д.

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

Однако, требует раскрытия вопрос:
Как именно обеспечить детерминированность результата в случае наследования классов и использования статических методов и операторов сравнения — с учетом того, что статические методы и операторы не обладают полиморфным поведением.

Для наглядности приведем класс Person из предыдущей публикации:

class Person

using System;  namespace HelloEquatable {     public class Person : IEquatable<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 virtual bool Equals(Person other)         {             //if ((object)this == null)             //    throw new InvalidOperationException("This is null.");              if ((object)this == (object)other)                 return true;              if ((object)other == null)                 return false;              if (this.GetType() != other.GetType())                 return false;              return EqualsHelper(this, other);         }          public override bool Equals(object obj) => this.Equals(obj as Person);          public static bool Equals(Person first, Person second) =>             first?.Equals(second) ?? (object)first == (object)second;          public static bool operator ==(Person first, Person second) => Equals(first, second);          public static bool operator !=(Person first, Person second) => !Equals(first, second);     } }

И создадим класс-наследник PersonEx:

class PersonEx

using System;  namespace HelloEquatable {     public class PersonEx : Person, IEquatable<PersonEx>     {         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 virtual bool Equals(PersonEx other)         {             //if ((object)this == null)             //    throw new InvalidOperationException("This is null.");              if ((object)this == (object)other)                 return true;              if ((object)other == null)                 return false;              if (this.GetType() != other.GetType())                 return false;              return EqualsHelper(this, other);         }          public override bool Equals(Person other) => this.Equals(other as PersonEx);          // Optional overloadings:          public override bool Equals(object obj) => this.Equals(obj as PersonEx);          public static bool Equals(PersonEx first, PersonEx second) =>             first?.Equals(second) ?? (object)first == (object)second;          public static bool operator ==(PersonEx first, PersonEx second) => Equals(first, second);          public static bool operator !=(PersonEx first, PersonEx second) => !Equals(first, second);     } }

В классе-наследнике появилось еще одно ключевое свойство MiddleName. Поэтому первым делом необходимо:

  • Реализовать интерфейс IEquatable(Of PersonEx).
  • Реализовать метод PersonEx.Equals(Person), перекрыв унаследованный метод Person.Equals(Person) (стоит обратить внимание, что последний изначально был объявлен виртуальным для учета возможности наследования) и попытавшись привести объект типа Person к типу PersonEx.

(В противном случае, сравнение объектов, у которых равны все ключевые поля, кроме MiddleName, возвратит результат "объекты равны", что неверно с предметной точки зрения.)

При этом:

  • Реализация метода PersonEx.Equals(PersonEx) аналогична реализации метода Person.Equals(Person).
  • Реализация метода PersonEx.Equals(Person) аналогична реализации метода Person.Equals(Object).
  • Реализация статического protected-метода EqualsHelper(PersonEx, PersonEx) аналогична реализации метода EqualsHelper(Person, Person) (для повторного использования кода, второй метод используется первым).

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

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

Другими словами, создавая класс PersonEx, наследуя при этом класс Person, мы поступали таким же образом, как при создании класса Person, наследуя класс Object.

Теперь, какой бы метод у объекта класса PersonEx мы не вызывали:
Equals(PersonEx), Equals(Person), Equals(object),
для любой одной и той же пары объектов будет возвращаться один и тот же результат;
при смене операндов местами так же будет возвращаться тот же самый результат.
Обеспечить такое поведение позволяет полиморфизм.

Также мы реализовали у класса PersonEx статический метод PersonEx.Equals(PersonEx, PersonEx) и соответствующие ему операторы сравнения PersonEx.==(PersonEx, PersonEx) и PersonEx.!=(PersonEx, PersonEx), также действуя таким же образом, как и при при создании класса Person.

Использование метода PersonEx.Equals(PersonEx, PersonEx) или операторов PersonEx.==(PersonEx, PersonEx) и PersonEx.!=(PersonEx, PersonEx) для любой одной и той же пары объектов даст тот же результат, что и использование экземплярных методов Equals класса PersonEx.

А вот дальше становится интереснее.
Ведь класс PersonEx "унаследовал" от класса Person статический метод Equals(Person, Person) и соответствующие ему операторы сравнения ==(Person, Person) и !=(Person, Person).

Какой результат будет получен, если выполнить такой код?

Код

bool isSamePerson;  PersonEx pex1 = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); PersonEx pex2 = new PersonEx("John", "Bobby", "Smith", new DateTime(1990, 1, 1)); Person p1 = pex1; Person p2 = pex2;  isSamePerson = Person.Equals(pex1, pex2); isSamePerson = PersonEx.Equals(p1, p2); isSamePerson = pex1 == pex2; isSamePerson = p1 == p2;

Несмотря на то, что метод Equals(Person, Person) и операторы сравнения ==(Person, Person) и !=(Person, Person) — статические, результат всегда будет тем же самым, что и при вызове метода Equals(PersonEx, PersonEx), операторов ==(Person, Person) и !=(Person, Person), или любого из экземплярных виртуальных методов Equals.

Именно для получения такого полиморфного поведения, статические методы Equals и операторы сравнения "==" и "!=", на каждом из этапов наследования реализуются с помощью экземплярного виртуального метода Equals.

Более того, в классе PersonEx реализация метода Equals(PersonEx, PersonEx) и операторов ==(Person, Person) и !=(Person, Person), так же, как и для метода PersonEx.Equals(Object), является опциональной, и реализована для "полноты" кода и большего быстродействия (за счет минимизации количества приведений типов и промежуточных вызовов методов).

Единственным нестройным моментом в "полиморфности" статических Equals, "==" и "!=" является то, что если два объекта типа Person или PersonEx привести к типу object, то сравнение объектов с помощью операторов == и != будет произведено по ссылке, а с помощью метода Object.Equals(Object, Object) — по значению.
Но это — "by design" платформы.

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

ссылка на оригинал статьи https://habrahabr.ru/post/315258/


Комментарии

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

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