Равенство объектов в моделях предметной области .NET

Мне нужны рекомендации по передовому опыту реализации равенства в модели предметной области. На мой взгляд, существует три (3) типа равенства:

  1. Ссылочное равенство - это означает, что оба объекта хранятся в одном и том же пространстве физической памяти.

  2. Identity Equality - это означает, что оба объекта имеют одинаковое значение идентичности. Например, два объекта Order с одинаковым номером заказа представляют одну и ту же сущность. Это особенно важно при хранении значений в списках, хэш-таблицах и т. Д., И объекту требуется уникальный идентификатор для поиска.

  3. Равенство ценностей - оба объекта имеют одинаковые свойства.

По соглашению .NET предоставляет два (2) способа проверки равенства: Equals и ==. Итак, как нам сопоставить три (3) типа с двумя (2) методами?

Я, конечно, исключил Object.ReferenceEquals, который MS добавила в знак признания того, что большинство людей переопределяли Equals, потому что ссылочное равенство не было их желаемым поведением. Так что, может быть, мы сможем вычеркнуть первый тип (?).

Учитывая поведение GetHashCode и Equals в контексте хэш-таблицы, можно ли с уверенностью сказать, что Equals всегда должен обеспечивать Identity Equality? Если да, то как мы можем предоставить вызывающим абонентам способ проверки равенства ценностей?

И разве большинство разработчиков не думают, что Equals и == дадут одинаковый результат? Поскольку == проверяет ссылочное равенство, означает ли это, что мы также должны перегружать == при переопределении Equals?

Твои мысли?

ОБНОВЛЕНИЕ

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

Кроме того, если посмотреть на типичные классы Assert, семантика еще более запутанная. AreEqual (a, b) обычно использует метод Equals, подразумевающий Identity или Value Equality, в то время как AreSame (a, b) использует ReferenceEquals для ссылочного равенства.


person SonOfPirate    schedule 02.11.2011    source источник
comment
Все, что я нашел до сих пор, похоже, указывает на WPF, использующий Equals(), а не ReferenceEquals() или == для обеспечения равенства. Ссылка 1 Ссылка 2. Может быть, ваш коллега изменил метод Equals() в классе, чтобы он работал в одну сторону, изменил данные, ожидал обновления привязки данных, но этого не произошло, потому что Equals() по-прежнему вернул истину?   -  person Johannes Kommer    schedule 03.11.2011
comment
Спасибо, я думаю, что начальный абзац второй ссылки прекрасно это объясняет (и лучше, чем мой коллега!).   -  person SonOfPirate    schedule 03.11.2011


Ответы (3)


Обычно я разрабатываю свои модели предметной области с помощью == и ReferenceEquals() выполнения ссылочного равенства. И Equals() выполнение равенства ценностей. Причина, по которой я не использую ни один из них для равенства идентичности, состоит из трех частей:

Не все имеет идентичность, поэтому может возникнуть путаница в отношении того, как на самом деле работают Equals () и ==, когда задействован объект без идентичности. Подумайте, например, о кеше, содержащем несколько сущностей или временных / вспомогательных объектах. А как насчет агрегированных объектов, которые могут быть основаны на нескольких разных объектах домена? Какую идентичность он мог бы сравнить?

Равенство идентичности - это подмножество равенства ценностей. По моему опыту, когда речь идет о равенстве идентичности, равенство ценностей не сильно отстает, и обычно идентичность значений включает также равенство идентичностей. В конце концов, если идентичности не совпадают, действительно ли ценности совпадают?

О чем на самом деле говорит равенство идентичности само по себе, задайте себе вопрос: «Что означает равенство идентичности без контекста?» Равен ли пользователь с идентификатором 1 комментарию с идентификатором 1? Я, конечно, надеюсь, что нет, поскольку обе сущности - очень разные вещи.

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

Например; в домене, где равенство идентичности встречается очень редко, я бы создал пользовательский EqualityComparer<T> для обеспечения равенства идентичности, когда и где это необходимо, контекстно-зависимым способом, если равенство идентичности не является распространенной проблемой в моем текущем домене.

Однако в домене, где равенство идентичности очень распространено, я бы вместо этого выбрал метод в моем базовом классе идентичности под названием IdentityEquals(), который заботится о равенстве идентичности на базовом уровне.

Таким образом, я раскрываю равенство идентичности только там, где это уместно и логично. Без какой-либо потенциальной путаницы в отношении того, как может работать какая-либо из моих проверок равенства. Будь то Equals(), == или IdentityEquals / EqualityComparer<T> (в зависимости от того, насколько распространено равенство идентичности в моем домене).

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

Конкретно:

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

РЕДАКТИРОВАТЬ:

Что касается Assert.AreEqual и Assert.AreSame, ваш домен определяет, что означает равенство; будь то ссылка, идентичность или ценность. Таким образом, ваше определение Equals в вашем домене расширяется до определения Assert.AreEqual. Если вы говорите, что Equals проверяет равенство идентичности, то с помощью логического расширения Assert.AreEqual проверяет равенство идентичности.

Assert.AreSame проверяет, являются ли оба объекта одним и тем же объектом. Одинаковый и равный - это два разных понятия. Единственный способ проверить, совпадает ли объект, на который ссылается A, с объектом, на который ссылается B, - это ссылочное равенство. Семантически и синтаксически оба имени имеют смысл.

person Johannes Kommer    schedule 02.11.2011
comment
Я согласен с желательностью согласованности и, прочитав руководство, разместил тему. Я не уверен, что EqualityComparer - это ответ, поскольку один метод Equals дает разные результаты от другого метода Equals - это то, чего я пытаюсь избежать. Придется подумать еще раз. - person SonOfPirate; 03.11.2011
comment
FWIW - вариант использования равенства идентичности распространен в сервис-ориентированных приложениях, где объект материализуется из запроса службы и должен быть сравнен с существующим объектом домена. Весьма вероятно, что два объекта будут иметь разные значения для одного или нескольких свойств, но при этом будут представлять одну и ту же сущность. - person SonOfPirate; 03.11.2011
comment
Я немного поясню свой первый комментарий. Я разрабатываю фреймворки и инструменты, которые используют другие разработчики. Зная мою целевую аудиторию, использование должно быть четким и последовательным, иначе признание быстро упадет. - person SonOfPirate; 03.11.2011
comment
Только что обновил свой исходный пост, чтобы отразить мои мысли о том, как потенциально решить эту проблему в области, где равенство идентичности является обычным явлением. По сути, я бы реализовал новый метод в моем базовом классе идентификации - это сохраняет логику локальной, отдельной и доступной только там, где это действительно имеет смысл, не вызывая путаницы re: Equals / == / IdentityEquals - person Johannes Kommer; 03.11.2011
comment
Я понимаю. Кажется, я припоминаю, что видел сообщение в блоге, в котором упоминался аналогичный подход с методом IsSameXYZ () (где XYZ - имя объекта). - person SonOfPirate; 03.11.2011

Для ссылочного равенства я использую object.ReferenceEquals, как вы сказали, хотя вы также можете просто привести ссылки к объектам и сравнить их (если они являются ссылочными типами).

Для 2 и 3 это действительно зависит от того, что хочет разработчик, если он хочет определить равенство как идентичность или равенство значений. Обычно я предпочитаю сохранять Equals () как равенство значений, а затем предоставлять внешние компараторы для равенства значений.

Большинство методов, которые сравнивают элементы, дают вам возможность передать настраиваемый компаратор, и именно здесь я обычно передаю любой настраиваемый компаратор равенства (например, identity), но это я.

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

Вы всегда можете создать очень простой ProjectionComparer, который принимает любой тип и создает компаратор на основе проекции, упрощает передачу пользовательских компараторов для идентификации и т. Д. В момент необходимости и оставляет метод Equals () только для значения.

Кроме того, как правило, я лично не перегружаю ==, если только я не пишу тип значения, для которого требуются типичные операторы сравнения, потому что существует много путаницы с перегрузкой операторов и тем, что перегрузки не переопределяют.

Но опять же, это только мое мнение :-)

ОБНОВЛЕНИЕ Вот мой компаратор проекций, вы, конечно, можете найти много других реализаций, но этот хорошо работает для меня, он реализует оба EqualityComparer<TCompare> (поддерживает bool Equals(T, T) и int GetHashCode(T) и IComparer<T>, который поддерживает Compare(T, T)):

public sealed class ProjectionComparer<TCompare, TProjected> : EqualityComparer<TCompare>, IComparer<TCompare>
{
    private readonly Func<TCompare, TProjected> _projection;

            // construct with the projection
    public ProjectionComparer(Func<TCompare, TProjected> projection)
    {
        if (projection == null)
        {
            throw new ArgumentNullException("projection");
        }

        _projection = projection;
    }

    // Compares objects, if either object is null, use standard null rules
            // for compare, then compare projection of each if both not null.
    public int Compare(TCompare left, TCompare right)
    {
        // if both same object or both null, return zero automatically
        if (ReferenceEquals(left, right))
        {
            return 0;
        }

        // can only happen if left null and right not null
        if (left == null)
        {
            return -1;
        }

        // can only happen if right null and left non-null
        if (right == null)
        {
            return 1;
        }

        // otherwise compare the projections
        return Comparer<TProjected>.Default.Compare(_projection(left), _projection(right));
    }

    // Equals method that checks for null objects and then checks projection
    public override bool Equals(TCompare left, TCompare right)
    {
        // why bother to extract if they refer to same object...
        if (ReferenceEquals(left, right))
        {
            return true;
        }

        // if either is null, no sense checking either (both are null is handled by ReferenceEquals())
        if (left == null || right == null)
        {
            return false;
        }

        return Equals(_projection(left), _projection(right));
    }

    // GetHashCode method that gets hash code of the projection result
    public override int GetHashCode(TCompare obj)
    {
        // unlike Equals, GetHashCode() should never be called on a null object
        if (obj == null)
        {
            throw new ArgumentNullException("obj");
        }

        var key = _projection(obj);

        // I decided since obj is non-null, i'd return zero if key was null.
        return key == null ? 0 : key.GetHashCode();
    }

    // Factory method to generate the comparer for the projection using type
    public static ProjectionComparer<TCompare, TProjected> Create<TCompare, 
                     TProjected>(Func<TCompare, TProjected> projection)
    {
        return new ProjectionComparer<TCompare, TProjected>(projection);
    }
}

Это позволит вам делать такие вещи, как:

List<Employee> emp = ...;

// sort by ID
emp.Sort(ProjectionComparer.Create((Employee e) => e.ID));

// sort by name
emp.Sort(ProjectionComparer.Create((Employee e) => e.Name));
person James Michael Hare    schedule 02.11.2011
comment
Итак, если я последую за вами, ваши переопределения Equals выполняют полное равенство значений, и если вы используете хэш-таблицу или словарь с объектом, вы предоставляете компаратор для хеш-таблицы для выполнения Identity Equality. Правильный? - person SonOfPirate; 03.11.2011
comment
@SonOfPirate: Обычно. И это только ЕСЛИ он мне нужен, если мой класс в основном является классом функциональности (например, DAO и т. Д.), Я обычно не буду беспокоиться. Только в POCO это действительно становится проблемой. Это просто действительно зависит от того, в чем я нуждаюсь, нужно ли мне равенство ценности или идентичности для текущего бизнес-кейса. - person James Michael Hare; 03.11.2011
comment
@SonOfPirate: И много раз я не переопределяю Equals (), а просто буду использовать компаратор проекций, потому что мне не нужен класс, определяющий равенство. - person James Michael Hare; 03.11.2011
comment
С помощью компаратора проекции я предполагаю, что вы имеете в виду лямбду, а не явный класс? - person SonOfPirate; 03.11.2011

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

Вот ключевые моменты, которые я почерпнул из этих обсуждений:

  1. Сущности по самому своему определению в моделях предметной области обладают идентичностью.

  2. Агрегированные корни - это (согласно определениям, которые я читал) сущности, которые содержат другие сущности; следовательно, совокупность также имеет идентичность.

  3. Хотя сущности изменчивы, их идентичность быть не может.

  4. В рекомендациях Microsoft указано, что, когда GetHashCode () для двух объектов равен, Equals должен возвращать true для этих объектов.

  5. При сохранении сущности в хэш-таблице GetHashCode должен возвращать значение, представляющее идентичность этой сущности.

  6. Идентификационное равенство не означает ссылочное равенство или ценностное равенство. Ценностное равенство также не означает ссылочного равенства. Но ссылочное равенство действительно означает идентичность и равенство ценностей.

По правде говоря, я понял, что это может быть просто проблема синтаксиса / семантики. Нам нужен третий способ определения равенства. У нас два:

Равно. В модели предметной области две сущности равны, если они имеют одну и ту же идентичность. Я считаю, что это должно быть так, чтобы удовлетворить пункты 4 и 5 выше. Мы используем идентификатор объекта для генерации хэш-кода, возвращаемого из GetHashCode, поэтому те же значения должны использоваться для определения равенства.

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

???. Как же тогда обозначить равенство ценностей в коде?

Во всех своих беседах я обнаружил, что мы применяем квалификаторы, чтобы так или иначе сформировать эти термины; использование таких имен, как «IdentityEquals» и «IsSameXYZ», поэтому «Equals» означает равенство значений или «IsEquivalentTo» и «ExactlyEquals» для обозначения равенства значений, поэтому «Equals» означает равенство идентификаторов.

Хотя я ценю гибкость, чем больше я иду по этому пути, тем больше понимаю, что нет двух разработчиков, которые видят это одинаково. И это вызывает проблемы.

И я могу вам сказать, что каждый разработчик, с которым я разговаривал, указывал на то, что они ожидают, что "==" будет вести себя точно так же, как Equals. Тем не менее, Microsoft рекомендует не перегружать "==", даже если мы переопределяем Equals. Было бы неплохо, если бы оператор core == просто делегировал Equals.

Итак, в нижней строке, я буду переопределять Equals, чтобы обеспечить Identity Equality, предоставить метод SameAs для Referential Equality (просто удобную оболочку для ReferenceEquals) и перегрузить == в нашем базовом классе, чтобы использовать Equals, чтобы они были согласованными. Затем я буду использовать компараторы для «сравнения» значений двух «равных» объектов.

Еще мысли?

person SonOfPirate    schedule 03.11.2011