Равенство объектов в доменных моделях .NET - PullRequest
4 голосов
/ 02 ноября 2011

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

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

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

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

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

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

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

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

Ваши мысли?

ОБНОВЛЕНИЕ

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

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

Ответы [ 3 ]

1 голос
/ 02 ноября 2011

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

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

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

Что само по себе равенство идентичности действительно говорит , задайте себе вопрос: «Что означает равенство идентичности?без контекста?Является ли пользователь с Id 1 равным комментарию с Id 1?Я, конечно, надеюсь, что нет, поскольку обе сущности очень разные вещи

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

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

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

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

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

В частности:

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

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

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

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

1 голос
/ 02 ноября 2011

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

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

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

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

Вы всегда можете создать очень простой 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));
0 голосов
/ 03 ноября 2011

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

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

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

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

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

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

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

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

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

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

должны использоваться те же значения.

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

??? . Как тогда мы указываем значение равенства в коде?

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

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

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

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

Больше мыслей?

...