HashSet<T>.CreateSetComparer() 无法指定 IEqualityComparer<T>,有替代方法吗?

HashSet<T>.CreateSetComparer() cannot specify IEqualityComparer<T>, is there an alternative?

internal source中有这样一个构造函数 public HashSetEqualityComparer(IEqualityComparer<T> comparer)但是它是内部的所以我不能使用它。

默认情况下,HashSet<T>.CreateSetComparer() 仅使用将应用 EqualityComparer<T>.Default 的无参数构造函数。

有没有办法在不从源代码中复制代码的情况下获得具有 IEqualityComparer<T> 选择的 HashSetEqualityComparer<T>

我认为最好的解决方案是使用 SetEquals。它以与 HashSetEqualityComparer 完全相同的方式完成您需要的工作,但它 考虑在其比较集中定义的任何自定义比较器。

因此,在您想要使用 HashSet<T> 作为字典键的特定场景中,您需要实现一个 IEqualityComparer<HashSet<T>> 来使用 SetEquals 和 "borrows" HashSetEqualityComparer.GetHashCode()的参考来源:

public class CustomHashSetEqualityComparer<T>
    : IEqualityComparer<HashSet<T>>
{
    public bool Equals(HashSet<T> x, HashSet<T> y)
    {
        if (ReferenceEquals(x, null))
            return false;

        return x.SetEquals(y);
    }

    public int GetHashCode(HashSet<T> set)
    {
        int hashCode = 0;

        if (set != null)
        {
            foreach (T t in set)
            {
                hashCode = hashCode ^ 
                    (set.Comparer.GetHashCode(t) & 0x7FFFFFFF);
            }
        }

        return hashCode;
    }
}

但是,是的,没有办法直接创建一个利用自定义比较器的 SetEqualityComparer 是一个小小的痛苦,但恕我直言,这种不幸的行为更多是由于现有实现的错误而不是缺乏所需的过载;没有理由 CreateSetComparer() 不能 return 一个 IEqualityComparer 实际使用集合的比较器,如上面的代码所示。

如果我有发言权,CreateSetComparer() 就根本不是静态方法了。然后很明显,或者至少可以预测,无论 returned 的比较器都将使用当前集合的比较器创建。

我同意@InBetween,使用SetEquals是最好的方法。就算加上构造函数也还是达不到你想要的效果

请看这段代码: http://referencesource.microsoft.com/#System.Core/System/Collections/Generic/HashSet.cs,1360

这是我尝试做的:

class HashSetEqualityComparerWrapper<T> : IEqualityComparer<HashSet<T>>
{
    static private Type HashSetEqualityComparerType = HashSet<T>.CreateSetComparer().GetType();
    private IEqualityComparer<HashSet<T>> _comparer;

    public HashSetEqualityComparerWrapper()
    {
        _comparer = HashSet<T>.CreateSetComparer();
    }
    public HashSetEqualityComparerWrapper(IEqualityComparer<T> comparer)
    {
        _comparer = HashSet<T>.CreateSetComparer();
        if (comparer != null)
        {
            FieldInfo m_comparer_field = HashSetEqualityComparerType.GetField("m_comparer", BindingFlags.NonPublic | BindingFlags.Instance);
            m_comparer_field.SetValue(_comparer, comparer);
        }
    }

    public bool Equals(HashSet<T> x, HashSet<T> y)
    {
        return _comparer.Equals(x, y);
    }
    public int GetHashCode(HashSet<T> obj)
    {
        return _comparer.GetHashCode(obj);
    }
}

更新

我花了 5 分钟实现了另一种形式的 HashSetEqualityComparer<T> 源代码。并重写 bool Equals(HashSet<T> x, HashSet<T> y) 方法。这并不复杂。所有代码都是从源码复制粘贴,我只是稍微修改一下。

class CustomHashSetEqualityComparer<T> : IEqualityComparer<HashSet<T>>
{
    private IEqualityComparer<T> m_comparer;

    public CustomHashSetEqualityComparer()
    {
        m_comparer = EqualityComparer<T>.Default;
    }

    public CustomHashSetEqualityComparer(IEqualityComparer<T> comparer)
    {
        if (comparer == null)
        {
            m_comparer = EqualityComparer<T>.Default;
        }
        else
        {
            m_comparer = comparer;
        }
    }

    // using m_comparer to keep equals properties in tact; don't want to choose one of the comparers
    public bool Equals(HashSet<T> x, HashSet<T> y)
    {
        // http://referencesource.microsoft.com/#System.Core/System/Collections/Generic/HashSet.cs,1360
        // handle null cases first
        if (x == null)
        {
            return (y == null);
        }
        else if (y == null)
        {
            // set1 != null
            return false;
        }

        // all comparers are the same; this is faster
        if (AreEqualityComparersEqual(x, y))
        {
            if (x.Count != y.Count)
            {
                return false;
            }
        }
        // n^2 search because items are hashed according to their respective ECs
        foreach (T set2Item in y)
        {
            bool found = false;
            foreach (T set1Item in x)
            {
                if (m_comparer.Equals(set2Item, set1Item))
                {
                    found = true;
                    break;
                }
            }
            if (!found)
            {
                return false;
            }
        }
        return true;
    }

    public int GetHashCode(HashSet<T> obj)
    {
        int hashCode = 0;
        if (obj != null)
        {
            foreach (T t in obj)
            {
                hashCode = hashCode ^ (m_comparer.GetHashCode(t) & 0x7FFFFFFF);
            }
        } // else returns hashcode of 0 for null hashsets
        return hashCode;
    }

    // Equals method for the comparer itself. 
    public override bool Equals(Object obj)
    {
        CustomHashSetEqualityComparer<T> comparer = obj as CustomHashSetEqualityComparer<T>;
        if (comparer == null)
        {
            return false;
        }
        return (this.m_comparer == comparer.m_comparer);
    }

    public override int GetHashCode()
    {
        return m_comparer.GetHashCode();
    }

    static private bool AreEqualityComparersEqual(HashSet<T> set1, HashSet<T> set2)
    {
        return set1.Comparer.Equals(set2.Comparer);
    }
}

如果您使用自定义比较器,请避免此 class。它使用自己的相等比较器来执行 GetHashCode,但是在执行 Equals(Set1, Set2) 时,如果 Set1 和 Set2 具有相同的相等比较器,则 HashSetEqualityComparer 将使用集合的比较器。如果 Set1 和 Set2 有不同的比较器,HashsetEqualityComparer 将只使用自己的比较器来比较 equals

情况变得更糟。它调用 HashSet.HashSetEquals,其中有一个错误(参见 https://referencesource.microsoft.com/#system.core/System/Collections/Generic/HashSet.cs 行 1489,在执行子集检查之前缺少 if (set1.Count != set2.Count) return false

该错误由以下程序说明:

class Program
{
    private class MyEqualityComparer : EqualityComparer<int>
    {
        public override bool Equals(int x, int y)
        {
            return x == y;
        }

        public override int GetHashCode(int obj)
        {
            return obj.GetHashCode();
        }
    }

    static void Main(string[] args)
    {
        var comparer = HashSet<int>.CreateSetComparer();
        var set1 = new HashSet<int>(new MyEqualityComparer()) { 1 };
        var set2 = new HashSet<int> { 1, 2 };

        Console.WriteLine(comparer.Equals(set1, set2));
        Console.WriteLine(comparer.Equals(set2, set1)); //True!

        Console.ReadKey();
    }
}

关于这个问题的其他答案(我没有代表发表评论):

  • Wilhelm Liao:他的回答也包含错误,因为它是从参考源中复制的
  • InBetween:解不对称。 CustomHashSetEqualityComparer.Equals(A, B) 并不总是等于 CustomHashSetEqualityComparer.Equals(B, A)。我会害怕的。

我认为一个健壮的实现应该在遇到一个具有与其自身不同的比较器的集合时抛出异常。它可以总是使用自己的比较器并忽略设置的比较器,但这会产生奇怪和不直观的行为。