C#比较两个字典是否相等

时间:2014-02-13 15:13:00

标签: c# unit-testing dictionary

我希望在C#中将两个词典与string作为键进行比较,并将int列表作为值进行比较。我假设两个字典在它们都具有相同的键时是相等的,并且对于每个键而言,它们是具有相同整数的列表(两者不一定是相同的顺序)。

我同时使用thisthis相关问题的答案,但我的测试套件DoesOrderKeysMatterDoesOrderValuesMatter都失败了。

我的测试套件:

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 = new Dictionary<string, List<int>>();
            List<int> list1 = new List<int>();
            list1.Add(1);
            list1.Add(2);
            dict1.Add("a", list1);
            List<int> list2 = new List<int>();
            list2.Add(3);
            list2.Add(4);
            dict1.Add("b", list2);

            // 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 = new Dictionary<string, List<int>>();
            List<int> list1 = new List<int>();
            list1.Add(1);
            list1.Add(2);
            dict1.Add("a", list1);
            List<int> list2 = new List<int>();
            list2.Add(3);
            list2.Add(4);
            dict1.Add("b", list2);

            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 = new Dictionary<string, List<int>>();
            List<int> list1 = new List<int>();
            list1.Add(1);
            list1.Add(2);
            dict1.Add("a", list1);
            List<int> list2 = new List<int>();
            list2.Add(3);
            list2.Add(4);
            dict1.Add("b", list2);

            Dictionary<string, List<int>> dict2 = new Dictionary<string, List<int>>();
            List<int> list3 = new List<int>();
            list3.Add(3);
            list3.Add(4);
            dict2.Add("b", list3);
            List<int> list4 = new List<int>();
            list4.Add(1);
            list4.Add(2);
            dict2.Add("a", list4);

            // 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 = new Dictionary<string, List<int>>();
            List<int> list1 = new List<int>();
            list1.Add(1);
            list1.Add(2);
            dict1.Add("a", list1);
            List<int> list2 = new List<int>();
            list2.Add(3);
            list2.Add(4);
            dict1.Add("b", list2);

            Dictionary<string, List<int>> dict2 = new Dictionary<string, List<int>>();
            List<int> list3 = new List<int>();
            list3.Add(2);
            list3.Add(1);
            dict2.Add("a", list3);
            List<int> list4 = new List<int>();
            list4.Add(4);
            list4.Add(3);
            dict2.Add("b", list4);

            // 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类型的第一个参数的SetEquals的定义。
  • 在AreDictionariesEqual it says DictionaryComparer&gt;是一种类型,但用作变量。

    命名空间UnitTestProject1 {     [识别TestClass]     公共类ProvideReportTests     {         [测试方法]         // ...与上面相同

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

    }

11 个答案:

答案 0 :(得分:28)

首先,我们需要一个字典的相等比较器。它需要确保它们具有匹配的密钥,如果是,则比较每个密钥的值:

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

答案 1 :(得分:17)

我知道这个问题已经有了一个公认的答案,但我想提供一个更简单的选择:

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

答案 2 :(得分: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])));         
}

答案 3 :(得分:5)

上面接受的答案并不总是会返回正确的比较,因为 使用HashSet比较2个列表不会考虑列表中的重复值。 例如,如果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 } } };

然后字典比较的结果是它们是相等的,当它们不相同时。我看到的唯一解决方案是对2列表进行排序并按索引比较值,但我确信有人比我聪明,可以提出更有效的方法。

答案 4 :(得分:4)

将字典转换为KeyValuePair列表,然后比较为集合:

CollectionAssert.AreEqual(
   dict1.OrderBy(kv => kv.Key).ToList(),
   dict2.OrderBy(kv => kv.Key).ToList()
)

答案 5 :(得分:2)

如果已知两个字典使用IEqualityComparer的等效实现,并且有人希望将该实现视为等效的所有键视为等效,则它们包含相同数量的项,以及一个(任意选择的)映射在另一个中找到的所有元素键对应于另一个的相应值,它们将是等效的,除非或直到其中一个被修改。对这些条件的测试将比任何不假设两个字典都使用相同IEqualityComparer的方法更快。

如果两个词典不使用IEqualityComparer的相同实现,则通常不应将它们视为等效,无论它们包含哪些项目。例如,带有区分大小写的比较器的Dictionary<String,String>和带有区分大小写的比较器的IEqualityComparer都包含键值对(“Fred”,“Quimby”)不等同,因为后者会将“FRED”映射到“Quimby”,但前者不会。

仅当字典使用Dictionary<TKey,TKey>的相同实现时,但是如果一个人对关键字相等的更细粒度的定义感兴趣而不是字典使用的那个,并且密钥的副本没有与每个值,都需要构建一个新的字典,以便测试原始字典的相等性。最好延迟此步骤,直到早期测试表明字典似乎匹配为止。然后构建一个{{1}},将每个键从一个词典映射到自身,然后在其中查找所有其他词典的键,以确保它们映射到匹配的内容。如果两个字典都使用不区分大小写的比较器,并且一个包含(“Fred”,“Quimby”)和另一个(“FRED”,“Quimby”),则新的临时字典会将“FRED”映射到“Fred”,并进行比较这两个字符串会显示字典不匹配。

答案 6 :(得分:2)

这是一种使用Linq的方法,可能会为整洁的代码牺牲一些效率。来自The other Linq examplejfren484实际上未通过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);
}

答案 7 :(得分: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}'");
        }
    }

答案 8 :(得分: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;
    }

答案 9 :(得分:1)

使用字符串键比较字典比乍一看要复杂得多。

Dictionary<TKey,TValue> 每次访问字典中的条目时都会使用 IEqualityComparer<TKey> 将您的输入与实际条目进行比较。比较器也用于散列计算,它作为某种索引以更快地随机访问条目。尝试将字典与不同的比较器进行比较可能会对键值对的键排序和相等性考虑产生一些副作用。 这里的关键是在比较字典时也需要比较比较器。

Dictionary<TKey,TValue> 还提供键和值的集合,但它们是未排序的。键和值集合在字典中是一致的(第n个键是第n个值的键),但不是个实例。 这意味着在比较它们之前,我们必须使用 KeyValuePairs<TKey,TValue> 并在两个字典上按键对它们进行排序。

然而,字典中的比较器只检查相等性,不能对键进行排序。为了对对进行排序,我们需要一个新的 IComparer<TKey> 实例,它是 IEqualityComparer<TKey> 之外的另一个接口。但是这里有一个陷阱:这两个接口的默认实现不一致。当您使用默认构造函数创建字典时,如果 TKey 实现 GenericEqualityComparer<TKey>,则该类将实例化 IEquatable<TKey>,这需要 TKey 实现 bool Equals(TKey other);(否则,它将回退到 ObjectEqualityComparer)。如果您创建默认的比较器,那么如果 TKey 实现 GenericComparer<TKey>,它将实例化 IComparable<TKey>,这将需要 TKey 实现 int CompareTo(TKey other);(否则它将默认为 ObjectComparer)。并非所有类型都实现这两个接口,并且有时会使用不同的实现。存在两个不同的键(根据 Equals)排序相同(根据 CompareTo)的风险。 在这种情况下,键排序一致性存在风险。

幸运的是,string 实现了这两个接口。不幸的是,它的实现不一致: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 结构,所以我们不需要检查空值。我们将简单地将比较委托给两个比较器:一个用于键,另一个用于值。

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
    }

答案 10 :(得分:0)

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

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