В предыдущей публикации мы получили вариант реализации сравнения объектов по значению для платформы .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 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:
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/
Добавить комментарий