Для поиска HashSet.CreateSetComparer() не может указать IEqalityComparer, есть ли альтернатива?


В внутреннем источнике есть такой конструктор public HashSetEqualityComparer(IEqualityComparer<T> comparer), но он внутренний, поэтому я не могу его использовать.

По умолчанию, HashSet<T>.CreateSetComparer() просто использует конструктор без параметров, который будет применять EqualityComparer<T>.Default.

Есть ли способ получить HashSetEqualityComparer<T> с выбором IEqualityComparer<T>, не копируя код из исходного кода?
3 2

3 ответа:

Я думаю, что лучшее решение-это использование 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, который использует пользовательские компараторы, но это неудачное поведение связано, IMHO, больше с ошибкой существующей реализации, чем с отсутствием необходимой перегрузки; нет никаких причин, почему CreateSetComparer() не может вернуть IEqualityComparer, который фактически использует компараторы множеств, которые его сравнивают, как показывает приведенный выше код.

Если бы у меня был голос в нем, CreateSetComparer() вообще не был бы статическим методом. Тогда было бы очевидно или, по крайней мере, предсказуемо, что любой возвращаемый компаратор будет создан с помощью компаратор текущего набора.

Я согласен @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);
    }
}

Избегайте этого класса, если вы используете пользовательские компараторы. Он использует свой собственный компаратор проверки на равенство для выполнения метода GetHashCode, но при выполнении равенства(Набор1, Набор2) если Набор1 и Набор2 же компаратор проверки на равенство, на HashSetEqualityComparer будет использовать компаратор наборов. HashsetEqualityComparer будет использовать свой собственный компаратор только для равных, если Set1 и Set2 имеют разные компараторы

Становится все хуже. Это называется 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();
    }
}

Что касается других ответов на этот вопрос (у меня нет представителя, чтобы комментировать):

    Вильгельм Ляо: его ответ также содержит ошибку, потому что он скопирован из справочного источника
  • между ними: решение не симметрично. CustomHashSetEqualityComparer.Equals (A, B) не всегда равно CustomHashSetEqualityComparer.Равняется(B, A). Я бы этого испугался.

Я думаю, что надежная реализация должна выдавать исключение, если она сталкивается с набором, который имеет другой компаратор к своему собственному. Онможет всегда использовать свой собственный компаратор и игнорировать установленный компаратор, но это даст странное и неинтуитивное поведение.