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

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

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

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

Экземпляры структур, в силу своей природы, всегда сравниваются по значению.

Для предопределенных типов, таких как Boolean или Int32, под сравнением по значению понимается сравнение непосредственно значений экземпляров структур.

Если структура определена разработчиком — пользователем платформы, то сравнение по умолчанию автоматически реализуется как сравнение значений полей экземпляров структур.
(Подробности см. в описании метода ValueType.Equals(Object) и операторов == и !=)
Также при этом автоматически определенным образом реализуется метод ValueType.GetHashCode(), перекрывающий метод Object.GetHashCode().

И в этом случае есть несколько существенных подводных камней:

  1. При сравнении значений полей используется рефлексия, что влияет на производительность.

  2. Поле структуры может иметь не "значимый", а ссылочный тип, а в этом случае сравнение полей по ссылке может не подойти с предметной (доменной) точки зрения, и может потребоваться сравнение полей по значению (хотя в общем случае использование в структуре ссылочный полей можно считать неверным архитектурным решением).
    документации рекомендуется создать для такой структуры собственную реализацию сравнения по значению для повышения производительности и наиболее точного отражения значения равенства для данного типа.)

  3. Может оказаться, что с предметной точки зрения не все поля должны участвовать в сравнении (хотя, опять же, для структур в общем случае это можно считать неверным решением).

  4. И наконец, дефолтная реализация метода ValueType.GetHashCode() не соответствует общим требованиям к реализации метода GetHashCode() (о которых мы говорили в первой публикации):
    • значение хеш-кода, полученное с помощью ValueType.GetHashCode(), может оказаться непригодным для использования в качестве ключа в хеш-таблице;
    • если значение одного или нескольких полей объекта изменилось, то значение, полученное с помощью ValueType.GetHashCode(), также может оказаться непригодным для использования ключа в хеш-таблице;
    • в документации рекомендуется создавать собственную реализацию метода GetHashCode(), наиболее точно отражающую концепцию хеш-кода для данного типа.

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

С другой стороны, необходимость корректной реализации метода GetHashCode() автоматически приводит к необходимости реализации сравнения по значению, т.к. метод GetHashCode() в силу природы хеш-кода (см. первую публикацию) должен "знать", какие данные (поля) и как участвуют в сравнении по значению.

С третьей стороны, возможен и особый случай, когда есть "простая" структура, состоящая, например, только из полей-структур, для которых побайтовое сравнение с помощью рефлексии заведомо дает сементически верный результат (например, Int32).
В этом случае возможно реализовать GetHashCode() корректным образом (чтобы для равных объектов хеш-код всегда был один и тот же), не создавая при этом собственную реализацию сравнения по значению.

Например:

Simple Point Structure

    public struct Point
    {
        private int x;

        private int y;

        public int X {
            get { return x; }
            set { x = value; }
        }

        public int Y
        {
            get { return y; }
            set { y = value; }
        }

        public override int GetHashCode() => x.GetHashCode() ^ y.GetHashCode();
    }

Однако, в случае переписывания этого простого примера с использованием "автосвойств" картина выглядит уже менее ясной:

Simple Point Structure with Auto-Implemented Properties

    public struct Point
    {
        public int X { get; set; }

        public int Y { get; set; }

        public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
    }

В документации к автосвойствам говорится об автоматическом создании anonymous backing field, соответствующим публичным свойствам.
Строго говоря, из описания неясно, будут ли равными с точки зрения дефолтной реализации сравнения по значению два объекта Point с попарно одинаковыми значениями X и Y:

  • Если дефолтная реализация сравнивает с помощью рефлексии значения полей, то как для разных объектов происходит сопоставление анонимных полей — что эти поля соответствуют друг друга, т.к. каждое соответствует свойству X, а эти соответствуют друг другу, т.к. каждое соответствует Y?
    Что если в двух разных объектах создаются backing-поля с разными именами вида (x1, y1) и (x2, y2)?
    Будет ли учитываться при сравнении, что x1 соответствует x2, а y1 соответствует y2?
  • Создаются ли при этом еще какие-то вспомогательные поля, которые могут иметь разные значения для одинаковых с точки зрения интерфейса (X, Y) объектов? Если да, то будут ли учитываться эти поля при сравнении?
  • Или, возможно, в случае структуры с автосвойствами, будет использоваться побайтовое сравнение всего содержимого структуры, без сравнения отдельных полей? Если да, то backing-поля для каждого объекта будут создаваться в памяти всегда в одном и том же порядке и с одинаковыми смещениями?

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

Тем не менее, представляется, что в общем случае для структур предпочтительнее всегда реализовывать собственное сравнение по значению.

Развернутый пример с подробными комментариями, на основе знакомой по предыдущим публикациям сущности Person, рассмотрим в следующей публикации.

Автор: sand14

Источник

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


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