[英]Compare two dictionaries for equality
我想在 C# 中比较两个字典,其中键是string
,值是int
列表。 我假设两个字典在它们都具有相同的键时是相等的,并且对于每个键作为值,一个具有相同整数的列表(两者不一定以相同的顺序)。
我使用了这个问题和这个相关问题的答案,但是我的测试套件都没有通过测试函数DoesOrderKeysMatter
和DoesOrderValuesMatter
。
我的测试套件:
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
说:“不包含 SetEquals 的定义,它接受 Generic.List 类型的第一个参数。
在 AreDictionariesEqual 中it says
DictionaryComparer<List> 是一种类型,但用作变量。`
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); } }
所以首先我们需要一个字典的相等比较器。 它需要确保它们具有匹配的键,如果匹配,则比较每个键的值:
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);
我知道这个问题已经有一个公认的答案,但我想提供一个更简单的选择:
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));
}
}
}
将字典转换为KeyValuePair
列表,然后作为集合进行比较:
CollectionAssert.AreEqual(
dict1.OrderBy(kv => kv.Key).ToList(),
dict2.OrderBy(kv => kv.Key).ToList()
);
我认为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])));
}
上面接受的答案并不总是返回正确的比较,因为使用 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 列表进行排序并按索引比较值,但我相信比我更聪明的人可以想出更有效的方法。
如果已知两个字典使用IEqualityComparer
的等效实现,并且希望将该实现视为等效的所有键视为等效,则它们包含相同数量的项目,并且一个(任意选择)映射在中找到的所有元素键另一个与另一个对应的值,除非或直到其中一个被修改,否则它们将是等价的。 对这些条件的测试将比任何不假设两个字典都使用相同的IEqualityComparer
的方法更快。
如果两个字典不使用相同的IEqualityComparer
实现,则通常不应将它们视为等效,无论它们包含哪些项目。 例如,带有区分大小写比较器的Dictionary<String,String>
和带有不区分大小写比较器的 Dictionary<String,String> 两者都包含键值对 ("Fred", "Quimby") 是不等价的,因为后者会将“FRED”映射到“Quimby”,但前者不会。
仅当字典使用相同的IEqualityComparer
实现时,但如果人们对键相等的细粒度定义感兴趣,而不是字典使用的定义,并且键的副本不与每个值一起存储,它将是为了测试原始字典的相等性,有必要建立一个新字典。 最好延迟此步骤,直到较早的测试表明字典似乎匹配。 然后构建一个Dictionary<TKey,TKey>
,它将每个字典中的每个键映射到自身,然后在其中查找所有其他字典的键,以确保它们映射到匹配的内容。 如果两个字典都使用不区分大小写的比较器,并且一个包含(“Fred”、“Quimby”)和另一个(“FRED”、“Quimby”),则新的临时字典会将“FRED”映射到“Fred”,并比较这两个字符串会显示字典不匹配。
这是使用Linq的一种方式,可能会为了整洁的代码而牺牲一些效率。 jfren484中的另一个 Linq 示例实际上未通过 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);
}
大多数答案都是多次迭代字典,而它应该很简单:
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;
}
我喜欢这种方法,因为它在测试失败时提供了更多细节
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}'");
}
}
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;
}
使用字符串键比较字典比乍一看要复杂得多。
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)。 如果您创建默认的比较器,如果 TKey 实现IComparable<TKey>
将实例化一个GenericComparer<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 结构,所以我们不需要检查空值。 我们将简单地将比较委托给两个比较器:一个用于键,另一个用于值。
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
}
下面显示的函数可用于执行任何通用比较:
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- 我们比较两个排序的序列并检索此比较的结果。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.