【问题标题】:Equals() contract for .NET Dictionary / IDictionary vs equals() contract for Java Map.NET Dictionary / IDictionary 的 Equals() 合同与 Java Map 的 equals() 合同
【发布时间】:2011-04-24 13:52:14
【问题描述】:

怀念Collections.unmodifiableMap(),我一直在基于this discussion实现一个只读的IDictionary包装器,我的单元测试很快就遇到了问题:

Assert.AreEqual (backingDictionary, readOnlyDictionary);

失败,即使键值对匹配。我又玩了一点,看起来至少(感谢 Simonyi)

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

确实通过了。

我快速浏览了DictionaryIDictionary 文档,令我惊讶的是,我找不到任何等效的Java Map 合约,即两个MapsentrySet()s 必须相等. (文档说 Dictionary -- not IDictionary -- 覆盖 Equals(),但不要说覆盖的作用。)

所以看起来 C# 中的键值相等是Dictionary 具体类的属性,而不是IDictionary 接口的属性。这是正确的吗?整个System.Collections 框架是否普遍如此?

如果是这样,我有兴趣阅读一些关于 MS 选择该方法的讨论 - 以及在 C# 中检查集合内容是否相等的首选方法。

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


预计到达时间:明确地说,我不是寻找有关如何测试我的实现的建议——这相对来说是微不足道的。我正在寻找有关这些测试应该执行什么合同的指导。以及为什么。


ETA:伙计们,我知道IDictionary是一个接口,我知道接口不能实现方法。在 Java 中也是如此。尽管如此,Java 的Map 接口记录了来自equals() 方法的certain behavior 的期望。肯定有 .NET 接口可以做这样的事情,即使集合接口不在其中。

【问题讨论】:

  • WindowsBase 程序集的 MS.Internal.Utility 命名空间中有一个 ReadOnlyDictionary。它不会覆盖 Equals。
  • 我正在为 iOS 编写一个 Mono 应用程序,但这是一个有趣的数据点。两个MS.Internal.Utility.ReadOnlyDictionaries的内容相同吗?
  • 为以后的读者注意:(1)PowerCollections 中的Algorithms 类提供了ReadOnly 将集合(包括字典)包装为只读的方法。 (2) LINQ 的SequenceEqual() 适用于有序集合(包括字典)。

标签: java .net collections immutability equality


【解决方案1】:

覆盖 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 会很麻烦,而不是帮助。

【讨论】:

  • 我承认,相等比较器增加了 Java 所没有的额外皱纹。
  • @David,Java 有一些方法可以定义对象之间等价的外部定义,确定吗?
  • 是和不是。有Comparator,如果你的Comparator.compareTo(a, b) 实现返回0,ab 应该是相等的。通常没有人会发现这一点,直到他们构造了一个 TreeSetTreeMap (默认排序实现)并且由于糟糕的 Comparator 实现而成员开始退出它。在标准库中并没有像IEqualityComparer 这样的东西——集合只使用equals()。 (如前所述,TreeMap/TreeSet 除外,它走捷径。)
  • @Jon:一个小小的挑剔:你的GetHashCode 方法依赖于dict.Keys 的排序,并且不能以任何方式保证排序。理论上,两个包含相同键值对集的 IDictionary&lt;K,V&gt; 实例可以产生不同的哈希码,即使它们比较相等。
  • 啊,我认为 Comparator 不支持弱排序,即事物可以在同一位置排序,但不相等。我想我更喜欢 IEqualityComparer/IComparer 的拆分(以及用于在类中定义默认值的可比较的 IEquatable/IComparable),但是您所描述的大部分时间仍然可以解决问题。你让我惊慌失措,因为我不太看 Java,但我对它可能没有任何这样的等价提供者的想法感到震惊,而且我可能没有注意到!
【解决方案2】:

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

【讨论】:

  • 这很有趣。看起来CollectionAssert.AreEqual() 确实通过了。 ——不,我收回了这一点,它没有。但它应该。也许我真的有一个错误。
  • 当然 CollectionAssert 就是你想要的。
  • 你能评论一下为什么 CollectionAssert 不适合吗?
【解决方案3】:

对于后来的读者,这是我被告知/能够弄清楚的:

  1. .NET 集合的合同, 与 Java 集合不同,不 包括任何特定的行为 Equals()GetHashCode()
  2. LINQEnumerable.SequenceEqual() 扩展方法将适用于 有序集合,包括 字典——呈现为 IEnumerable&lt;KeyValuePair&gt;; KeyValuePair 是一个结构体,它的 Equals 方法 uses reflection 比较内容。
  3. Enumerable 提供其他扩展 可用于拼凑的方法 一起进行内容平等检查,例如 如Union()Intersect()

我想,虽然 Java 方法很方便,但如果我们谈论可变集合和典型的隐式 equals() 语义,它们可能不是最好的主意——这两个 @ 987654333@ 对象是可互换的。 .NET 没有为不可变集合提供很好的支持,但是开源的 PowerCollections 库可以。

【讨论】:

  • Java 与 .NET 不同,没有“相等比较器”的概念。当可变类型用作实体时,引用相等是有意义的;当用作在字典中不会改变的值时,值相等是有意义的。在大多数情况下,对类一无所知的东西将比较实例,最好在报告不相等的情况下犯错,而 .NET 的默认引用相等就是这样做的。如果在字典中存储东西的代码知道值相等更有意义,它可以指定一个相等比较器来测试它。
  • 由于 Java 没有相等比较器类型,类型使它们的值可用作字典键的唯一方法是覆盖它们的相等相关方法以使用值相等,并希望实例是被用作实体不会被存储到字典中。
【解决方案4】:
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;
        }
    }
}

【讨论】:

  • 小建议:使用EqualityComparer&lt;TValue&gt;.Default.Equals(kvp.Value, yValue) 避免Equals 方法中出现空异常。同样在GetHashCode 方法中,您需要检查kvp.Value == null
【解决方案5】:

所以它看起来像键值相等 C# 是字典的属性 具体类,而不是 IDictionary 界面。这是正确的吗?是吗 总体上是正确的 System.Collections 框架?

如果是这样,我有兴趣阅读一些 讨论为什么 MS 选择它 接近

我认为这很简单——IDictionary 是一个接口,接口不能有任何实现,在 .NET 世界中,两个对象的相等性是通过Equals 方法定义的。所以不可能重写 IDictionary 接口的Equals 以允许它拥有“键值相等”。

【讨论】:

    【解决方案6】:

    您在原始帖子中犯了一个大错误。您谈到了IDictionary 接口中的Equals() 方法。这就是重点!

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

    现在重点...System.Collections.Generic.Dictionary&lt;K,V&gt; 确实 覆盖 Equals。您说您以自己的方式实现了 IDictionary,并合理地覆盖了 Equals,但请查看您自己的代码

    Assert.AreEqual (backingDictionary, readOnlyDictionary); 
    

    这个方法基本上实现为return backingDictionary.Equals(readOnlyDictionary),这也是重点。

    如果两个对象是不同类的实例,则基本 Equals() 方法返回 false,您无法控制。否则,如果两个对象属于同一类型,则使用 Equals() 方法而不是 == (这就是手册所说的“值比较”而不是“参考比较")

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

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

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

    【讨论】:

    • 对不起,我说得不准确。 Java 中的情况也是如此——equals()Object 上的一个方法。然而,Java Map 接口“覆盖”equals()(不是真的;它只是重新声明它)document expectation 它对任何两个返回 true Map 具有相同键-> 值映射的实现。这就是 .NET 集合中的这些期望,以及为什么,我想知道。
    • 现在明白了...您可能需要深入了解 .NET 开发人员的想法 :(
    • @djechelon:如果使用两个引用来封装身份,当且仅当它们标识相同的对象时,它们应该比较相等。封装状态且不封装身份的两个非共享引用如果封装相同的状态,则它们应该比较相等。不幸的是,.NET 和 Java 都没有对应该封装身份的引用和不应该封装身份的引用进行任何区分——这一限制不会使相等性测试和克隆等复杂化,而是许多与可变性相关的错误的潜在原因。跨度>
    猜你喜欢
    • 2015-06-20
    • 1970-01-01
    • 2021-11-01
    • 2015-07-20
    • 2011-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-04
    相关资源
    最近更新 更多