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

╄→гoц情女王★ 提交于 2020-08-08 04:06:10

问题


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?


回答1:


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.




回答2:


I agree @InBetween, using SetEquals is the best way. Even if add the constructor still can not achieve what you want.

please see this code: http://referencesource.microsoft.com/#System.Core/System/Collections/Generic/HashSet.cs,1360

Here is I try to do:

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

UPDATE

I took 5 mins to implement another version form HashSetEqualityComparer<T> source code. And rewrite the bool Equals(HashSet<T> x, HashSet<T> y) method. It is not complex. All code just copy and paste from source, I just revise a bit.

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



回答3:


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.



来源:https://stackoverflow.com/questions/48842499/hashsett-createsetcomparer-cannot-specify-iequalitycomparert-is-there-an

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!