2010-10-12 57 views
7

怀旧Collections.unmodifiableMap(),我一直在实现一个基于this discussion的只读IDictionary包装,我的单元测试很快遇到了一个问题:.NET Dictionary/IDictionary的Equals()合同vs等于()Java地图的合同

Assert.AreEqual (backingDictionary, readOnlyDictionary); 

失败,即使键值对匹配。我玩了更多一点,至少看起来像(感谢西蒙尼)

Assert.AreEquals (backingDictionary, new Dictionary<..> { /* same contents */ }); 

确实通过。

我率先通过了DictionaryIDictionary文档咋一看,让我吃惊我无法找到Java Map合同两个Maps平等entrySet()s必须相等的任何等同。 (该文档说Dictionary - IDictionary - 覆盖Equals(),但不要说超越做什么。)

所以它看起来像在C#中的键值平等是Dictionary具体类的属性,而不是IDictionary接口。这是正确的吗?整个System.Collections框架通常是真的吗?

如果是这样,我有兴趣阅读一些关于为什么MS选择这种方法的讨论 - 以及首选方法是检查C#中的收集内容是否相同。

最后,我不介意指向经过充分测试的ReadOnlyDictionary实现。 :)


ETA:要清楚,我不寻找如何来测试我的工作的建议 - 这是比较琐碎。我正在寻找这些测试应该执行的合同的指导。为什么。


ETA:伙计们,我知道IDictionary是一个接口,而我知道界面无法实现的方法。在Java中是一样的。尽管如此,Java Map接口记录了equals()方法的期望certain behavior。毫无疑问,必须有.NET接口来处理这种事情,即使这些接口不在其中。

+0

WindowsBase程序集中的MS.Internal.Utility命名空间中有一个ReadOnlyDictionary 。它不会覆盖等于。 – dtb 2010-10-12 22:03:55

+1

我正在为iOS编写一个Mono应用程序,但这是一个有趣的数据点。两个“MS.Internal.Utility.ReadOnlyDictionaries”具有相同的内容吗? – 2010-10-12 22:06:16

+1

注意到后面的读者:(1)[PowerCollections]中的'Algorithms'类(http://powercollections.codeplex.com/)提供了用于将集合(包括字典)包装为只读的'ReadOnly'方法。 (2)LINQ的[SequenceEqual()](http://msdn.microsoft.com/en-us/library/bb348567.aspx)适用于有序集合(包括字典)。 – 2011-06-23 23:48:14

回答

2

对于后来的读者,这里就是我一直在说/已经能搞清楚:

  1. 的合同.NET集合, 不像Java集合,不 包括 任何特定的行为Equals()GetHashCode()
  2. LINQ Enumerable.SequenceEqual() 扩展方法将用于 有序集合,包括 字典工作 - 这存在 IEnumerable<KeyValuePair>; KeyValuePair是一个结构体,它的 Equals方法uses reflection 来比较内容。
  3. Enumerable提供了其他的扩展,可以使用凑齐 在一起的含量相等性检查,例如 如Union()Intersect() 方法。

我在我身边的想法,作为方便的Java方法是,他们可能不是最好的主意,如果我们谈论的可变集合,以及有关典型的隐equals()语义 - 两个物体可互换。 .NET不提供对不可变集合的非常好的支持,但是开源的PowerCollections库。

+0

与.NET不同,Java没有“相等比较器”的概念,当可变类型用作实体时,引用相等是有意义的;当使用作为在字典中不会改变的价值观,价值平等是有道理的,在大多数情况下,什么都不知道一个类将会比较实例,最好在报告事情不一致的方面犯错,而.NET的默认引用平等就是这样做的。在字典中存储事物的代码知道值相等更有意义的情况下,它可以指定一个用于测试的相等比较器。 – supercat 2013-11-13 20:14:58

+0

因为Java没有相等比较器类型,所以类型可以使它们的值可用作字典键的唯一方法是覆盖它们的相等方法以使用值相等性,并且希望被用作实体的实例不会存储到字典中。 – supercat 2013-11-13 20:16:53

3

重写equals通常只能用具有一定数值语义的类来完成(例如string)。引用相等是人们更经常关心的大多数引用类型和良好的默认值,特别是在不太清楚的情况下(两个字典具有完全相同的键值对但不同的相等比较器[因此添加相同的额外键值对可能会使它们现在不同]相等或不相等?),或者不会经常查找值相等。

毕竟,你正在寻找一种情况,其中两种不同的类型被认为是平等的。平等覆盖可能会让你失望。

更何况,你总是可以创建自己的相等比较器的速度不够快:

public class SimpleDictEqualityComparer<TKey, TValue> : IEqualityComparer<IDictionary<TKey, TValue>> 
{ 
    // We can do a better job if we use a more precise type than IDictionary and use 
    // the comparer of the dictionary too. 
    public bool Equals(IDictionary<TKey, TValue> x, IDictionary<TKey, TValue> y) 
    { 
     if(ReferenceEquals(x, y)) 
      return true; 
     if(ReferenceEquals(x, null) || ReferenceEquals(y, null)) 
      return false; 
     if(x.Count != y.Count) 
      return false; 
     TValue testVal = default(TValue); 
     foreach(TKey key in x.Keys) 
      if(!y.TryGetValue(key, out testVal) || !Equals(testVal, x[key])) 
       return false; 
     return true; 
    } 
    public int GetHashCode(IDictionary<TKey, TValue> dict) 
    { 
     unchecked 
     { 
      int hash = 0x15051505; 
      foreach(TKey key in dict.Keys) 
      { 
       var value = dict[key]; 
       var valueHash = value == null ? 0 : value.GetHashCode(); 
       hash ^= ((key.GetHashCode() << 16 | key.GetHashCode() >> 16)^valueHash); 
      } 
      return hash; 
     } 
    } 
} 

这不会成为其中一个需要比较字典所有可能的情况,但是呢,这是我的观点。

用“可能意味着什么”的平等方法填补BCL会是一种麻烦,而不是帮助。

+0

我承认,相等比较器增加了Java没有的额外皱纹。 – 2010-10-12 22:43:17

+0

@David,Java有一些定义对象之间等价的外部定义的方法,但是,当然? – 2010-10-12 22:46:46

+0

是和不是。有'Comparator',如果你的'Comparator.compareTo(a,b)'的实现返回0,'a'和'b'应该是相等的。但是,通常没有人发现这种情况,直到他们构造了一个TreeSet或TreeMap(缺省的排序实现),并且由于比较器实现的错误,成员开始退出。标准库中没有任何类似于“IEqualityComparer”的东西 - 集合只使用equals()。 (除了,如上所述,'TreeMap/TreeSet',它采取快捷方式。) – 2010-10-12 22:55:55

2

我建议使用NUnit的CollectionAssert.AreEquivalent()。 Assert.AreEqual()实际上不适用于集合。 http://www.nunit.org/index.php?p=collectionAssert&r=2.4

+0

这很有趣。它看起来像'CollectionAssert.AreEqual()'确实通过。 - 不,我收回它,它不。但它应该。也许我真的有一个错误。 – 2010-10-12 22:59:10

+0

当然珍藏是你想要的。 – 2010-10-13 18:50:21

+0

您可以评论为什么CollectionAssert不合适? – 2010-10-26 18:04:47

1
public sealed class DictionaryComparer<TKey, TValue> 
    : EqualityComparer<IDictionary<TKey, TValue>> 
{ 
    public override bool Equals(
     IDictionary<TKey, TValue> x, IDictionary<TKey, TValue> y) 
    { 
     if (object.ReferenceEquals(x, y)) return true; 
     if ((x == null) || (y == null)) return false; 
     if (x.Count != y.Count) return false; 

     foreach (KeyValuePair<TKey, TValue> kvp in x) 
     { 
      TValue yValue; 
      if (!y.TryGetValue(kvp.Key, out yValue)) return false; 
      if (!kvp.Value.Equals(yValue)) return false; 
     } 
     return true; 
    } 

    public override int GetHashCode(IDictionary<TKey, TValue> obj) 
    { 
     unchecked 
     { 
      int hash = 1299763; 
      foreach (KeyValuePair<TKey, TValue> kvp in obj) 
      { 
       int keyHash = kvp.Key.GetHashCode(); 
       if (keyHash == 0) keyHash = 937; 

       int valueHash = kvp.Value.GetHashCode(); 
       if (valueHash == 0) valueHash = 318907; 

       hash += (keyHash * valueHash); 
      } 
      return hash; 
     } 
    } 
} 
+0

小建议:使用'EqualityComparer .Default.Equals(kvp.Value,yValue)'来避免'Equals'方法中的空例外。同样在'GetHashCode'方法中,你需要检查'kvp.Value == null'。 – nawfal 2013-11-08 23:35:03

1

所以它看起来像在 C#键值平等是字典 具体类的属性,而不是IDictionary的 接口。这是正确的吗? System.Collections框架是否全部为 ?

如果是这样,我会有兴趣看的,为什么MS选择一些 讨论中 方法

我觉得这是很简单 - IDictionary是一个接口,接口不能有任何实现和在.NET中,两个对象的世界平等是通过Equals方法定义的。因此,为IDictionary接口重写Equals以允许其拥有“键值相等”是不可能的。

0

你在原文中犯了一个大错。您在IDictionary界面中提到了Equals()方法。这才是重点!

Equals()是System.Object的虚拟方法,类可以覆盖。接口根本不实现方法。相反,接口实例是参考类型,因此继承自System.Object,可能声明覆盖Equals()

现在点... System.Collections.Generic.Dictionary<K,V>确实不是覆盖等于。你说你实现你的IDictionary你自己的方式,合理重写平等相待,而是看你自己的代码

Assert.AreEqual (backingDictionary, readOnlyDictionary); 

这种方法基本上实现为return backingDictionary.Equals(readOnlyDictionary),并再次在这里是点。

如果两个对象是不同类的实例,则基本Equals()方法返回false,但无法控制该对象。否则,如果两个对象的类型相同,则使用Equals()方法而不是==(这是手册所称的“值比较”而不是“参考比较”)通过反射(仅成员而非属性)比较每个成员。

因此,首先,如果Assert.AreEqual (readOnlyDictionary,backingDictionary);成功,我不会感到惊讶,因为它会触发用户定义的Equals方法。

我毫不怀疑其他用户在这个线程中的工作方式,但我只是想解释一下你原来的方法中的错误。毫无疑问,微软会更好地实现Equals方法,该方法将当前实例与任何其他IDictionary实例进行比较,但是,这又会超出Dictionary类的范围,该类是公共独立类,并不意味着IDictionary的唯一可用公共实现。例如,当您定义一个接口,一个工厂和一个在库中实现它的受保护类时,您可能希望将该类与基本接口的其他实例进行比较,而不是该类本身不公开。

我希望能对你有所帮助。 干杯。

+1

对不起,我说话不太准确。这在Java中是一样的 - equals()是Object中的一个方法。然而,Java'Map'接口“覆盖”equals()(不是真的;它只是重新声明它)来_document_ _expectation_它返回true对任何两个具有相同键值映射的Map实现。这些是.NET集合中的期望,以及我想知道的原因。 – 2010-10-25 23:42:45

+0

现在明白了......您可能需要阅读.NET开发人员的头脑:( – 2010-10-26 20:41:43

+0

@djechelon:如果使用两个引用来封装标识,那么当且仅当它们标识相同的对象时,它们应该相等。如果它们封装了同一个状态,那么它应该是相等的。不幸的是,.NET和Java都没有区分应该封装身份的引用和那些不应该引用的引用 - 这种限制不会使复杂的等式测试变得复杂和其他东西一起克隆,但它是许多与可变性相关的bug的基础。 – supercat 2013-11-20 23:07:51