Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

In the internal source there is such a constructor public HashSetEqualityComparer(IEqualityComparer<T> comparer) but it's internal so I can't use it.

By default, HashSet<T>.CreateSetComparer() just uses the parameterless constructor which will apply EqualityComparer<T>.Default.

Is there a way to get a HashSetEqualityComparer<T> with a IEqualityComparer<T> of choice, without copying out the code from the source?

like image 766
David S. Avatar asked Oct 23 '25 12:10

David S.


2 Answers

I think best solution is using SetEquals. It does the job you need and exactly in the same way that HashSetEqualityComparer does but it will account for any custom comparers defined in the sets its comparing.

So, in your specific scenario where you want to use a HashSet<T> as a key of a dictionary, you need to implement an IEqualityComparer<HashSet<T>> that makes use of SetEquals and "borrows" the reference source of 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;
    }
}

But yes, its a small pain that there is not way to directly create a SetEqualityComparer that leverages custom comparers but this unfortunate behavior is due, IMHO, more to a bug of the existing implementation than a lack of the needed overload; there is no reason why CreateSetComparer() can't return an IEqualityComparer that actually uses the comparers of the sets its comparing as the code above demonstrates.

If I had a voice in it, CreateSetComparer() wouldn't be static method at all. It would then be obvious, or at least predictable, that whatever comparer was returned would be created with the current set's comparer.

like image 120
InBetween Avatar answered Oct 25 '25 01:10

InBetween


Avoid this class if you use custom comparers. It uses its own equality comparer to perform GetHashCode, but when performing Equals(Set1, Set2) if Set1 and Set2 have the same equality comparer, the the HashSetEqualityComparer will use the comparer of the sets. HashsetEqualityComparer will only use its own comparer for equals if Set1 and Set2 have different comparers

It gets worse. It calls HashSet.HashSetEquals, which has a bug in it (See https://referencesource.microsoft.com/#system.core/System/Collections/Generic/HashSet.cs line 1489, which is missing a if (set1.Count != set2.Count) return false before performing the subset check.

The bug is illustrated by the following program:

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();
    }
}

Regarding other answers to this question (I don't have the rep to comment):

  • Wilhelm Liao: His answer also contains the bug because it's copied from the reference source
  • InBetween: The solution is not symmetric. CustomHashSetEqualityComparer.Equals(A, B) does not always equals CustomHashSetEqualityComparer.Equals(B, A). I would be scared of that.

I think a robust implementation should throw an exception if it encounters a set which has a different comparer to its own. It could always use its own comparer and ignore the set comparer, but that would give strange and unintuitive behaviour.

like image 42
Barry Smith Avatar answered Oct 25 '25 03:10

Barry Smith



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!