.NET 域模型中的对象平等

发布于 2024-12-13 08:46:41 字数 973 浏览 1 评论 0原文

我正在寻找有关在域模型中实现平等时的最佳实践的建议。在我看来,存在三 (3) 种相等类型:

  1. 引用相等 - 意味着两个对象存储在同一个对象中 物理内存空间。

  2. 身份平等 - 意味着两个对象具有相同的身份值。 例如,两个具有相同订单号的订单对象 代表同一实体。这一点尤其重要 当在列表、哈希表等中存储值时 对象需要唯一的标识进行查找。

  3. 值相等 - 两个对象的所有属性都相同。

按照惯例,.NET 提供了两 (2) 种测试相等性的方法:Equals 和 ==。那么我们如何将三 (3) 类型映射到两 (2) 方法呢?

当然,我遗漏了 Object.ReferenceEquals,MS 添加它是为了认识到大多数人都在重写 Equals,因为引用相等不是他们想要的行为。所以也许我们可以划掉第一种类型(?)。

考虑到 GetHashCode 和 Equals 在哈希表上下文中的行为,是否可以肯定地说 Equals 应该始终提供身份相等性?如果是这样,我们如何为调用者提供测试价值平等的方法?

而且,大多数开发人员不是都认为 Equals 和 == 会产生相同的结果吗?由于 == 测试引用相等性,这是否意味着我们在重写 Equals 时也应该重载 == ?

你的想法?

更新

我不知道所有细节,但我被告知(在与同事的面对面交谈中)WPF 对数据绑定对象对 Equals 或 data- 使用引用相等性有严格要求绑定无法正常工作。

此外,看看典型的 Assert 类,还有更令人困惑的语义。 AreEqual(a, b) 通常使用 Equals 方法来表示身份或值相等,而 AreSame(a, b) 使用 ReferenceEquals 来表示引用相等。

I'm looking for recommendations on best practices when implementing equality in a domain model. As I see it, there are three (3) types of equality:

  1. Referential Equality - meaning that both objects are stored in the same
    physical memory space.

  2. Identity Equality - meaning that both object have the same identity value.
    For instance, two Order objects with the same Order Number
    represent the same entity. This is especially important
    when storing values in lists, hashtables, etc. and the
    object needs a unique identity for lookup.

  3. Value Equality - both objects have all properties the same.

By convention, .NET provides two (2) ways to test for equality: Equals and ==. So how do we map the three (3) types to the two (2) methods?

I, of course, left out Object.ReferenceEquals which MS added in recognition that most people were overriding Equals because referential equality wasn't their desired behavior. So maybe we can cross off the first type (?).

Given the behavior of GetHashCode and Equals in the context of a hashtable, is it safe to say that Equals should always provide Identity Equality? If so, how do we provide callers with as way to test for Value Equality?

And, don't most developers assume that Equals and == will yield the same result? Since == tests referential equality, does this mean we should also be overloading == when we override Equals?

Your thoughts?

UPDATE

I don't know all of the details but I was informed (in an in-person conversation with a colleague) that WPF has strict requirements that data-bound objects use referential equality for Equals or data-binding does not work correctly.

Also, looking at typical Assert classes, there is even more confusing semantics. AreEqual(a, b) will typically use the Equals method implying Identity or Value Equality while AreSame(a, b) uses ReferenceEquals for Referential Equality.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

離人涙 2024-12-20 08:46:41

我通常开发域模型的方式是围绕执行引用相等的 ==ReferenceEquals()Equals() 执行值相等。我不使用这些来实现身份相等的原因有三个:

并非所有东西都有身份,因此当涉及没有身份的对象时,会导致人们对 Equals() 和 == 的实际工作原理感到困惑。例如,考虑包含多个实体或临时/辅助对象的缓存。可能基于多个不同域对象的聚合对象又如何呢?它会比较哪个身份?

身份平等是价值平等的一个子集,从我的经验来看,只要涉及到身份平等,价值平等就紧随其后,通常价值身份也包含身份平等。毕竟,如果身份不一样,价值观真的一样吗?

身份平等本身到底意味着什么,问自己这个问题:“在没有上下文的情况下,身份平等意味着什么?” Id 1的用户是否等于Id 1的评论?我当然希望不会,因为这两个实体是非常不同的东西。

那么,为什么要对例外而不是规则的情况使用任何内置相等方法(==Equals())呢?相反,我倾向于实现一个基类,它提供我的身份信息,并根据我当前域中身份平等的常见程度来实现身份平等。

例如;在身份平等非常罕见的领域中,如果身份平等在我当前的领域中不是一个常见问题,我将创建一个自定义 EqualityComparer ,以便在需要时以上下文敏感的方式实现身份平等。

但是,在身份平等非常常见的领域中,我会选择身份基类中名为 IdentityEquals() 的方法,该方法负责处理基本级别上的身份平等。

这样我只在相关且合乎逻辑的地方公开身份平等。不会对我的任何平等检查如何工作产生任何潜在的困惑。无论是 Equals()== 还是 IdentityEquals / EqualityComparer(取决于常见程度)身份平等属于我的领域)。

另外,作为旁注,我建议阅读 Microsoft 的 指南用于重载相等

具体来说:

默认情况下,运算符 == 通过以下方式测试引用相等性:
判断两个引用是否指示同一个对象,因此引用
类型不需要实现运算符 == 来获得这个
功能。当类型是不可变的时,意味着包含在
实例无法更改,重载运算符 == 进行比较
值相等而不是引用相等可能很有用,因为
不可变对象,只要它们可以被认为是相同的
具有相同的值。 在非不可变类型中重写运算符 == 是
不推荐。

编辑:

关于 Assert.AreEqualAssert.AreSame,您的域定义了平等的含义;无论是参考、身份还是价值。因此,通过扩展,域中 Equals 的定义也可以扩展到 Assert.AreEqual 的定义。如果您说 Equals 检查身份相等性,那么通过逻辑扩展 Assert.AreEqual 将验证身份相等性。

Assert.AreSame 检查两个对象是否是同一对象。相同和等于是两个不同的概念。检查 A 引用的对象与 B 引用的对象是否相同的唯一方法是引用相等。从语义和句法上看,这两个名字都有道理。

The way I usually develop my domain models is around == and ReferenceEquals() performing referential equality. And Equals() performing value equality. The reason I use none of these for identity equality is three-fold:

Not everything has an identity, so would cause confusion about how Equals() and == actually work when an object without identity is involved. Think for example about a cache containing several entities, or temporary / helper objects. What about aggregated objects which might be based on several different domain objects? Which identity would it compare?

Identity equality is a subset of value equality, from my experience whenever identity equality is involved, value equality is not far behind and usually value identity includes identity equality aswell. After all if the identities are not the same, are the values really the same?

What does identity equality on it's own really say, ask yourself this question: "What does identity equality mean without context?" Is a user with Id 1 equal to a comment with Id 1? I certainly hope not as both entities are very different things.

So why use any of the build-in equality methods (== and Equals()) for something that is the exception, and not the rule? Instead I tend to implement a base class which provides my identity information and implement identity equality depending on how common identity equality is within my current domain.

For example; in a domain where identity equality is very uncommon I would create a custom EqualityComparer<T> to do identity equality when and where needed in a context sensitive way if identity equality is not a common issue within my current domain.

However, in a domain where identity equality is very common I'd instead opt for a method in my identity base-class called IdentityEquals() which takes care of the identity equality on a base-level.

This way I only expose identity equality where it is relevant and logical. Without any potential confusion about how any of my equality checks might work. Whether it be Equals(), ==, or IdentityEquals / EqualityComparer<T> (depending on how common identity equality is within my domain).

Also as a side note I would recommend reading Microsoft's guidelines for overloading equality.

Specifically:

By default, the operator == tests for reference equality by
determining if two references indicate the same object, so reference
types do not need to implement operator == in order to gain this
functionality. When a type is immutable, meaning the data contained in
the instance cannot be changed, overloading operator == to compare
value equality instead of reference equality can be useful because, as
immutable objects, they can be considered the same as long as they
have the same value. Overriding operator == in non-immutable types is
not recommended.

EDIT:

Regarding Assert.AreEqual and Assert.AreSame, your domain defines what equality means; whether it be reference, identity or value. So by extension your definition of Equals within your domain also extends to the definition of Assert.AreEqual. If you say that Equals checks for identity equality then by logical extension Assert.AreEqual will verify identity equality.

Assert.AreSame checks whether both objects are the same object. Same and equals are two different concepts. The only way to check whether object referenced by A is the same as the object referenced by B is referential equality. Semantically and syntactically both names make sense.

初与友歌 2024-12-20 08:46:41

对于引用相等,我使用 object.ReferenceEquals 正如你所说,尽管你也可以将引用强制转换为对象并比较它们(只要它们是引用类型)。

对于 2 和 3,这实际上取决于开发人员想要什么,他们是否想将平等定义为身份平等或价值平等。通常,我喜欢将 Equals() 保留为值相等,然后提供外部比较器以实现身份相等。

大多数比较项目的方法都使您能够传递自定义比较器,这就是我通常传递任何自定义相等比较器(如身份)的地方,但这就是我。

正如我所说,这是我的典型用法,我还构建了对象模型,其中我只考虑属性的子集来表示身份,并且不比较其他属性。

您始终可以创建一个非常简单的 ProjectionComparer,它采用任何类型并基于投影创建比较器,使得在需要时非常容易地传递用于身份等的自定义比较器,并将 Equals() 方法仅用于值。

另外,通常情况下,我个人不会重载 == ,除非我正在编写需要典型比较运算符的值类型,因为运算符重载以及重载如何不被覆盖存在很多混乱。

但这只是我的意见:-)

更新这是我的投影比较器,当然,您可以找到许多其他实现,但这一个对我来说效果很好,它实现了 EqualityComparer; (支持 bool Equals(T, T)int GetHashCode(T)IComparer 支持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));

For referential equality, i use object.ReferenceEquals as you said, though you can also just cast the references to objects and compare them (as long as they are reference types).

For 2 and 3 it really depends what the developer wants, if they want to define equality as identity or value equality. Typically, I like to keep my Equals() as value equality and then provide external comparers for identity equality.

Most methods that compare items give you the ability to pass in a custom comparer, and that is where I typically pass in any custom equality comparer (like identity), but that's me.

And as I said, that's my typical usage, I've also constructed object models where I do only consider a subset of properties to represent identity and the others aren't compared.

You can always create a very simple ProjectionComparer that takes any type and creates a comparer based on a projection, makes it very easy to pass custom comparers for identity, etc at point of need and leave the Equals() method just for value.

Also, typically, I personally don't overload == unless I am writing a value type that needs the typical comparison operators because there's so much confusion with operator overloading and how overloads aren't overrides.

But again, that's just my opinion :-)

UPDATE Here's my projection comparer, you can find many other implementations, of course, but this one works well for me, it implements both EqualityComparer<TCompare> (supports bool Equals(T, T) and int GetHashCode(T) and IComparer<T> which supports 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);
    }
}

This let's you do things like:

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));
蓝梦月影 2024-12-20 08:46:41

我想我应该从上述帖子以及外部对话中提出我的总结作为答案,而不是通过更新原始帖子来混淆主题。我将保留该主题,让读者投票选择他们认为最好的答案,然后再选择一个。

以下是我从这些讨论中收集到的要点:

  1. 实体在域模型中的定义具有标识。

  2. 聚合根是(根据我读过的定义)包含其他实体的实体;因此,聚合也具有身份。

  3. 虽然实体是可变的,但它们的身份不应该是。

  4. Microsoft 指南指出,当两个对象的 GetHashCode() 相等时,Equals 应为这些对象返回 true。

  5. 当在哈希表中存储实体时,GetHashCode 应返回一个表示该实体身份的值。

  6. 身份平等并不意味着参照平等或价值平等。价值平等也不意味着参照平等。但是,参照平等确实意味着身份和价值平等。

说实话,我逐渐意识到这可能只是一个语法/语义问题。我们需要第三种定义平等的方式。我们有两个:

等于。在域模型中,当两个实体共享相同的身份时,它们相等。我觉得为了满足#4和#4,情况必须如此。上面#5。我们使用实体的身份来生成从 GetHashCode 返回的哈希码,因此必须使用相同的值来确定相等性。

相同。根据现有用法(在调试和测试框架中),当两个对象/实体相同时,它们引用相同的实例(引用相等)。

<强>???。那么我们如何在代码中表示值相等呢?

在我所有的谈话中,我发现我们正在以某种方式应用限定词来塑造这些术语;使用“IdentityEquals”和“IsSameXYZ”等名称,因此“Equals”表示值相等,或使用“IsEquivalentTo”和“ExactlyEquals”表示值相等,因此“Equals”表示身份相等。

虽然我欣赏灵活性,但沿着这条路走得越多,我就越意识到没有两个开发人员会以相同的方式看待这一点。这会导致问题。

我可以告诉你,我采访过的每一位开发人员都表示,他们希望“==”的行为与 Equals 完全相同。然而,即使我们覆盖 Equals,Microsoft 建议也不要重载“==”。如果核心 == 运算符能够简单地委托给 Equals,那就太好了。

因此,最重要的是,我将重写 Equals 以提供身份平等,为引用平等提供 SameAs 方法(只是 ReferenceEquals 的方便包装),并在我们的基类中重载 == 以使用 Equals,以便它们保持一致。然后,我将使用比较器来“比较”两个“相等”实体的值。

更多想法?

I thought I'd propose my summary from the above posts as well as outside conversations as an answer rather than confuse the topic by updating the original post. I'll leave the topic open and let readers vote which answer they feel is the best before selecting one.

Here are the key points that I've gleened from these discussions:

  1. Entities by their very definition in domain models have identity.

  2. Aggregate Roots are (according to the definitions I've read) entities that contain other entities; therefore, an aggregate also has identity.

  3. While entities are mutable, their identity should not be.

  4. Microsoft guidelines indicate that that when GetHashCode() for two objects is equal, Equals should return true for those objects.

  5. When storing an entity in a hashtable, GetHashCode should return a value that represents the identity of that entity.

  6. Identity Equality does not mean Referential Equality or Value Equality. Nor does Value Equality mean Referential Equality. But, Referential Equality does mean Identity and Value Equality.

Truth be told, what I have come to realize is that this may simply be a syntax/semantics issue. We need a third way of defining equality. We have two:

Equals. In a domain model, two entities are equal when they share the same identity. I feel this must be the case in order to satisfy #4 & #5 above. We use the entity's identity to generate the hashcode returned from GetHashCode, therefore, the same values must be used to determine equality.

Same. Based on existing usage (in debugging and testing frameworks), when two object/entities are the same, they reference the same instance (Referential Equality).

???. How then do we indicate Value Equality in code?

In all of my conversations I found that we are applying qualifiers to shape these terms one way or another; using names like "IdentityEquals" and "IsSameXYZ" so "Equals" means Value Equality or "IsEquivalentTo" and "ExactlyEquals" to mean Value Equality so "Equals" means Identity Equality.

While I appreciate flexibility, the more I walk down this path the more I realize no two developers see this the same way. And that causes problems.

And I can tell you that every developer I talked to, to a one, indicated that they expect "==" to behave exactly the same as Equals. Yet, Microsoft recommends not overloading "==" even if we override Equals. It would have been nice if the core == operator would have simply delegated to Equals.

So, bottom line, I will be overriding Equals to provide Identity Equality, provide a SameAs method for Referential Equality (just a convenience wrapper on ReferenceEquals) and overloading == in our base class to use Equals so they are consistent. I will then use comparers to "compare" the values of two "equal" entities.

More thoughts?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文