Pull to refresh

О сравнении объектов по значению — 6: Structure Equality Implementation

Programming *Perfect code *.NET *Designing and refactoring *C# *

В предыдущей публикации мы рассмотрели особенности устройства и работы структур платформы .NET, являющихся "типами по значению" (Value Types) в разрезе сравнения по значению объектов — экземпляров структур.


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


Поможет ли пример для структур более точно определить с предметной (доменной) точки зрения область применимости сравнения объектов по значению в целом, и тем самым упростить образец сравнения по значению объектов — экземпляров классов, являющихся ссылочными типами (Reference Types), выведенный в одной из предыдущих публикаций?


Структура PersonStruct:


struct PersonStruct
using System;

namespace HelloEquatable
{
    public struct PersonStruct : IEquatable<PersonStruct>, IEquatable<PersonStruct?>
    {
        private static int GetHashCodeHelper(int[] subCodes)
        {
            int result = subCodes[0];

            for (int i = 1; i < subCodes.Length; i++)
                result = unchecked(result * 397) ^ subCodes[i];

            return result;
        }

        private static string NormalizeName(string name) => name?.Trim() ?? string.Empty;

        private static DateTime? NormalizeDate(DateTime? date) => date?.Date;

        public string FirstName { get; }

        public string LastName { get; }

        public DateTime? BirthDate { get; }

        public PersonStruct(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()
            }
        );

        public static bool Equals(PersonStruct first, PersonStruct second) =>
            first.BirthDate == second.BirthDate &&
            first.FirstName == second.FirstName &&
            first.LastName == second.LastName;

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

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

        public bool Equals(PersonStruct other) =>
            Equals(this, other);

        public static bool Equals(PersonStruct? first, PersonStruct? second) =>
            first == second;
        // Alternate version:
        //public static bool Equals(PersonStruct? first, PersonStruct? second) =>
        //    first.HasValue == second.HasValue &&
        //    (
        //        !first.HasValue || Equals(first.Value, second.Value)
        //    );

        public bool Equals(PersonStruct? other) => this == other;
        // Alternate version:
        //public bool Equals(PersonStruct? other) =>
        //    other.HasValue && Equals(this, other.Value);

        public override bool Equals(object obj) =>
            (obj is PersonStruct) && Equals(this, (PersonStruct)obj);
        // Alternate version:
        //public override bool Equals(object obj) =>
        //    obj != null &&
        //    this.GetType() == obj.GetType() &&
        //    Equals(this, (PersonStruct)obj);
    }
}

Пример с реализацией сравнения объектов по значению для структур меньше по объему и проще по структуре благодаря тому, что экземпляры структур не могут принимать null-значения и тому, что от структур, определенных пользователем (User defined structs), нельзя унаследоваться (особенности реализации сравнения по значению объектов — экземпляров классов, с учетом наследования, рассмотрены в четвертой публикации данного цикла).


Аналогично предыдущим примерам, определены поля для сравнения и реализован метод GetHashCode().


Методы и операторы сравнения реализованы последовательно следующим образом:


  1. Реализован статический метод PersonStruct.Equals(PersonStruct, PersonStruct) для сравнения двух экземпляров структур.
    Этот метод будет использован как эталонный способ сравнения при реализации других методов и операторов.
    Также этот метод может использоваться для сравнения экземпляров структур в языках, не поддерживающих операторы.


  2. Реализованы операторы PersonStruct.==(PersonStruct, PersonStruct) и PersonStruct.!=(PersonStruct, PersonStruct).
    Следует отметить, что компилятор C# имеет интересную особенность:


    • При наличии у структуры T перегруженных операторов T.==(T, T) и T.!=(T, T), для структур Nullable(Of T) также появляется возможность сравнения с помощью операторов T.==(T, T) и T.!=(T, T).
    • Вероятно, это "магия" компилятора, проверяющая наличие значения у экземпляров структуры, перед проверкой равенства непосредственно значений, и не приводящая к упаковке экземпляров структур в объекты.
    • Что характерно, в этом случае сравнение экземпляра структуры Nullable(Of T) с нетипизированным null также приводит к вызову оператора T.==(T, T) или T.!=(T, T), в то время как аналогичное сравнение экземпляра структуры Nullable(Of T), не имеющей перегруженных операторов T.==(T, T) и T.!=(T, T), приводит к вызову оператора Object.==(Object, Object) или Object.!=(Object, Object) и, как следствие, к упаковке экземпляра структуры объект.

  3. Реализован метод PersonStruct.Equals(PersonStruct) (реализация IEquatable(Of PersonStruct)), путем вызова метода PersonStruct.Equals(PersonStruct, PersonStruct).


  4. Для предотвращения упаковки экземпляров структур в объект, если в сравнении участвует один или два экземпляра Nullable(Of PersonStruct), реализованы:

  • Метод PersonStruct.Equals(PersonStruct?, PersonStruct?) — для предотвращения упаковки экземпляров структур обоих аргументов в объекты и вызова метода Object.Equals(Object, Object), если хотя бы один из аргументов является экземпляром Nullable(Of PersonStruct). Также этот метод может быть использован при сравнении экземпляров Nullable(Of PersonStruct) в языках, не поддерживающих операторы. Метод реализован как вызов оператора PersonStruct.==(PersonStruct, PersonStruct). Рядом с методом приведен закомментированный код, показывающий, каким образом нужно было бы реализовать этот метод, если бы компилятор C# не поддерживал вышеупомянутую "магию" использования операторов T.==(T, T) и T.!=(T, T) для Nullable(Of T)-аргументов.


  • Метод PersonStruct.Equals(PersonStruct?) (реализация интерфейса IEquatable(Of PersonStruct?)) — для предотвращения упаковки Nullable(Of PersonStruct)-аргумента в объект и вызова метода PersonStruct.Equals(Object). Метод также реализован как вызов оператора PersonStruct.==(PersonStruct, PersonStruct), с закомментированным кодом реализации при отсутствии "магии" компилятора.


  • И наконец, реализован метод PersonStruct.Equals(Object), перекрывающий метод Object.Equals(Object).
    Метод реализован путем проверки совместимости типа аргумента с типом текущего объекта с помощью оператора is, с последующими приведением аргумента к PersonStruct и вызовом PersonStruct.Equals(PersonStruct, PersonStruct).

Примечание:


  • Реализация интерфейса IEquatable(Of PersonStruct?) — IEquatable(Of Nullable(Of PersonStruct)) приведена для демонстрации определенных проблем в платформе при работе со структурами в той части, что упаковка их экземпляров в объект происходит чаще, чем этого хотелось бы и можно ожидать.
  • В реальных проектах, только если вам не нужно специально оптимизировать производительность, реализовывать IEquatable(Of Nullable(Of T)) не следует по архитектурным причинам — не следует реализовывать в типе T типизированный IEquatable для какого-то другого типа.
  • Да и в целом, не стоит загромождать код различными преждевременными оптимизациями, даже если в самой платформе оптимизация не будет произведена. В этой публикации дополнительно посмотреть, как часто выполняется упаковка при работе со структурами.

Для структур исчерпывающая реализация сравнения экземпляров по значению получилась существенно проще и компактнее благодаря отсутствию наследования у User defined structs, а также благодаря отсутствию необходимости проверок на null.
(Однако, по сравнению с реализацией для классов, появилась и новая логика, поддерживающая Nullable(Of T)-аргументы).


В следующей публикации мы подведем итоги цикла на тему "Object Equality", в т.ч. рассмотрим:


  • в каких случаях, с предметной и технической точек зрения, действительно целесообразно реализовывать сравнение значений объектов по значению;
  • каким образом в этих случаях возможно упростить реализацию сравнения по значению для объектов — экземпляров классов, являющихся ссылочными типами (Reference Types), с учетом опыта упрощенной реализации для структур.
Tags:
Hubs:
Total votes 14: ↑12 and ↓2 +10
Views 6.1K
Comments Comments 15