是否有内置方法来比较集合?

197
我希望在我的Equals方法中比较几个集合的内容。我有一个字典和一个列表。是否有内置方法来做到这一点? 编辑: 我想比较两个字典和两个列表,所以我认为相等的含义很清楚——如果两个字典包含相同的键映射到相同的值,则它们相等。

Enumerable.SequenceEqualISet.SetEquals提供了这个功能的版本。如果你想要无序并且处理有重复项的集合,你需要自己编写代码。请查看此文章中提出的实现建议。 - ChaseMedallion
如下评论所述,对于99%的情况,您可以依赖NUnit/MSTest方法CollectionAssert.AreEquivalent - alelom
@alexlomba87 那个函数值得一提,但是依赖于测试程序集来进行生产代码是否有些不妥呢? - matt
15个回答

208

Enumerable.SequenceEqual

使用指定的 IEqualityComparer(T),通过比较其元素来确定两个序列是否相等。

您不能直接比较列表和字典,但可以将字典的值列表与列表进行比较。


74
问题在于SequenceEqual函数要求元素顺序相同。而Dictionary类在枚举键或值时不保证顺序,因此如果你要使用SequenceEqual函数,必须先对.Keys和.Values进行排序! - Orion Edwards
3
除非你想检测顺序差异,当然了 :-) - schoetbi
37
为什么你想要在一个“不保证顺序”的容器中检测排序差异呢? - Matti Virkkunen
4
这段话是关于从IEnumerable中获取特定元素的方法,但是字典不保证顺序,所以.Keys.Values可能会以任何顺序返回键和值,并且随着字典的修改,这个顺序很可能会发生变化。建议您了解一下什么是字典以及什么不是字典。 - Matti Virkkunen
7
MS的TestTools和NUnit都提供了CollectionAssert.AreEquivalent方法。 - tymtam
显示剩余4条评论

50

正如其他人所建议并指出的那样,SequenceEqual 是有序敏感的。要解决这个问题,您可以按键(唯一,并且因此排序始终稳定)对字典进行排序,然后使用SequenceEqual。以下表达式检查两个字典是否相等,而不考虑它们的内部顺序:

dictionary1.OrderBy(kvp => kvp.Key).SequenceEqual(dictionary2.OrderBy(kvp => kvp.Key))

编辑:如Jeppe Stig Nielsen所指出的,某些对象具有与其IEqualityComparer<T>不兼容的IComparer<T>,导致结果不正确。当使用这样的对象键时,必须为这些键指定正确的IComparer<T>。例如,对于字符串键(表现出此问题),您必须执行以下操作才能获得正确的结果:

dictionary1.OrderBy(kvp => kvp.Key, StringComparer.Ordinal).SequenceEqual(dictionary2.OrderBy(kvp => kvp.Key, StringComparer.Ordinal))

如果键类型无法进行 CompareTo 操作,那么您的解决方案将会崩溃。如果键类型具有与其默认相等比较器不兼容的默认比较器,该怎么办?就像 string 一样。例如,这些带有隐式默认相等比较器的字典将在我所知道的所有文化信息下都无法通过您的测试:var dictionary1 = new Dictionary<string, int> { { "Strasse", 10 }, { "Straße", 20 }, }; var dictionary2 = new Dictionary<string, int> { { "Straße", 20 }, { "Strasse", 10 }, }; - Jeppe Stig Nielsen
@JeppeStigNielsen:关于IComparerIEqualityComparer之间的不兼容性问题,我之前并不知道,非常有趣!我已经更新了答案,并提供了可能的解决方案。至于缺少CompareTo的问题,我认为开发人员应该确保提供给OrderBy()方法的委托返回可比较的内容。我认为这对于任何使用OrderBy()的情况都是适用的,即使在字典比较之外也是如此。 - Allon Guralnek

17

除了提到的SequenceEqual外,该方法是真的当两个列表长度相等且它们的对应元素根据比较器按顺序比较相等时,这个方法返回true

(这可以是默认比较器,即重写的Equals())

值得一提,在 .Net4 中,SetEquals可以在ISet对象上使用,

它忽略元素的顺序和任何重复的元素。

因此,如果您想要一个对象列表,但不需要以特定顺序排列,则可以考虑使用ISet(如HashSet)。


请注意,只有在您可以保证它们的内容值是不同的情况下,才能使用SetEquals比较两个可枚举对象的内容。否则,在[1, 2, 2, 3]和[1, 2, 3, 3, 3]上执行SetEquals将返回true,而您可能并不希望如此。 - ErroneousFatality

6
请看Enumerable.SequenceEqual方法。
var dictionary = new Dictionary<int, string>() {{1, "a"}, {2, "b"}};
var intList = new List<int> {1, 2};
var stringList = new List<string> {"a", "b"};
var test1 = dictionary.Keys.SequenceEqual(intList);
var test2 = dictionary.Values.SequenceEqual(stringList);

17
这不可靠,因为SequenceEqual期望从字典中获得的值是按可靠顺序排列的,但字典并不保证顺序,而dictionary.Keys可能会以[2, 1]而不是[1, 2]的顺序输出,这将导致测试失败。 - Orion Edwards

5

这并不是直接回答你的问题,但是微软的TestTools和NUnit都提供了

 CollectionAssert.AreEquivalent

这基本上是你想要的。


这个函数不会返回任何东西,就像布尔值一样。只有在集合不相等的情况下,它才会抛出异常。 - ataraxia

4

.NET缺乏强大的比较集合的工具。我开发了一个简单的解决方案,您可以在下面的链接中找到:

http://robertbouillon.com/2010/04/29/comparing-collections-in-net/

这将执行无序的相等比较:

var list1 = new[] { "Bill", "Bob", "Sally" };
var list2 = new[] { "Bob", "Bill", "Sally" };
bool isequal = list1.Compare(list2).IsSame;

这将检查项目是否已添加/删除:

var list1 = new[] { "Billy", "Bob" };
var list2 = new[] { "Bob", "Sally" };
var diff = list1.Compare(list2);
var onlyinlist1 = diff.Removed; //Billy
var onlyinlist2 = diff.Added;   //Sally
var inbothlists = diff.Equal;   //Bob

这将查看字典中哪些项发生了变化:

var original = new Dictionary<int, string>() { { 1, "a" }, { 2, "b" } };
var changed = new Dictionary<int, string>() { { 1, "aaa" }, { 2, "b" } };
var diff = original.Compare(changed, (x, y) => x.Value == y.Value, (x, y) => x.Value == y.Value);
foreach (var item in diff.Different)
  Console.Write("{0} changed to {1}", item.Key.Value, item.Value.Value);
//Will output: a changed to aaa

10
当然,.NET拥有强大的工具来比较集合(它们是基于集合操作的)。.Removedlist1.Except(list2)相同,.Addedlist2.Except(list1).Equallist1.Intersect(list2).Differentoriginal.Join(changed, left => left.Key, right => right.Key, (left, right) => left.Value == right.Value)。您几乎可以使用LINQ进行任何比较。 - Allon Guralnek
3
更正:.Different应为Original.Join(changed, left => left.Key, right => right.Key, (left, right) => new { Key = left.Key, NewValue = right.Value, Different = left.Value != right.Value}).Where(d => d.Different)。如果需要旧值,你还可以添加OldValue = left.Value - Allon Guralnek
4
@AllonGuralnek,你的建议很好,但是它们无法处理列表不是真正集合的情况——即列表包含多个相同对象的情况。比较 { 1, 2 } 和 { 1, 2, 2 } 将返回未添加/删除任何内容。 - Niall Connaughton

4

我之前不知道 Enumerable.SequenceEqual 方法(每天都有新的学习),但是我会建议使用扩展方法,就像这样:

    public static bool IsEqual(this List<int> InternalList, List<int> ExternalList)
    {
        if (InternalList.Count != ExternalList.Count)
        {
            return false;
        }
        else
        {
            for (int i = 0; i < InternalList.Count; i++)
            {
                if (InternalList[i] != ExternalList[i])
                    return false;
            }
        }

        return true;

    }

有趣的是,在花费2秒钟阅读SequenceEqual的介绍后,似乎微软已经为您构建了我所描述的函数。


2
为了比较集合,您也可以使用LINQ。Enumerable.Intersect返回所有相等的对。您可以像这样比较两个字典:
(dict1.Count == dict2.Count) && dict1.Intersect(dict2).Count() == dict1.Count

第一个比较是必需的,因为dict2可以包含来自dict1和更多的所有键。
您还可以使用Enumerable.ExceptEnumerable.Union来考虑变化,这会导致类似的结果。但可用于确定集合之间的确切差异。

1
对于有序集合(List、Array),请使用SequenceEqual
对于HashSet,请使用SetEquals
对于Dictionary,您可以这样做:
namespace System.Collections.Generic {
  public static class ExtensionMethods {
    public static bool DictionaryEquals<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> d1, IReadOnlyDictionary<TKey, TValue> d2) {
      if (object.ReferenceEquals(d1, d2)) return true; 
      if (d2 is null || d1.Count != d2.Count) return false;
      foreach (var (d1key, d1value) in d1) {
        if (!d2.TryGetValue(d1key, out TValue d2value)) return false;
        if (!d1value.Equals(d2value)) return false;
      }
      return true;
    }
  }
}

更优化的解决方案将使用排序,但这将需要 IComparable<TValue> 接口。


1
这个例子怎么样?
 static void Main()
{
    // Create a dictionary and add several elements to it.
    var dict = new Dictionary<string, int>();
    dict.Add("cat", 2);
    dict.Add("dog", 3);
    dict.Add("x", 4);

    // Create another dictionary.
    var dict2 = new Dictionary<string, int>();
    dict2.Add("cat", 2);
    dict2.Add("dog", 3);
    dict2.Add("x", 4);

    // Test for equality.
    bool equal = false;
    if (dict.Count == dict2.Count) // Require equal count.
    {
        equal = true;
        foreach (var pair in dict)
        {
            int value;
            if (dict2.TryGetValue(pair.Key, out value))
            {
                // Require value be equal.
                if (value != pair.Value)
                {
                    equal = false;
                    break;
                }
            }
            else
            {
                // Require key be present.
                equal = false;
                break;
            }
        }
    }
    Console.WriteLine(equal);
}

致谢:https://www.dotnetperls.com/dictionary-equals


value != pair.Value 是在进行引用比较,请使用 Equals 方法。 - kofifus

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