如何在 C# 中将 ExceptWith 与 HashSet<ReadOnlyCollection<string>> 类型一起使用?

How to use ExceptWith with the type of HashSet<ReadOnlyCollection<string>> in C#?

HashSet<ReadOnlyCollection<int>> test1 = new HashSet<ReadOnlyCollection<int>> ();
for (int i = 0; i < 10; i++) {
    List<int> temp = new List<int> ();
    for (int j = 1; j < 2; j++) {
        temp.Add (i);
        temp.Add (j);
    }
    test1.Add (temp.AsReadOnly ());
}

这里test1是{[0,1], [1,1], [2,1], [3,1], [4,1], [5,1], [6,1] , [7,1], [8,1], [9,1]}

HashSet<ReadOnlyCollection<int>> test2 = new HashSet<ReadOnlyCollection<int>> ();
for (int i = 5; i < 10; i++) {
    List<int> temp = new List<int> ();
    for (int j = 1; j < 2; j++) {
        temp.Add (i);
        temp.Add (j);
    }
    test2.Add (temp.AsReadOnly ());
}

这里test2是{[5,1], [6,1], [7,1], [8,1], [9,1]}

test1.ExceptWith(test2);

这样做之后,我希望 test1 为 {[0,1]、[1,1]、[2,1]、[3,1]、[4,1]},但它给了我原来的test1.
如何解决这个问题?或者还有其他方法可以做同样的事情吗?谢谢!

这里有 ExceptWith 的实现:

https://github.com/microsoft/referencesource/blob/3b1eaf5203992df69de44c783a3eda37d3d4cd10/System.Core/System/Collections/Generic/HashSet.cs#L532

它实际做的是:

 // remove every element in other from this
 foreach (T element in other) {
    Remove(element);
 }

Remove实施:

https://github.com/microsoft/referencesource/blob/3b1eaf5203992df69de44c783a3eda37d3d4cd10/System.Core/System/Collections/Generic/HashSet.cs#L287

 if (m_slots[i].hashCode == hashCode && m_comparer.Equals(m_slots[i].value, item)) {

因此,如果哈希码不相同,删除将不会执行任何操作。

证明hashcode不一样的小测试:

    List<int> temp = new List<int> ();
     temp.Add(1);
     temp.Add(2);

    HashSet<ReadOnlyCollection<int>> test1 = new HashSet<ReadOnlyCollection<int>> ();
    HashSet<ReadOnlyCollection<int>> test2 = new HashSet<ReadOnlyCollection<int>> ();
    test1.Add (temp.AsReadOnly ());
    test2.Add (temp.AsReadOnly ());

    Console.WriteLine(test1.First().GetHashCode() == test2.First().GetHashCode());

c# 中的对象通常通过 reference 进行比较,而不是通过 value 进行比较。 这意味着 new object() != new object()。同理,new List<int>() { 1 } != new List<int>() { 1 }。另一方面,结构和原语是通过 value 进行比较的,而不是通过引用进行比较的。

一些对象重写了它们的相等方法来比较值。例如字符串:new string(new[] { 'a', 'b', 'c'}) == "abc",即使 object.ReferenceEquals(new string(new[] { 'a', 'b', 'c'}), "abc") == false.

但集合、列表、数组等则不然。有充分的理由——当比较两个整数列表时,你想比较什么?确切的元素,不管顺序?确切的元素顺序?元素之和?没有一个答案适合所有情况。通常你可能真的想检查你是否有相同的对象。

在使用集合或 LINQ 时,您通常可以指定自定义 'comparer' 来按照您想要的方式处理比较。收集方法然后在需要比较两个元素时使用此 'comparer'。

适用于 ReadOnlyCollection<T> 的非常简单的比较器可能如下所示:

class ROCollectionComparer<T> : IEqualityComparer<IReadOnlyCollection<T>>
{
    private readonly IEqualityComparer<T> elementComparer;

    public ROCollectionComparer() : this(EqualityComparer<T>.Default) {}
    public ROCollectionComparer(IEqualityComparer<T> elementComparer) {
        this.elementComparer = elementComparer;
    }

    public bool Equals(IReadOnlyCollection<T> x, IReadOnlyCollection<T> y)
    {
        if(x== null && y == null) return true;
        if(x == null || y == null) return false;
        if(object.ReferenceEquals(x, y)) return true;

        return x.Count == y.Count && 
            x.SequenceEqual(y, elementComparer);
    }

    public int GetHashCode(IReadOnlyCollection<T> obj)
    {       
        // simplistic implementation - but should OK-ish when just looking for equality
        return (obj.Count, obj.Count == 0 ? 0 : elementComparer.GetHashCode(obj.First())).GetHashCode();
    }
}

然后您可以比较默认相等性检查的行为和您自定义的行为:

var std = new HashSet<int[]>(new[] { new[] { 1, 2 }, new[] { 2, 2}});
std.ExceptWith(new[] { new[] { 2, 2}});
std.Dump();

var custom = new HashSet<int[]>(new[] { new[] { 1, 2 }, new[] { 2, 2 } }, new ROCollectionComparer<int>());
custom.ExceptWith(new[] { new[] { 2, 2 }});
custom.ExceptWith(new[] { new int[] { }});
custom.Dump();

你可以在这个 fiddle.

中测试整个事情