Ранее мы рассмотрели корректную реализации минимально необходимого набора доработок класса для сравнения объектов класса по значению.
Теперь рассмотрим Type-specific реализацию сравнения объектов по значению, включающую реализацию Generic-интерфейса IEquatable(Of T) и перегрузку операторов == и !=.
Type-specific сравнение объектов по значению позволяет достичь:
- Более стабильного, масштабируемого и мнемонического (читаемого) кода (последнее за счет перегруженных операторов).
- Более высокой производительности.
Кроме того, реализация Type-specific сравнения по значению необходима по причинам:
- Стандартные Generic коллекции (List(Ot T), LinkedList(Of T), Dictionary(Of TKey, TValue) и др.) требуют реализацию IEquatable(Of T) всех объектов, для хранимых в коллекциях.
- Стандартный компаратор-по-значению EqualityComparer(Of T) по умолчанию использует реализацию IEquatable(Of T) у операндов.
Реализация одновременно всех способов сравнения сопряжена определенными с трудностями, если требуется обеспечить:
- Соответствие результатов сравнения у различных способов.
- Сохранение поведения при наследовании.
- Учет того, что операторы технически являются статическими методами и, соответственно, у них отсутствует полиморфность поведения (а также, что не все CLS-совместимые языки поддерживают операторы или их перегрузку).
- Минимизацию copy-paste и общего объема кода.
Рассмотрим реализацию сравнения объектов по значению с учетом вышеизложенных условий, на примере класса Person.
Сразу приведем окончательный варианта кода с пояснениями, почему это сделано именно так, и как именно это работает.
(Демонстрация вывода решения с учетом каждого нюанса дает слишком много итераций.)
Итак, класс 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); } }
-
Метод Person.GetHashCode() вычисляет хеш-код объекта, основываясь на полях, сочетание которых образует уникальность значения конкретного объекта.
Особенности вычисления хеш-кодов и требования к перекрытию метода Object.GetHashCode() приведены в документации, а также в первой публикации. -
Статический protected метод-хелпер EqualsHelper(Person, Person) сравнивает два объекта по полям, сочетание значений которых образует уникальность значения конкретного объекта.
-
Виртуальный метод Person.Equals(Person) реализует интерфейс IEquatable(Of Person).
(Метод объявлен виртуальным, т.к. его перекрытие понадобится при наследовании — будет рассмотрено ниже.)- На "нулевом" шаге закомментирован код, проверяющий на null ссылку на текущий объект.
Если ссылка равна null, то генерируется исключение InvalidOperationException, говорящее о том, что объект находится в недопустимом состоянии.
Зачем это может быть нужно — чуть ниже. - На первом шаге проверяется равенство по ссылке текущего и входящего объекта.
Если да — то объекты равны (это один и тот же объект). - На втором шаге проверяется на null ссылка на входящий объект.
Если да — то объекты не равны (это разные объекты).
(Равенство по ссылке проверяется с помощью операторов == и !=, с предварительным приведением операндов к object для вызова неперегруженного оператора, либо с помощью метода Object.ReferenceEquals(Object, Object).
Если используются операторы == и !=, то в данном случае приведение операндов к object обязательно, т.к. в данном классе эти операторы будут перегружены и сами будут использовать метод Person.Equals(Person).) - Далее проверяется идентичность типов текущего и входящего объектов
Если типы не идентичны — то объекты не равны.
(Проверка идентичности типов объектов, вместо проверки совместимости, используется для учета реализации сравнения по значению при наследовании типа. Подробнее об этом в предыдущей публикации.) - Затем, если предыдущие проверки не позволили дать быстрый ответ, равны объекты или нет, то текущий и входящий объекты проверяются непосредственно по значению с помощью метода-хелпера EqualsHelper(Person, Person).
- На "нулевом" шаге закомментирован код, проверяющий на null ссылку на текущий объект.
-
Метод Person.Equals(Object), реализован как вызов метода Person.Equals(Person) с приведением входящего объекта к типу Person с помощью оператора as.
Примечание. Если типы объектов не совместимы, то результатом приведения будет null, что приведет к получению результата сравнения объектов в методе Person.Equals(Person) на втором шаге (объекты не равны).- Однако, в общем случае, результат сравнения в методе Person.Equals(Person) может быть получен и на первом шаге (объекты равны), т.к. теоретически в .NET возможен вызов экземплярного метода без создания экземпляра (подробнее об этом в первой публикации).
- И тогда, если ссылка на текущий объект будет равна null, ссылка на входящий объект будет не равна null, а типы текущего и входящего объектов будут несовместимы, то такой вызов Person.Equals(Object) с последующим вызовом Person.Equals(Person) даст неверный результат на первом шаге — "объекты равны", в то время на самом деле объекты не равны.
- Представляется, что такой редкий случай не требует специальной обработки, т.к. вызов экземплярного метода и использование его результата не имеет смысла без создания самого экземпляра.
Если потребуется его учесть, то достаточно раскомментировать код "нулевого шага" в методе Person.Equals(Person), что не только предотвратит получение теоретически возможного неверного результата при вызове метода Person.Equals(Object), но и, при непосредственном вызове метода Person.Equals(Person) у null-объекта, сгенерирует на "нулевом" шаге более информативное исключение, вместо NullReferenceException на третьем шаге.
- Для поддержки статического сравнения объектов по значению для CLS-совместимых языков, не поддерживающих операторы или их перегрузку, реализован статический метод Person.Equals(Person, Person).
(В качестве Type-specific, и более быстродействующей, альтернативы методу Object.Equals(Object, Object).)
(О необходимости реализации методов, соответствующих операторам, и рекомендации по соответствию операторов и имен методов, можно прочесть в книге Джеффри Рихтера (Jeffrey Richter) CLR via C# (Part II "Designing Types", Chapter 8 "Methods", Subchapter "Operator Overload Methods").)
- Метод Person.Equals(Person, Person) реализован через вызов экземплярного виртуального метода Person.Equals(Person), т.к. это необходимо для обеспечения того, чтобы "вызов x == y давал давал тот же результат, что и вызов "y == x", что соответствует требованию "вызов x.Equals(y) должен давать тот же результат, что и вызов y.Equals(x)" (подробнее о последнем требовании, включая его обеспечение при наследовании — в предыдущей публикации).
- Т.к. статические методы при наследовании типа не могут быть перекрыты (речь именно о перекрытии — override, а не о переопределении — new), т.е. не имеют полиморфного поведения, то причина именно такой реализации — вызов статического метода Person.Equals(Person, Person) через вызов виртуального экземплярного Person.Equals(Person) — именно в необходимости обеспечить полиморфизм при статических вызовах, и, тем самым, обеспечения соответствия результатов "статического" и "экземплярного" сравнения при наследовании.
- В методе Person.Equals(Person, Person) вызове экземплярного метода Person.Equals(Person) реализован с проверкой на null ссылки на тот объект, у которого вызывается метод Equals(Person).
Если этот объект — null, то выполняется сравнение объектов по ссылке.
Итак, мы нашли корректный и достаточно компактный способ реализации в одном классе всех способов сравнения объектов класса по значению, и даже учли корректность поведения на случай наследования, заложили в коде возможности, которые сможем использовать при наследовании.
При этом необходимо отдельно рассмотреть, как для данного варианта реализации сравнения объектов по значению корректно выполнить наследование, если в класс наследник вносится поле, входящее в множество полей объекта, образующих уникальное значение объекта:
Пусть есть класс PersonEx, наследующий класс Person, и имеющий дополнительное свойство MiddleName.
В этом случае сравнение двух объектов класса PersonEx:
John Teddy Smith 1990-01-01 John Bobby Smith 1990-01-01
любым реализованным способом даст результат "объекты равны", что неверно с предметной точки зрения.
Таким образом, при кажущейся тривиальности задачи, помимо достаточно больших затрат и рисков, реализация сравнения объектов по значению в текущей инфраструктуре .NET, чревата еще и тем, что как только в классе реализовано сравнение объектов по значению, то реализацию сравнения придется "тащить" (и делать это правильным образом) в классы-наследники, что несет дополнительные затраты и потенциал ошибок.
Как решение этой задачи сделать, насколько возможно, легким и компактным, поговорим в продолжении.
ссылка на оригинал статьи https://habrahabr.ru/post/315168/
Добавить комментарий