比较两个字典的相等性

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个回答

32

所以首先我们需要为字典创建一个相等比较器。它需要确保它们具有匹配的键,并且如果匹配,比较每个键的值:

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);
}

现在我们可以简单地写成:

new DictionaryComparer<string, List<int>>(new ListComparer<int>())
    .Equals(dict1, dict2);

感谢您详细的回答!我在将其合并到我的测试套件中遇到了一些问题,您能否看一下我的更新。谢谢! - BioGeek
1
@BioGeek 扩展方法需要在静态类中。那个类可能不是静态的。而且你创建比较器的方式有误。你把 new ListComparer<int>() 放在比较器类型的定义中,但实际上应该作为构造函数的参数。 - Servy

25

我知道这个问题已经有一个被采纳的答案,但是我想提供一个更简单的替代方案:

using System.Linq;
using System.Collections.Generic;

namespace Foo
{
    public static class DictionaryExtensionMethods
    {
        public static bool ContentEquals<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, Dictionary<TKey, TValue> otherDictionary)
        {
            return (otherDictionary ?? new Dictionary<TKey, TValue>())
                .OrderBy(kvp => kvp.Key)
                .SequenceEqual((dictionary ?? new Dictionary<TKey, TValue>())
                                   .OrderBy(kvp => kvp.Key));
        }
    }
}

我想知道是否可以通过首先检查 dictionary.Count 是否等于 otherDictionary.Count 来进一步改进。这可能会加快执行速度。 - Kyle Falconer
1
@KyleFalconer 如果速度是一个问题(如果这将在数千次调用的循环中执行),那么首先检查计数会是一个好主意。不过,这会增加更多的代码行,并使其稍微复杂一些,因此如果它不会在紧密的循环内执行,我就不会这样做。 - jfren484
1
不错的答案。你需要添加 using System.Linq; 才能编译通过。 - fig
我非常喜欢在这种情况下保持简单。如果预期的使用是单元测试,使用相等比较器似乎有些过头了。 - rollsch
看起来这个 null 变量将等于空字典。 - juraj
显示剩余4条评论

11
将字典转换为 KeyValuePair 列表,然后作为集合进行比较:
CollectionAssert.AreEqual(
   dict1.OrderBy(kv => kv.Key).ToList(),
   dict2.OrderBy(kv => kv.Key).ToList()
);

5
我认为AreDictionariesEqual()需要另一种用于列表比较的方法。
因此,如果条目的顺序不重要,您可以尝试这样做:
  static bool ListEquals(List<int> L1, List<int> L2)
{
    if (L1.Count != L2.Count)
        return false;

    return L1.Except(L2).Count() == 0;
}            
    /*
    if it is ok to change List content you may try
    L1.Sort();
    L2.Sort();
    return L1.SequenceEqual(L2);
    */


static bool DictEquals(Dictionary<string, List<int>> D1, Dictionary<string, List<int>> D2)
{
    if (D1.Count != D2.Count)
        return false;

    return D1.Keys.All(k => D2.ContainsKey(k) && ListEquals(D1[k],D2[k]));

}

如果条目的顺序很重要,请尝试以下方法:

static bool DictEqualsOrderM(Dictionary<string, List<int>> D1, Dictionary<string, List<int>> D2)
{
    if (D1.Count != D2.Count)
        return false;

    //check keys for equality, than lists.           
    return (D1.Keys.SequenceEqual(D2.Keys) && D1.Keys.All(k => D1[k].SequenceEqual(D2[k])));         
}

5

大多数答案在多次迭代字典,而它应该很简单:

    static bool AreEqual(IDictionary<string, string> thisItems, IDictionary<string, string> otherItems)
    {
        if (thisItems.Count != otherItems.Count)
        {
            return false;
        }
        var thisKeys = thisItems.Keys;
        foreach (var key in thisKeys)
        {
            if (!(otherItems.TryGetValue(key, out var value) &&
                  string.Equals(thisItems[key], value, StringComparison.OrdinalIgnoreCase)))
            {
                return false;
            }
        }
        return true;
    }

4
上面的接受答案并不总是能够返回正确的比较结果,因为使用 HashSet 来比较两个列表将无法考虑到列表中的重复值。例如,如果 OP 有以下列表:
var dict1 = new Dictionary<string, List<int>>() { { "A", new List<int>() { 1, 2, 1 } } };
var dict2 = new Dictionary<string, List<int>>() { { "A", new List<int>() { 2, 2, 1 } } };

那么字典比较的结果是相等的,但实际上它们并不相等。我唯一能想到的解决方法是对这两个列表进行排序,并按索引比较值,但我相信有更聪明的人可以想出更高效的方法。


3

下面介绍一种使用Linq的方法,虽然可能会牺牲一些效率来保持代码的整洁。另外一个Linq示例来自于jfren484,实际上在DoesOrderValuesMatter()测试中失败了,因为它依赖于默认的List<int> Equals(), 这是有顺序依赖性的。

private bool AreDictionariesEqual(Dictionary<string, List<int>> dict1, Dictionary<string, List<int>> dict2)
{
    string dict1string = String.Join(",", dict1.OrderBy(kv => kv.Key).Select(kv => kv.Key + ":" + String.Join("|", kv.Value.OrderBy(v => v))));
    string dict2string = String.Join(",", dict2.OrderBy(kv => kv.Key).Select(kv => kv.Key + ":" + String.Join("|", kv.Value.OrderBy(v => v))));

    return dict1string.Equals(dict2string);
}

2
如果两个字典使用等效的实现,并且希望将该实现视为等效,则包含相同数量项的一个字典(任意选择)将另一个字典中找到的所有元素键映射到另一个字典中对应的值,它们将是等效的,除非其中一个被修改。测试这些条件比不假定两个字典使用相同的的任何方法都要快。
如果两个字典没有使用相同的实现,则通常不应将它们视为等效,无论它们包含什么项。例如,使用区分大小写比较器和使用不区分大小写比较器的>,两者都包含键值对(“Fred”,“Quimby”),但它们不等效,因为后者会将“FRED”映射到“Quimby”,而前者则不会。
只有当字典使用相同的实现时,但如果想要比字典使用的键相等的更精细的定义,并且每个值中没有存储密钥的副本,则需要构建一个新的字典,以便测试原始字典是否相等。最好推迟此步骤,直到早期测试表明字典似乎匹配为止。然后构建一个>,将一个字典中的每个键映射到自身,然后在其中查找所有其他字典的键,以确保它们映射到匹配的内容。如果两个字典都使用不区分大小写的比较器,并且一个包含(“Fred”,“Quimby”),另一个包含(“FRED”,“Quimby”),则新的临时字典将“FRED”映射到“Fred”,比较这两个字符串将揭示字典不匹配。

1
public static IDictionary<string, object> ToDictionary(this object source)
    {
        var fields = source.GetType().GetFields(
            BindingFlags.GetField |
            BindingFlags.Public |
            BindingFlags.Instance).ToDictionary
        (
            propInfo => propInfo.Name,
            propInfo => propInfo.GetValue(source) ?? string.Empty
        );

        var properties = source.GetType().GetProperties(
            BindingFlags.GetField |
            BindingFlags.GetProperty |
            BindingFlags.Public |
            BindingFlags.Instance).ToDictionary
        (
            propInfo => propInfo.Name,
            propInfo => propInfo.GetValue(source, null) ?? string.Empty
        );

        return fields.Concat(properties).ToDictionary(key => key.Key, value => value.Value); ;
    }
    public static bool EqualsByValue(this object source, object destination)
    {
        var firstDic = source.ToFlattenDictionary();
        var secondDic = destination.ToFlattenDictionary();
        if (firstDic.Count != secondDic.Count)
            return false;
        if (firstDic.Keys.Except(secondDic.Keys).Any())
            return false;
        if (secondDic.Keys.Except(firstDic.Keys).Any())
            return false;
        return firstDic.All(pair =>
          pair.Value.ToString().Equals(secondDic[pair.Key].ToString())
        );
    }
    public static bool IsAnonymousType(this object instance)
    {

        if (instance == null)
            return false;

        return instance.GetType().Namespace == null;
    }
    public static IDictionary<string, object> ToFlattenDictionary(this object source, string parentPropertyKey = null, IDictionary<string, object> parentPropertyValue = null)
    {
        var propsDic = parentPropertyValue ?? new Dictionary<string, object>();
        foreach (var item in source.ToDictionary())
        {
            var key = string.IsNullOrEmpty(parentPropertyKey) ? item.Key : $"{parentPropertyKey}.{item.Key}";
            if (item.Value.IsAnonymousType())
                return item.Value.ToFlattenDictionary(key, propsDic);
            else
                propsDic.Add(key, item.Value);
        }
        return propsDic;
    }

1
我喜欢这种方法,因为当测试失败时,它会提供更多的细节。
    public void AssertSameDictionary<TKey,TValue>(Dictionary<TKey,TValue> expected,Dictionary<TKey,TValue> actual)
    {
        string d1 = "expected";
        string d2 = "actual";
        Dictionary<TKey,TValue>.KeyCollection keys1= expected.Keys;
        Dictionary<TKey,TValue>.KeyCollection keys2= actual.Keys;
        if (actual.Keys.Count > expected.Keys.Count)
        {
            string tmp = d1;
            d1 = d2;
            d2 = tmp;
            Dictionary<TKey, TValue>.KeyCollection tmpkeys = keys1;
            keys1 = keys2;
            keys2 = tmpkeys;
        }

        foreach(TKey key in keys1)
        {
            Assert.IsTrue(keys2.Contains(key), $"key '{key}' of {d1} dict was not found in {d2}");
        }
        foreach (TKey key in expected.Keys)
        {
            //already ensured they both have the same keys
            Assert.AreEqual(expected[key], actual[key], $"for key '{key}'");
        }
    }

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