【问题标题】:Testing for equality between dictionaries in C#在 C# 中测试字典之间的相等性
【发布时间】:2011-04-17 18:58:00
【问题描述】:

假设字典键和值的equals和hash方法正确实现,那么测试两个字典是否相等的最简洁和有效的方法是什么?

在这种情况下,如果两个字典包含相同的键集(顺序不重要),则称它们相等,并且对于每个这样的键,它们的值都一致。

以下是我想出的一些方法(可能还有更多):

public bool Compare1<TKey, TValue>(
    Dictionary<TKey, TValue> dic1, 
    Dictionary<TKey,TValue> dic2)
{
    return dic1.OrderBy(x => x.Key).
        SequenceEqual(dic2.OrderBy(x => x.Key));
}

public bool Compare2<TKey, TValue>(
    Dictionary<TKey, TValue> dic1, 
    Dictionary<TKey, TValue> dic2)
{
    return (dic1.Count == dic2.Count && 
        dic1.Intersect(dic2).Count().
        Equals(dic1.Count));
}

public bool Compare3<TKey, TValue>(
    Dictionary<TKey, TValue> dic1, 
    Dictionary<TKey, TValue> dic2)
{
    return (dic1.Intersect(dic2).Count().
        Equals(dic1.Union(dic2).Count()));
}

【问题讨论】:

    标签: c# dictionary equality


    【解决方案1】:
    dic1.Count == dic2.Count && !dic1.Except(dic2).Any();
    

    【讨论】:

    • 为什么这是正确的?它不尊重所需的值相等。它只是检查两个字典中是否存在所有键。
    • @SebastianP.R.Gingter:Dictionary&lt;TKey, TValue&gt;&gt; 也是IEnumerable&lt;KeyValuePair&lt;TKey, TValue&gt;&gt; 的一个实例。因此,您正在比较 KeyValuePair&lt;TKey, TValue&gt; 的实例,如果键和值都相等,则它们相等。
    • 为什么会被接受和支持?它没有执行 OP 要求的操作,即 并且对于每个这样的键,他们就值达成一致。
    • 我相信这个答案只有在字典的键和值类型只使用内置类型或正确设置 IEqualityComparer 的自定义类时才有效。虽然,我会使用dict1.SequenceEqual(dict2)。如果键或值是集合,例如 List,它将不起作用。 (见我的回答。)
    • 这个答案是正确的“假设 [所有] 字典键和值都正确实现了它们的等值和哈希方法” - 方法 except() 将在字典中的KeyValuePairs,每个KeyValuePair 将委托给键和值上的EqualsGetHashCode 方法(因此必须正确实现这些方法)。如果键和值是列表或字典,这将无法按预期工作,因为这些类型仅使用 EqualsGetHashCode 的引用相等。
    【解决方案2】:

    这真的取决于你所说的平等。

    此方法将测试两个字典是否包含具有相同值的相同键(假设两个字典使用相同的IEqualityComparer&lt;TKey&gt; 实现)。

    public bool CompareX<TKey, TValue>(
        Dictionary<TKey, TValue> dict1, Dictionary<TKey, TValue> dict2)
    {
        if (dict1 == dict2) return true;
        if ((dict1 == null) || (dict2 == null)) return false;
        if (dict1.Count != dict2.Count) return false;
    
        var valueComparer = EqualityComparer<TValue>.Default;
    
        foreach (var kvp in dict1)
        {
            TValue value2;
            if (!dict2.TryGetValue(kvp.Key, out value2)) return false;
            if (!valueComparer.Equals(kvp.Value, value2)) return false;
        }
        return true;
    }
    

    【讨论】:

    • 你不是在清空字典吗? comparex 将失败第二次调用,因为第二个参数是空的。为什么要修改字典 - 这不违反关于简单相等检查的原则吗?
    • @Ani:我真的不明白这会有什么帮助。生成和比较哈希将需要通过两个字典,读取键和值。如果我们生成并比较这些键和值的哈希,那么我们会得到一个“高概率”的结果;如果我们直接比较它们,我们会得到一个准确的答案。我忽略了什么吗?
    • @rony:方法的第一行负责处理。
    • 这比尼克的回答更有效率吗? dic1.Count == dic2.Count && !dic1.Except(dic2).Any();
    • @rony:Except 方法的工作方式与我的回答类似。性能应该非常接近,尽管我预计我的可能会有 slight 优势:Except 方法需要初始通过 dic2 来构造一个单独的集合。您需要对自己进行基准测试才能确定,但​​如果有任何重大差异,我会感到惊讶。
    【解决方案3】:

    您可以使用 linq 进行键/值比较:

    public bool Compare<TKey, TValue>(Dictionary<TKey, TValue> dict1, Dictionary<TKey, TValue dict2)
    {
        IEqualityComparer<TValue> valueComparer = EqualityComparer<TValue>.Default;
    
        return  dict1.Count == dict2.Count &&
                dict1.Keys.All(key => dict2.ContainsKey(key) && valueComparer.Equals(dict1[key], dict2[key]));
    }
    

    【讨论】:

    • TValue val; return dict1.Count == dict2.Count &amp;&amp; dict1.All(x =&gt; dict2.TryGetValue(x.Key, out val) &amp;&amp; valueComparer.Equals(x.Value, val)); 呢?
    【解决方案4】:

    @Allen's answer:

    bool equals = a.Intersect(b).Count() == a.Union(b).Count()
    

    是关于数组,但只要使用IEnumerable&lt;T&gt;方法,它也可以用于Dictionary&lt;K,V&gt;

    【讨论】:

      【解决方案5】:

      我认为接受的答案是正确的,基于我在 smarthelp 中阅读的 except 方法:“通过使用默认相等比较器比较值来产生两个序列的集合差异。”但我发现这不是一个好的答案。

      考虑这段代码:

      Dictionary<string, List<string>> oldDict = new Dictionary<string, List<string>>()
          {{"001A", new List<string> {"John", "Doe"}},
           {"002B", new List<string> {"Frank", "Abignale"}},
           {"003C", new List<string> {"Doe", "Jane"}}};
      Dictionary<string, List<string>> newDict = new Dictionary<string, List<string>>()
          {{"001A", new List<string> {"John", "Doe"}},
           {"002B", new List<string> {"Frank", "Abignale"}},
           {"003C", new List<string> {"Doe", "Jane"}}};
      
      bool equal = oldDict.Count.Equals(newDict.Count) && !oldDict.Except(newDict).Any();
      Console.WriteLine(string.Format("oldDict {0} newDict", equal?"equals":"does not equal"));
      equal = oldDict.SequenceEqual(newDict);
      Console.WriteLine(string.Format("oldDict {0} newDict", equal ? "equals" : "does not equal"));
      
      Console.WriteLine(string.Format("[{0}]", string.Join(", ", 
          oldDict.Except(newDict).Select(k => 
              string.Format("{0}=[{1}]", k.Key, string.Join(", ", k.Value))))));
      

      这会导致以下结果:

      oldDict does not equal newDict
      oldDict does not equal newDict
      [001A=[John, Doe], 002B=[Frank, Abignale], 003C=[Doe, Jane]]
      

      如您所见,“oldDict”和“newDict”的设置完全相同。建议的解决方案和对 SequenceEqual 的调用都不能正常工作。我想知道这是否是由于使用延迟加载或为字典设置比较器的方式的结果。 (虽然,查看结构和参考解释表明它应该。)

      这是我想出的解决方案。请注意,我使用的规则如下:如果两个字典都包含相同的键并且每个键的值匹配,则它们是相等的。键和值的顺序必须相同。而且我的解决方案可能不是最有效的,因为它依赖于遍历整个键集。

      private static bool DictionaryEqual(
          Dictionary<string, List<string>> oldDict, 
          Dictionary<string, List<string>> newDict)
      {
          // Simple check, are the counts the same?
          if (!oldDict.Count.Equals(newDict.Count)) return false;
      
          // Verify the keys
          if (!oldDict.Keys.SequenceEqual(newDict.Keys)) return false;
      
          // Verify the values for each key
          foreach (string key in oldDict.Keys)
              if (!oldDict[key].SequenceEqual(newDict[key]))
                  return false;
      
          return true;
      }
      

      如果出现以下情况,还可以查看结果如何变化: 键顺序不一样。 (返回假)

      newDict = new Dictionary<string, List<string>>()
          {{"001A", new List<string> {"John", "Doe"}},
           {"003C", new List<string> {"Doe", "Jane"}},
           {"002B", new List<string> {"Frank", "Abignale"}}};
      

      和 键顺序匹配,但值不匹配(返回 false)

      newDict = new Dictionary<string, List<string>>()
          {{"001A", new List<string> {"John", "Doe"}},
           {"002B", new List<string> {"Frank", "Abignale"}},
           {"003C", new List<string> {"Jane", "Doe"}}};
      

      如果序列顺序无关紧要,可以将函数更改为以下,但可能会影响性能。

      private static bool DictionaryEqual_NoSort(
          Dictionary<string, List<string>> oldDict,
          Dictionary<string, List<string>> newDict)
      {
          // Simple check, are the counts the same?
          if (!oldDict.Count.Equals(newDict.Count)) return false;
      
          // iterate through all the keys in oldDict and
          // verify whether the key exists in the newDict
          foreach(string key in oldDict.Keys)
          {
              if (newDict.Keys.Contains(key))
              {
                  // iterate through each value for the current key in oldDict and 
                  // verify whether or not it exists for the current key in the newDict
                  foreach(string value in oldDict[key])
                      if (!newDict[key].Contains(value)) return false;
              }
              else { return false; }
          }
      
          return true;
      }
      

      检查 DictionaryEqual_NoSort 是否使用以下 for newDict(DictionaryEquals_NoSort 返回 true):

      newDict = new Dictionary<string, List<string>>()
          {{"001A", new List<string> {"John", "Doe"}},
           {"003C", new List<string> {"Jane", "Doe"}},
           {"002B", new List<string> {"Frank", "Abignale"}}};     
      

      【讨论】:

      • 在我的 DictionaryEquals 方法中,我不确定是否需要进行计数检查。 SequenceEqual 已经这样做了吗?
      • 另外,如果我设置的已接受答案和失败的证明不正确,请随时纠正我。
      • 我很惊讶List&lt;String&gt; 没有正确返回Equals。对于没有覆盖 Equals 的自定义类,我可以看到它失败了,但我很惊讶地看到列表中的这种行为。
      • @Macchtyn List 不会覆盖 Equals 和 Hashcode。因此,即使列表包含“相同”元素,原始示例中的 Except 调用也会为列表获得 Equals false - 它们正在使用引用相等性进行比较,这显然是错误的。
      【解决方案6】:

      除了@Nick Jones 的回答之外,您还需要以相同的、与顺序无关的方式实现 gethashcode。我会建议这样的事情:

      public override int GetHashCode()
      {
              var hash = 13;
              var orderedKVPList = this.DictProp.OrderBy(kvp => kvp.Key);
              foreach (var kvp in orderedKVPList)
              {
                       hash = (hash * 7)  + kvp.Key.GetHashCode();
                       hash = (hash * 7)  + kvp.Value.GetHashCode();
              }
              return hash;
      }
      

      【讨论】:

      • 嗯,我不太确定。每当您覆盖对象上的实际Equals 方法时,当然可以。但是在这种情况下,你需要确保你的类型是不可变的,否则如果你把它放入一个集合然后改变它的状态,它就会丢失。所以我不认为覆盖Equals(和哈希码)是我们想要的,因为字典是可变的。我认为这就是为什么您会在其他答案中注意到小心使用“Compare”和“DictEquals”等方法名称而不是“Equals”本身的原因。
      【解决方案7】:

      简单的 O(N) 时间、O(1) 空间解决方案,带有空值检查

      使用 Set 操作 IntersectUnionExcept 的其他解决方案很好,但这些需要额外的 O(N) 内存用于最终结果字典,该字典仅用于计数元素。

      改为使用 Linq Enumerable.All 来检查这一点。首先验证两个字典的计数,然后遍历所有 D1 的键值对并检查它们是否等于D2 的键值对。 注意: Linq 确实为集合迭代器分配内存,但它与集合大小无关 - O(1) 空间。 Amortized TryGetValue 的复杂度为 O(1)。

      // KV is KeyValue pair      
      var areDictsEqual = d1.Count == d2.Count && d1.All(
           (d1KV) => d2.TryGetValue(d1KV.Key, out var d2Value) && (
                d1KV.Value == d2Value ||
                d1KV.Value?.Equals(d2Value) == true)
      );
      
      • 为什么是d1KV.Value == d2Value? - 这是检查对象引用是否相等。此外,如果两者都是 nulld1KV.Value == d2Value 将评估为 true

      • 为什么是d1Kv.Value?.Equals(d2Value) == true? - Value?. 用于空值安全检查,.Equals 用于根据对象的 Equals 和 HashCode 方法测试两个对象的相等性。

      您可以根据需要调整相等性检查。我假设字典值是nullable 类型以使解决方案更通用(例如:string, int?, float?)。如果它是不可为空的类型,则可以简化检查。


      最后说明:在 C# 字典中,Keys 不能为空。但值可以为空。 Docs for reference.

      【讨论】:

        【解决方案8】:

        如果两个字典包含相同的键,但顺序不同,它们应该被认为是相等的吗?如果不是,则应通过同时运行枚举器来比较字典。这可能比通过一个字典枚举并在另一个字典中查找每个元素要快。如果您预先知道相等的字典将具有相同顺序的元素,那么这种双重枚举可能是要走的路。

        【讨论】:

        • 取决于您的应用程序,我想。在我的特殊情况下,键顺序无关紧要,值的顺序与类似键相比无关紧要。
        • 如果您需要与顺序无关的比较,包含对此类事物的工程支持的自定义字典类型可能比任何内置类型都快。否则,如果您控制何时将项目添加到字典或从字典中删除,计算添加或删除的每个项目的哈希码并保持运行 UInt64 总计 (hash+0x123456789L)*hash 可能会有所帮助,在 @987654323 中进行计算@context [添加项目时,将上述值加到总数中;删除时,减去它]。如果两个集合的总数不相等...
        • ...没有必要比较它们的内容。同样,如果它们的大小不相等。如果大小相等,并且扩展哈希的总和相等,并且可以假定集合使用相同的EqualityComparer,则遍历一个并检查另一个是否包含所有项目。
        猜你喜欢
        • 2020-10-30
        • 1970-01-01
        • 1970-01-01
        • 2015-01-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多