2011-11-02 73 views
4

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

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

  2. 身份平等 - 意味着两个对象都具有相同的身份值。 例如,具有相同订货号 的两个订单对象表示相同的实体。当将值存储在列表,哈希表等中时,这是特别重要的,并且 对象需要唯一标识用于查找。

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

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

当然,我忽略了MS添加的Object.ReferenceEquals,因为认识到大多数人都在压倒Equals,因为参照平等不是他们想要的行为。所以也许我们可以将第一种类型(?)分开。

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

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

您的想法?

UPDATE

我不知道所有的细节,但我被告知(在与同事的面对面的谈话)是WPF有数据绑定对象使用的Equals参考平等的严格要求或数据绑定无法正常工作。

另外,看着典型的Assert类,还有更令人困惑的语义。 AreEqual(a,b)通常会使用暗示着Identity或Value Equality的Equals方法,而AreSame(a,b)将ReferenceEquals用于引用相等。

+0

到目前为止,我发现的一切似乎都指向WPF,使用'Equals()'而不是'ReferenceEquals()'或'=='来执行相等操作。 [链接1](http://www.lhotka.net/weblog/DataRefreshInWPF.aspx)[链接2](http://kentb.blogspot.com/2007/03/beware-datacontext-and-equals.html) 。可能是因为你的同事改变了类的'Equals()'方法以单向工作,改变了数据,期望数据绑定更新,并且它不是因为Equals()仍然返回true? –

+0

谢谢,我认为第二个链接的开头段落完美地解释了它(并且比我的同事更好!)。 – SonOfPirate

回答

0

我想我会建议从上面的帖子我的总结以及外部对话作为一个答案,而不是通过更新原来的职位迷惑的话题。我会把这个话题放开,让读者在选择之前投票选出他们认为最好的答案。

下面是我从这些讨论gleened的关键点:

  1. 实体的定义本身就在域模型具有同一性。

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

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

  4. 微软指南指出,当GetHashCode()方法的两个对象是相等的,equals需要对这些对象返回true。

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

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

真相被告知,我已经意识到的是,这可能只是一个语法/语义问题。我们需要第三种定义平等的方式。我们有两个:

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

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

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

在我所有的谈话中,我发现我们正在应用限定词来形式化这些术语;使用“IdentityEquals”和“IsSameXYZ”等名称,因此“Equals”意味着价值平等或“IsEquivalentTo”和“ExactlyEquals”意味着价值平等,因此“Equals”意味着身份平等。

虽然我很欣赏的灵活性,更多的我走这条道路,我就越意识到没有任何开发此看到相同的方式。这会导致问题。

而且我可以告诉你,每一个开发者,我谈过,一个一个,表明他们希望“==”表现得正好等于相同。但是,即使我们覆盖Equals,微软建议不要重载“==”。如果核心==运算符只是简单地委托给Equals,那本来就不错。

因此,底线,我会忽略equals提供身份平等,提供我们的基类,用于参照平等sameAs的方法(只是一个的ReferenceEquals方便包装)和重载==等于什么,他们是一致的。然后,我将使用比较器来“比较”两个“相等”实体的值。

更多想法?

1

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

对于2和3,它真的取决于开发人员想要什么,如果他们想要将相等定义为身份或值相等。通常,我喜欢将Equals()保留为值相等,然后为外部比较器提供身份相等。

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

正如我所说,这是我的典型用法,我也构造的对象模型在那里我只考虑属性的子集来表示身份和其他人不比较。

您可以随时创建一个非常简单的ProjectionComparer,它可以接受任何类型的数据并根据投影创建一个比较器,使得在需要的地方传递自定义比较器以识别标识等非常容易,并且只需等待Equals()方法值。

而且,通常情况下,我个人不超载==除非我写的是需要典型的比较操作的值类型,因为运算符重载和超载怎么都没有覆盖这么大的混乱。

但同样,这只是我的:-)

UPDATE意见这里是我的投影比较器,你可以找到许多其他的实现,当然,不过这一次很适合我,它实现了两个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

所以,如果我遵循你,你的Equals覆盖将执行完整的Value Equality,并且如果你使用散列表或字典与对象一起提供一个比较器来散列表来执行Identity Equality。正确? – SonOfPirate

+0

@SonOfPirate:通常。这只是如果我需要一个,如果我的类主要是一个功能类(如DAO等),我通常不会打扰。只有在POCO中它真的成为一个问题。这只取决于我的需求,我是否需要价值或身份平等为目前的商业案例。 –

+0

@SonOfPirate:有很多次我不会覆盖Equals(),只会使用投影比较器,因为我不需要定义相等的类。 –

1

我通常发展我的域模型的方式是围绕==ReferenceEquals()进行参考平等。和Equals()执行价值平等。我用这些都不对身份平等的原因有三方面:

并非事事有身份,所以会引起有关如何混淆的Equals()和==其实当无身份对象参与工作。想一想有关包含多个实体的缓存或临时/帮助对象的例子。关于可能基于几个不同的域对象的聚合对象呢?它会比较哪个身份?

身份平等是价值平等的一个子集,根据我的经验,每当涉及身份平等时,价值平等并不遥远,通常价值身份包括身份平等。毕竟,如果身份不一样,这些值是否真的是一样的?

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

那么为什么使用任何内建的等式方法(==Equals())来处理例外情况,而不是规则?相反,我倾向于实现一个基类,该基类提供我的身份信息并实现身份平等,具体取决于身份平等在我当前的域中的普遍程度。

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

但是,在身份平等很常见的域中,我会选择一个名为IdentityEquals()的身份基类中的方法,该方法负责基本级别上的身份相等性。

这样我只在相关性和逻辑性的地方暴露身份平等。没有任何潜在的混淆关于我的任何平等检查可能如何工作。无论是Equals(),==IdentityEquals/EqualityComparer<T>(取决于身份平等在我的域中的普遍程度)。

另外,作为一个方面说明我会建议阅读微软的guidelines for overloading equality

具体做法是:

默认情况下,运营商==参考相等测试通过 确定两个引用是否表示相同的对象,所以引用 类型并不需要实现==操作符,以获得这 功能。当类型是不可变的,这意味着包含在 的实例中的数据不能被改变,重载运算符==比较 值相等,而不是参考平等可以是有用的,因为如 不可变的对象,它们可以,只要认为是相同的他们 具有相同的值。 不推荐使用非不可变类型覆盖运算符== 。

编辑:

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

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

+0

我同意一致性是需要的,阅读指南后,这就是我发布该主题的原因。我不确定EqualityComparer是否是答案,因为一个Equals方法产生与另一个Equals方法不同的结果是我试图避免的。我将不得不更多考虑。 – SonOfPirate

+0

FWIW - 在面向服务的应用程序中,身份相等的用例很常见,其中一个对象是从服务请求实现的,需要与现有的域对象进行比较。这两个对象很可能对一个或多个属性具有不同的值,但仍然代表相同的实体。 – SonOfPirate

+0

我会澄清我的第一条评论。我开发了其他开发人员使用的框架和工具。了解我的目标受众,使用需要清晰,一致或快速接受。 – SonOfPirate