比较两个字典的相等性

28

使用C#编写代码,比较两个字典:

  • 键为string类型
  • 值为一个int列表。

当两个字典满足以下条件时,我认为它们相等:

  • 它们具有相同的键
  • 并且对于每个键,整数列表中的每个值都相同(不一定按照相同顺序)。

我尝试了这个这个与之相关的问题的解决方案,但它们都未通过我的测试函数DoesOrderKeysMatterDoesOrderValuesMatter的测试套件。

我的测试套件:

public static List<int> GetList(int x, int y)
{
   List<int> list = new List<int>();
             list.Add(x);
             list.Add(y);
   return list;
}

public static Dictionary<string, List<int>> GetDict1()
{
   Dictionary<string, List<int>> dict1 = new Dictionary<string, List<int>>();
   dict1.Add("a", GetList(1,2));
   dict1.Add("b", GetList(3,4));
   return dict1;
}

public static Dictionary<string, List<int>> GetDict2()
{
   Dictionary<string, List<int>> dict2 = new Dictionary<string, List<int>>();
   dict2.Add("b", GetList(3,4));
   dict2.Add("a", GetList(1,2));
   return dict2;
}

测试类。
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
    
    
namespace UnitTestProject1
{
  [TestClass]
  public class ProvideReportTests
  {
     [TestMethod]
     public void AreSameDictionariesEqual()
     {
        // arrange
        Dictionary<string, List<int>> dict1 = GetDict1();    
        // act
        bool dictsAreEqual = false;
        dictsAreEqual = AreDictionariesEqual(dict1, dict1);    
        // assert
        Assert.IsTrue(dictsAreEqual, "Dictionaries are not equal");    
     }
    
     [TestMethod]
     public void AreDifferentDictionariesNotEqual()
     {
        // arrange
        Dictionary<string, List<int>> dict1 = GetDict1();
        Dictionary<string, List<int>> dict2 = new Dictionary<string, List<int>>();    
        // act
        bool dictsAreEqual = true;
        dictsAreEqual = AreDictionariesEqual(dict1, dict2);    
        // assert
        Assert.IsFalse(dictsAreEqual, "Dictionaries are equal");    
     }
    
     [TestMethod]
     public void DoesOrderKeysMatter()
     {
        // arrange
        Dictionary<string, List<int>> dict1 = GetDict1();
        Dictionary<string, List<int>> dict2 = GetDict2();    
        // act
        bool dictsAreEqual = false;
        dictsAreEqual = AreDictionariesEqual(dict1, dict2);    
        // assert
        Assert.IsTrue(dictsAreEqual, "Dictionaries are not equal");    
    }
    
    [TestMethod]
    public void DoesOrderValuesMatter()
    {
        // arrange
        Dictionary<string, List<int>> dict1 = GetDict1();    
        Dictionary<string, List<int>> dict2 = new Dictionary<string, List<int>>();
        dict2.Add("a", GetList(2,1));
        dict2.Add("b", GetList(3,4));    
        // act
        bool dictsAreEqual = false;
        dictsAreEqual = AreDictionariesEqual(dict1, dict2);    
        // assert
        Assert.IsTrue(dictsAreEqual, "Dictionaries are not equal");    
    }
    
    
     private bool AreDictionariesEqual(Dictionary<string, List<int>> dict1, Dictionary<string, List<int>> dict2)
     {
          return dict1.Keys.Count == dict2.Keys.Count &&
                 dict1.Keys.All(k => dict2.ContainsKey(k) && 
                 object.Equals(dict2[k], dict1[k]));
    
          // also fails:
          //    return dict1.OrderBy(kvp => kvp.Key).SequenceEqual(dict2.OrderBy(kvp => kvp.Key));
     }
  }
}
这些字典的正确比较方式是什么?或者我的TestSuite(虽然笨拙地编写)中有错误吗? 更新 我正试图将Servy的答案纳入我的测试套件,如下所示,但我遇到了一些错误(在Visual Studio中用红色波浪线标出): - 在“Equals”方法中的“SetEquals”说:“不包含接受Generic.List类型的第一个参数的定义。 - 在AreDictionariesEqual中,“DictionaryComparer”是一种类型,但被用作变量。
namespace UnitTestProject1
{
    [TestClass]
    public class ProvideReportTests
    {
        [TestMethod]
        // ... same as above    

        private bool AreDictionariesEqual(Dictionary<string, List<int>> dict1, Dictionary<string, List<int>> dict2)
        {
            DictionaryComparer<string, List<int>>(new ListComparer<int>() dc = new DictionaryComparer<string, List<int>>(new ListComparer<int>();
            return dc.Equals(dict1, dict2);

        }

    }

    public class DictionaryComparer<TKey, TValue> :
        IEqualityComparer<Dictionary<TKey, TValue>>
    {
        private IEqualityComparer<TValue> valueComparer;
        public DictionaryComparer(IEqualityComparer<TValue> valueComparer = null)
        {
            this.valueComparer = valueComparer ?? EqualityComparer<TValue>.Default;
        }
        public bool Equals(Dictionary<TKey, TValue> x, Dictionary<TKey, TValue> y)
        {
            if (x.Count != y.Count)
                return false;
            if (x.Keys.Except(y.Keys).Any())
                return false;
            if (y.Keys.Except(x.Keys).Any())
                return false;
            foreach (var pair in x)
                if (!valueComparer.Equals(pair.Value, y[pair.Key]))
                    return false;
            return true;
        }

        public int GetHashCode(Dictionary<TKey, TValue> obj)
        {
            throw new NotImplementedException();
        }
    }

    public class ListComparer<T> : IEqualityComparer<List<T>>
    {
        private IEqualityComparer<T> valueComparer;
        public ListComparer(IEqualityComparer<T> valueComparer = null)
        {
            this.valueComparer = valueComparer ?? EqualityComparer<T>.Default;
        }

        public bool Equals(List<T> x, List<T> y)
        {
            return x.SetEquals(y, valueComparer);
        }

        public int GetHashCode(List<T> obj)
        {
            throw new NotImplementedException();
        }
    }

    public static bool SetEquals<T>(this IEnumerable<T> first, IEnumerable<T> second, IEqualityComparer<T> comparer)
        {
            return new HashSet<T>(second, comparer ?? EqualityComparer<T>.Default)
                .SetEquals(first);
        }

}

3
object.Equals(dict2[k], dict1[k])通过引用比较列表。不同的列表实例具有不同的引用。列表中有什么项并不重要。 - Sergey Berezovskiy
你的 AreDictionariesEqual 可能需要使用 Enumerable.SequenceEqual - AakashM
1
@AakashM 这将取决于顺序。它需要是无序的。 - Servy
@Servy 当然,我搞混了 CollectionAssert.AreEquivalent - AakashM
12个回答

1
比较具有字符串键的字典比一开始看起来要复杂得多。
每次访问字典中的条目时,Dictionary<TKey,TValue>都会使用一个IEqualityComparer<TKey>与实际条目比较您的输入。比较器还用于哈希计算,这作为某种索引,以便更快地随机访问条目。尝试比较具有不同比较器的字典可能会对键排序和键值对的相等性考虑产生一些副作用。重点在于比较字典时也需要比较比较器。 Dictionary<TKey,TValue>还提供了键和值的集合,但它们是未排序的。键和值集合在字典内部是一致的(第n个键是第n个值的键),但在实例之间不一致。这意味着我们必须使用KeyValuePairs<TKey,TValue>并在比较之前对两个字典按键进行排序。
然而,字典中的比较器只能检查相等性,无法对键进行排序。为了对键值对进行排序,我们需要一个新的 IComparer<TKey> 实例,这是另一个接口,不同于 IEqualityComparer<TKey>。但这里有一个陷阱:这两个接口的默认实现并不一致。当您使用默认构造函数创建字典时,如果 TKey 实现了 IEquatable<TKey>,则该类将实例化一个 GenericEqualityComparer<TKey>,它要求 TKey 实现 bool Equals(TKey other);(否则,它将回退到 ObjectEqualityComparer)。如果您创建默认比较器,则将实例化一个 GenericComparer<TKey>,如果 TKey 实现了 IComparable<TKey>,则要求 TKey 实现 int CompareTo(TKey other);(否则将默认为 ObjectComparer)。并非所有类型都实现这两个接口,而且有些类型使用不同的实现。存在两个不同的键(根据 Equals)在排序时具有相同的顺序(根据 CompareTo)的风险。在这种情况下,有可能出现键排序不一致的风险。

幸运的是,字符串实现了这两个接口。不幸的是,它的实现是不一致的:CompareTo依赖于当前文化来排序项目,而Equals则不依赖!解决这个问题的方法是向字典注入自定义比较器,提供这两个接口的一致实现。我们可以使用StringComparer来代替默认实现。然后,我们只需获取字典比较器,转换它,并用它来对键进行排序。此外,StringComparer允许比较比较器,因此我们可以确保两个字典使用相同的比较器。

首先,我们需要一种比较字典值的方法。由于您想比较无序的int列表,我们将实现一个通用的相等比较器,对项目进行排序并SequenceEqual它们。

internal class OrderInsensitiveListComparer<TValue>
    : IEqualityComparer<IEnumerable<TValue>>
{
    private readonly IComparer<TValue> comparer;

    public OrderInsensitiveListComparer(IComparer<TValue> comparer = null)
    {
        this.comparer = comparer ?? Comparer<TValue>.Default;
    }

    public bool Equals([AllowNull] IEnumerable<TValue> x, [AllowNull] IEnumerable<TValue> y)
    {
        return x != null
            && y != null
            && Enumerable.SequenceEqual(
                x.OrderBy(value => value, comparer),
                y.OrderBy(value => value, comparer));
    }

    public int GetHashCode([DisallowNull] IEnumerable<TValue> obj)
    {
        return obj.Aggregate(17, (hash, item) => hash * 23 ^ item.GetHashCode());
    }
}

现在,我们已经涵盖了值,但我们还需要比较 KeyValuePair。它是一个简单的 ref 结构体,因此我们不需要检查 null。我们将简单地将比较委托给两个比较器:一个用于键,另一个用于值。
internal class KeyValuePairComparer<TKey, TValue> : IEqualityComparer<KeyValuePair<TKey, TValue>>
{
    private readonly IEqualityComparer<TKey> key;

    private readonly IEqualityComparer<TValue> value;

    public KeyValuePairComparer(
        IEqualityComparer<TKey> key = null,
        IEqualityComparer<TValue> value = null)
    {
        this.key = key ?? EqualityComparer<TKey>.Default;
        this.value = value ?? EqualityComparer<TValue>.Default;
    }

    public bool Equals([AllowNull] KeyValuePair<TKey, TValue> x, [AllowNull] KeyValuePair<TKey, TValue> y)
    {
        // KeyValuePair is a struct, you can't null check
        return key.Equals(x.Key, y.Key) && value.Equals(x.Value, y.Value);
    }

    public int GetHashCode([DisallowNull] KeyValuePair<TKey, TValue> obj)
    {
        return 17 * 23 ^ obj.Key.GetHashCode() * 23 ^ obj.Value.GetHashCode();
    }
}

现在,我们可以实现字典比较器。我们进行空值检查并比较字典比较器。然后,我们将字典视为一个简单的KeyValuePair可枚举项,并在按键排序后对它们进行SequenceEqual。为此,我们强制转换字典比较器并将比较委托给KeyValueComparer。
internal class DictionaryComparer<TValue> : IEqualityComparer<Dictionary<string, TValue>>
{
    private readonly IEqualityComparer<TValue> comparer;

    public DictionaryComparer(
        IEqualityComparer<TValue> comparer = null)
    {
        this.comparer = comparer ?? EqualityComparer<TValue>.Default;
    }

    public bool Equals([AllowNull] Dictionary<string, TValue> x, [AllowNull] Dictionary<string, TValue> y)
    {
        return x != null
            && y != null
            && Equals(x.Comparer, y.Comparer)
            && x.Comparer is StringComparer sorter
            && Enumerable.SequenceEqual(
                x.AsEnumerable().OrderBy(pair => pair.Key, sorter),
                y.AsEnumerable().OrderBy(pair => pair.Key, sorter),
                new KeyValuePairComparer<string, TValue>(x.Comparer, comparer));
    }

    public int GetHashCode([DisallowNull] Dictionary<string, TValue> obj)
    {
        return new OrderInsensitiveListComparer<KeyValuePair<string, TValue>>()
            .GetHashCode(obj.AsEnumerable()) * 23 ^ obj.Comparer.GetHashCode();
    }
}

最后,我们只需要实例化比较器并让它们完成工作。
    private bool AreDictionariesEqual(Dictionary<string, List<int>> dict1, Dictionary<string, List<int>> dict2)
    {
        return new DictionaryComparer<List<int>>(
            new OrderInsensitiveListComparer<int>())
                .Equals(dict1, dict2);
    }

然而,为了使其正常工作,我们需要在每个字典中使用StringComparer。
    [TestMethod]
    public void DoesOrderValuesMatter()
    {
        Dictionary<string, List<int>> dict1 = new Dictionary<string, List<int>>(StringComparer.CurrentCulture);
        // more stuff
    }

0
下面所示的函数可以适用于执行任何通用比较:
public bool AreDictionaryEquals(Dictionary<ulong?, string> dictionaryList1, Dictionary<ulong?, string> dictionaryList2)
{

   if (dictionaryList1.Count != dictionaryList2.Count)
      return false;

   IDictionary<ulong?, string> orderedList1 = new Dictionary<ulong?, string>();
   IDictionary<ulong?, string> orderedList2 = new Dictionary<ulong?, string>();

   foreach (var itemDict1 in dictionaryList1.OrderByDescending(key => key.Id))
   {

     orderedList1.Add(itemDict1.Id, itemDict1.PropertyX);

   }

   foreach (var itemDict2 in dictionaryList2.OrderByDescending(key => key.Id))
   {

     orderedList2.Add(itemDict2.Id, itemDict2.PropertyX);

   }

  //check keys and values for equality
  return (orderedList1.Keys.SequenceEqual(orderedList2.Keys) && orderedList1.Keys.All(k => orderedList1[k].SequenceEqual(orderedList2[k])));

}

1- 如果两个字典的长度不相等,我们可以安全地返回false。

2- 然后,我们使用键的值对两个字典进行排序。这样做的原因是你可能会遇到这样的情况:

字典A:[1,A],[3,B]

字典B:[3,B],[1,A]

即使顺序不同,两者的内容也可以被认为是相等的。

最后:

3- 我们比较两个排序后的序列,并检索此比较的结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接