Про порівняння об'єктів за значенням — 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, second Person) =>
first.BirthDate == second.BirthDate &&
first.FirstName == second.FirstName &&
first.LastName == second.LastName;

public virtual bool Equals(other Person)
{
//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, second Person) =>
first?.Equals(second) ?? (object)first == (object)second;

public static bool operator ==(Person first, second Person) => Equals(first, second);

public static bool operator !=(Person first, second Person) => !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(other Person) => 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), який перекриває успадкований метод Equals(Object), і представляє собою виклик методу PersonEx.Equals(PersonEx), з приведенням вхідного об'єкта до типу PersonEx з допомогою оператора as.

Варто відзначити, що реалізація PersonEx.Equals(Object) не є обов'язковою, оскільки в разі її відсутності та виклику клієнтським кодом метод Equals(Object) зголосився б успадкований метод Person.Equals(Object), який всередині себе викликає віртуальний метод PersonEx.Equals(Person), що приводить до виклику PersonEx.Equals(PersonEx).
Проте, метод PersonEx.Equals(Object) реалізований для "повноти" коду і більшої швидкодії (за рахунок мінімізації кількості привидів типів і проміжних викликів методів).

Іншими словами, створюючи клас 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));
//PersonEx pex2 = new PersonEx("John", "Teddy", "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), операторів ==(PersonEx, PersonEx) і !=(PersonEx, PersonEx), або будь-якого з примірникових віртуальних методів Equals.

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

Більш того, реалізація в класі PersonEx метод Equals(PersonEx, PersonEx) і операторів ==(PersonEx, PersonEx) і !=(PersonEx, PersonEx), так само, як і для методу PersonEx.Equals(Object), є необов'язковою.
Метод Equals(PersonEx, PersonEx) і оператори ==(PersonEx, PersonEx) і !=(PersonEx, PersonEx) реалізовані для "повноти" коду і більшої швидкодії (за рахунок мінімізації кількості привидів типів і проміжних викликів методів).
Єдиним нестройным моментом в "полиморфности" статичних Equals, "==" і "!=" є те, що якщо два об'єкти типу Person або PersonEx призвести до типу object, порівняння об'єктів за допомогою операторів == і != буде вироблено за посиланням, а з допомогою методу Object.Equals(Object, Object) — за значенням. Але це — "by design" платформи.

У продовженні розглянемо особливості реалізації порівняння за значенням для об'єктів — примірників структур, а також поговоримо про кейсах, коли дійсно доцільно реалізовувати для своїх типів порівняння об'єктів за значенням, і як це робити, в т. ч. з предметної точки зору.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.