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

4
内部源代码中,有一个构造函数 public HashSetEqualityComparer(IEqualityComparer<T> comparer),但它是内部的,所以我不能使用它。
默认情况下,HashSet<T>.CreateSetComparer()仅使用无参数的构造函数,这将应用EqualityComparer<T>.Default
有没有一种方法可以获得具有所选IEqualityComparer<T>HashSetEqualityComparer<T>,而不需要从源代码中复制出代码?

2
通过哈希集的构造函数? - Johnny
@Lepijohnny 你是什么意思?我不是在尝试构建一个哈希集,我正在尝试构建一个HashSetEqualityComparer - David S.
@Lepijohnny 我感觉你们两个都误解了.. 是的,它很特殊,它使得像 Dictionary<HashSet<Tk>,Tv> 这样的东西成为可能,在查找时比较哈希集合的内容。基本上像 HashSet<T>.CreateSetComparer(IEqualityComparer<T> comparer) 这样的东西就是我需要的。请注意,这是用于比较集合本身而不是元素的比较器。 - David S.
看起来你最好的选择是复制源代码,幸运的是它非常小而简单。 - Evk
1
这样的方法永远不会有用。请记住,它比较整个集合。哈希集本身已经定义了它们的元素如何进行比较。 - Hans Passant
显示剩余11条评论
4个回答

6

我认为最好的解决方案是使用SetEquals。它能完成你所需的工作,并且与HashSetEqualityComparer完全相同,但它考虑到集合中定义的任何自定义比较器。

所以,在你特定的场景中,当你想要将HashSet<T>用作字典的键时,你需要实现一个IEqualityComparer<HashSet<T>>,该实现利用SetEquals并"借用"了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()没有理由不能返回一个实际上使用集合的比较器作为比较器的IEqualityComparer

如果我能发表意见,CreateSetComparer()根本不应该是静态方法。这样就能明确或者至少是可预测,返回的任何比较器都会使用当前集合的比较器来创建。


x.SetEquals(y)ynull的情况下会抛出异常。另外,我不确定& 0x7FFFFFFF的目的是什么。负数哈希码值是完全有效的。 - Theodor Zoulias

2
我同意@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);
    }
}

2
如果您使用自定义比较器,请避免使用此类。它使用自己的相等比较器执行GetHashCode,但是在执行Equals(Set1,Set2)时,如果Set1和Set2具有相同的相等比较器,则HashSetEqualityComparer将使用集合的比较器。只有当Set1和Set2具有不同的比较器时,HashsetEqualityComparer才会使用自己的比较器进行相等比较。
更糟糕的是,它调用了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)。我会对此感到担忧。

我认为一个强大的实现应该在遇到具有不同比较器的集合时抛出异常。它可以始终使用自己的比较器并忽略集合比较器,但这将产生奇怪和不直观的行为。


-1

除了原来的解决方案外,我们可以使用HashCode.Combine函数简化GetHashCode

public int GetHashCode(HashSet<T> set)
{
    int hashCode = 0;
    foreach (var item in set)
    {
        hashCode ^= HashCode.Combine(item);
    }

    return hashCode;
}

HashCode.Combine 使用单个参数将哈希码扩散到一个大范围内。当底层数据类型简单时,例如整数值,它可以提高哈希码的质量。因此,您的建议可以产生更高质量的哈希码,但不幸的是,您绕过了set.Comparer,所以您的建议存在缺陷。它可能会为根据其非默认比较器相等的哈希集生成不同的哈希码。这是违反规则的。 - Theodor Zoulias

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