.NET域模型中的对象平等

时间:2011-11-02 19:14:45

标签: .net equality

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

  1. Referential Equality - 意味着两个对象都存储在同一个中                       物理记忆空间。

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

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

  4. 按照惯例,.NET提供了两种测试相等性的方法: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进行指称平等。

3 个答案:

答案 0 :(得分:1)

对于引用相等,我按照你的说法使用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));

答案 1 :(得分:1)

我通常开发域模型的方式是==ReferenceEquals()执行引用相等。 Equals()执行价值平等。我没有使用这些用于身份平等的原因有三个:

并非所有内容都具有身份,因此当涉及没有身份的对象时,会导致混淆Equals()和==如何实际工作。例如,考虑一个包含多个实体或临时/辅助对象的缓存。那些可能基于几个不同域对象的聚合对象呢?它会比较哪种身份?

身份平等是价值平等的一个子集,根据我的经验,只要涉及身份平等,价值平等就不会落后,通常价值身份也包括身份平等。毕竟,如果身份不相同,那么值是否真的相同?

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

那么为什么要使用任何内置的相等方法(==Equals())作为异常,而不是规则呢?相反,我倾向于实现一个基类,它提供我的身份信息并实现身份相等,具体取决于我当前域中的常见身份相等性。

例如;在身份相等非常罕见的域中,如果身份相等不是我当前域中的常见问题,我将创建一个自定义EqualityComparer<T>以在上下文敏感的方式中在需要的时间和地点执行身份相等。

但是,在身份相同非常普遍的域中,我会在我的身份基类中选择一个名为IdentityEquals()的方法,该方法负责基类级别的身份相同。

这样我只会在相关且合乎逻辑的情况下公开身份相等。没有任何关于我的任何平等检查可能如何工作的混淆。无论是Equals()==还是IdentityEquals / EqualityComparer<T>(取决于我的域中的共同身份相同性)。

另外作为附注,我建议阅读微软的guidelines for overloading equality

具体做法是:

  

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

修改

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

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

答案 2 :(得分:0)

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

以下是我从这些讨论中获得的关键点:

  1. 实体在域模型中的定义具有一致性。

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

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

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

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

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

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

    <强>等于即可。在域模型中,当两个实体共享相同的标识时,它们相等。我觉得必须是这样的,以满足#4&amp; #5以上。我们使用实体的标识来生成从GetHashCode返回的哈希码,因此,必须使用相同的值来确定相等

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

    <强> ??? 即可。那么我们如何在代码中指出价值平等?

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

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

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

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

    更多想法?