Comparing two collections for equality irrespective of the order of items in them

后端 未结 19 1906
我在风中等你
我在风中等你 2020-11-22 10:28

I would like to compare two collections (in C#), but I\'m not sure of the best way to implement this efficiently.

I\'ve read the other thread about Enumerable.Sequen

19条回答
  •  轻奢々
    轻奢々 (楼主)
    2020-11-22 11:10

    It turns out Microsoft already has this covered in its testing framework: CollectionAssert.AreEquivalent

    Remarks

    Two collections are equivalent if they have the same elements in the same quantity, but in any order. Elements are equal if their values are equal, not if they refer to the same object.

    Using reflector, I modified the code behind AreEquivalent() to create a corresponding equality comparer. It is more complete than existing answers, since it takes nulls into account, implements IEqualityComparer and has some efficiency and edge case checks. plus, it's Microsoft :)

    public class MultiSetComparer : IEqualityComparer>
    {
        private readonly IEqualityComparer m_comparer;
        public MultiSetComparer(IEqualityComparer comparer = null)
        {
            m_comparer = comparer ?? EqualityComparer.Default;
        }
    
        public bool Equals(IEnumerable first, IEnumerable second)
        {
            if (first == null)
                return second == null;
    
            if (second == null)
                return false;
    
            if (ReferenceEquals(first, second))
                return true;
    
            if (first is ICollection firstCollection && second is ICollection secondCollection)
            {
                if (firstCollection.Count != secondCollection.Count)
                    return false;
    
                if (firstCollection.Count == 0)
                    return true;
            }
    
            return !HaveMismatchedElement(first, second);
        }
    
        private bool HaveMismatchedElement(IEnumerable first, IEnumerable second)
        {
            int firstNullCount;
            int secondNullCount;
    
            var firstElementCounts = GetElementCounts(first, out firstNullCount);
            var secondElementCounts = GetElementCounts(second, out secondNullCount);
    
            if (firstNullCount != secondNullCount || firstElementCounts.Count != secondElementCounts.Count)
                return true;
    
            foreach (var kvp in firstElementCounts)
            {
                var firstElementCount = kvp.Value;
                int secondElementCount;
                secondElementCounts.TryGetValue(kvp.Key, out secondElementCount);
    
                if (firstElementCount != secondElementCount)
                    return true;
            }
    
            return false;
        }
    
        private Dictionary GetElementCounts(IEnumerable enumerable, out int nullCount)
        {
            var dictionary = new Dictionary(m_comparer);
            nullCount = 0;
    
            foreach (T element in enumerable)
            {
                if (element == null)
                {
                    nullCount++;
                }
                else
                {
                    int num;
                    dictionary.TryGetValue(element, out num);
                    num++;
                    dictionary[element] = num;
                }
            }
    
            return dictionary;
        }
    
        public int GetHashCode(IEnumerable enumerable)
        {
            if (enumerable == null) throw new ArgumentNullException(nameof(enumerable));
    
            int hash = 17;
    
            foreach (T val in enumerable.OrderBy(x => x))
                hash = hash * 23 + (val?.GetHashCode() ?? 42);
    
            return hash;
        }
    }
    

    Sample usage:

    var set = new HashSet>(new[] {new[]{1,2,3}}, new MultiSetComparer());
    Console.WriteLine(set.Contains(new [] {3,2,1})); //true
    Console.WriteLine(set.Contains(new [] {1, 2, 3, 3})); //false
    

    Or if you just want to compare two collections directly:

    var comp = new MultiSetComparer();
    Console.WriteLine(comp.Equals(new[] {"a","b","c"}, new[] {"a","c","b"})); //true
    Console.WriteLine(comp.Equals(new[] {"a","b","c"}, new[] {"a","b"})); //false
    

    Finally, you can use your an equality comparer of your choice:

    var strcomp = new MultiSetComparer(StringComparer.OrdinalIgnoreCase);
    Console.WriteLine(strcomp.Equals(new[] {"a", "b"}, new []{"B", "A"})); //true
    

提交回复
热议问题