Programming
Perfect code
.NET
Designing and refactoring
C#
November 2016 17

О сравнении объектов по значению — 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 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:


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() => 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" платформы.


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


+5
6.4k 38
Comments 7
Top of the day