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

в 16:12, , рубрики: .net, C#, equality operators, Equals, Equals(T), GetHashCode, IEquatable<T>, Nullable<T>, object equality, struct, value types, Программирование, Проектирование и рефакторинг, Совершенный код

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

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

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

struct PersonStruct

using System;

namespace HelloEquatable
{
    public struct PersonStruct : IEquatable<PersonStruct>, IEquatable<PersonStruct?>
    {
        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() =>
            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).

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

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

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

Автор: sand14

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js