В предыдущей публикации мы получили вариант реализации сравнения объектов по значению для платформы .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 из предыдущей публикации:
using System;
namespace HelloEquatable
{
public class Person : IEquatable<Person>
{
protected static int GetHashCodeHelper(int[] subCodes)
{
if ((object)subCodes == null || subCodes.Length == 0)
return 0;
int result = subCodes[0];
for (int i = 1; i < subCodes.Length; i++)
result = unchecked(result * 397) ^ subCodes[i];
return result;
}
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() => GetHashCodeHelper(
new int[]
{
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:
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() => GetHashCodeHelper(
new int[]
{
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), перекрывающий унаследованный метод 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" платформы.