【问题标题】:Object Equality in .NET Domain Models.NET 域模型中的对象平等
【发布时间】:2011-12-20 15:01:03
【问题描述】:

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

  1. 参照平等 - 意味着两个对象存储在同一个 物理内存空间。

  2. Identity Equality - 表示两个对象具有相同的标识值。 例如,具有相同订单号的两个订单对象 代表同一个实体。这是特别重要的 在列表、哈希表等中存储值时 对象需要一个唯一的标识进行查找。

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

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

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

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

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

你的想法?

更新

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

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

【问题讨论】:

  • 到目前为止,我发现的所有内容似乎都指向 WPF,使用 Equals() 而不是 ReferenceEquals()== 来做平等。 Link 1Link 2。可能是您的同事将类上的 Equals() 方法更改为以一种方式工作,更改了数据,期望数据绑定更新但它没有因为 Equals() 仍然返回 true?
  • 谢谢,我认为第二个链接的开头段落完美地解释了它(并且比我的同事更好!)。

标签: .net equality


【解决方案1】:

我想我会从上述帖子以及外部对话中提出我的摘要作为答案,而不是通过更新原始帖子来混淆主题。我将保持话题开放,让读者在选择之前投票选出他们认为最好的答案。

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

  1. 根据领域模型中的定义,实体具有身份。

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

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

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

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

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

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

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

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

???。那么我们如何在代码中表示价值平等呢?

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

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

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

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

更多想法?

【讨论】:

    【解决方案2】:

    对于引用相等,我使用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));
    

    【讨论】:

    • 因此,如果我关注您,您的 Equals 覆盖将执行完整的值平等,如果您将哈希表或字典与对象一起使用,则您为哈希表提供比较器以执行身份平等。对吗?
    • @SonOfPirate:通常。只有当我需要一个时,如果我的类主要是功能类(例如 DAO 等),我通常不会打扰。只有在 POCO 中,它才真正成为一个问题。这真的取决于我的需求是什么,我是否需要当前业务案例的价值或身份平等。
    • @SonOfPirate: 很多时候我不会覆盖 Equals() 并且只会使用投影比较器,因为我不需要定义相等的类。
    • 通过“投影比较器”,我假设您指的是 lambda 而不是显式类?
    【解决方案3】:

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

    并非所有事物都有标识,因此当涉及到没有标识的对象时,Equals() 和 == 的实际工作方式会让人感到困惑。例如,考虑一个包含多个实体或临时/辅助对象的缓存。可能基于几个不同域对象的聚合对象呢?它会比较哪个身份?

    身份平等是价值平等的一个子集,根据我的经验,无论何时涉及身份平等,价值平等都紧随其后,通常价值身份也包括身份平等。毕竟如果身份不一样,值真的一样吗?

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

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

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

    但是,在身份平等非常普遍的领域中,我会选择在我的身份基类中使用一个名为IdentityEquals() 的方法,它负责处理基级别的身份平等。

    这样我只在相关且合乎逻辑的地方公开身份平等。对于我的任何平等检查如何工作,没有任何潜在的混淆。无论是Equals()== 还是IdentityEquals / EqualityComparer&lt;T&gt;(取决于我的域内的身份平等程度)。

    另外,我建议阅读 Microsoft 的 guidelines for overloading equality

    具体来说:

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

    编辑:

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

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

    【讨论】:

    • 我同意需要一致性,并且在阅读了指南后,这就是我发布该主题的原因。我不确定 EqualityComparer 是否是答案,因为我试图避免使用一种 Equals 方法产生与另一种 Equals 方法不同的结果。我得再考虑一下。
    • FWIW - 身份相等的用例在面向服务的应用程序中很常见,其中对象从服务请求中具体化,需要与现有的域对象进行比较。这两个对象很可能在一个或多个属性上具有不同的值,但仍代表同一个实体。
    • 我会稍微澄清一下我的第一条评论。我开发其他开发人员使用的框架和工具。了解我的目标受众后,使用需要清晰且一致,否则接受度会迅速下降。
    • 刚刚更新了我的原始帖子,以反映我对如何在身份平等普遍存在的领域内解决这个问题的想法。基本上,我会在我的基本身份类上实现一个新方法——这使逻辑保持在本地、独立且仅在它真正有意义的地方可用,而不会引起混淆:Equals / == / IdentityEquals
    • 我明白了。我似乎记得看到一篇博客文章提到了使用“IsSameXYZ()”方法(其中 XYZ 是实体名称)的类似方法。
    猜你喜欢
    • 1970-01-01
    • 2012-12-18
    • 2011-08-17
    • 2011-01-07
    • 2013-10-14
    • 1970-01-01
    • 2011-04-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多