Оберните делегата в IEqualityComparer


127

Некоторые функции Linq.Enumerable принимают расширение IEqualityComparer<T>. Есть ли удобный класс-оболочка, который адаптирует delegate(T,T)=>boolдля реализации IEqualityComparer<T>? Его достаточно легко написать (если вы игнорируете проблемы с определением правильного хэш-кода), но я хотел бы знать, есть ли готовое решение.

В частности, я хочу выполнять операции set с Dictionarys, используя только ключи для определения членства (при сохранении значений в соответствии с разными правилами).

Ответы:


44

Обычно я бы решил эту проблему, прокомментировав ответ @Sam (я немного отредактировал исходный пост, чтобы немного очистить его, не меняя поведения).

Ниже приводится мой рифф ответа @Sam с критическим исправлением [IMNSHO] политики хеширования по умолчанию: -

class FuncEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    public FuncEqualityComparer( Func<T, T, bool> comparer )
        : this( comparer, t => 0 ) // NB Cannot assume anything about how e.g., t.GetHashCode() interacts with the comparer's behavior
    {
    }

    public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> hash )
    {
        _comparer = comparer;
        _hash = hash;
    }

    public bool Equals( T x, T y )
    {
        return _comparer( x, y );
    }

    public int GetHashCode( T obj )
    {
        return _hash( obj );
    }
}

5
Насколько я понимаю, это правильный ответ. Все, IEqualityComparer<T>что упускается GetHashCode, просто ломается.
— Дэн Тао

1
@ Джошуа Франк: Недопустимо использовать хеш-равенство, чтобы подразумевать равенство - верно только обратное. Короче говоря, @Dan Tao полностью прав в том, что он говорит, и этот ответ является просто приложением этого факта к ранее неполному ответу
— Рубен Бартелинк

2
@Ruben Bartelink: Спасибо за разъяснения. Но я все еще не понимаю вашу политику хеширования t => 0. Если все объекты всегда хешируют одно и то же (ноль), то разве это не еще более нарушено, чем использование obj.GetHashCode, на точку @Dan Tao? Почему не всегда заставлять вызывающего абонента предоставлять хорошую хеш-функцию?
— Джошуа Франк,

1
Таким образом, неразумно предполагать, что произвольный алгоритм в Func, который он предоставил, не может вернуть true, несмотря на разные хэш-коды. Ваша точка зрения о том, что постоянный возврат нуля - это просто не хеширование, верна. Вот почему существует перегрузка, которая берет на себя функцию хеширования, когда профилировщик сообщает нам, что поиск недостаточно эффективен. Единственный момент во всем этом заключается в том, что если вы собираетесь использовать алгоритм хеширования по умолчанию, он должен быть таким, который работает 100% времени и не имеет опасного, внешне правильного поведения. И тогда мы сможем работать над спектаклем!
— Рубен Бартелинк,

4
Другими словами, поскольку вы используете настраиваемый компаратор, он не имеет ничего общего с хэш-кодом объекта по умолчанию, связанным с компаратором по умолчанию , поэтому вы не можете его использовать.
— Peet Brits,

170

О важности GetHashCode

Другие уже прокомментировали тот факт, что любая настраиваемая IEqualityComparer<T>реализация действительно должна включать GetHashCodeметод ; но никто не удосужился подробно объяснить почему .

Вот почему. В вашем вопросе конкретно упоминаются методы расширения LINQ; почти все они полагаются на хэш-коды для правильной работы, потому что они используют хэш-таблицы внутри для повышения эффективности.

Взять Distinct, к примеру. Рассмотрим последствия этого метода расширения, если бы все, что он использовал, было Equalsметодом. Как вы определяете, был ли элемент уже отсканирован в последовательности, если вы только сканировали Equals? Вы перебираете всю коллекцию значений, которые уже просмотрели, и проверяете соответствие. Это приведет к Distinctиспользованию алгоритма O (N 2 ) в худшем случае вместо алгоритма O (N)!

К счастью, это не так. Distinctне просто использовать Equals; он также использует GetHashCode. На самом деле, он абсолютно не работает должным образом без соответствующего IEqualityComparer<T>источника питанияGetHashCode . Ниже приведен надуманный пример, иллюстрирующий это.

Скажем, у меня есть следующий тип:

class Value
{
    public string Name { get; private set; }
    public int Number { get; private set; }

    public Value(string name, int number)
    {
        Name = name;
        Number = number;
    }

    public override string ToString()
    {
        return string.Format("{0}: {1}", Name, Number);
    }
}

Теперь предположим, что у меня есть, List<Value>и я хочу найти все элементы с разными именами. Это идеальный вариант Distinctиспользования настраиваемого компаратора равенства. Итак, давайте использовать Comparer<T>класс из ответа Аку :

var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);

Теперь, если у нас есть группа Valueэлементов с одним и тем же Nameсвойством, все они должны свернуться в одно значение, возвращаемое Distinct, верно? Посмотрим...

var values = new List<Value>();

var random = new Random();
for (int i = 0; i < 10; ++i)
{
    values.Add("x", random.Next());
}

var distinct = values.Distinct(comparer);

foreach (Value x in distinct)
{
    Console.WriteLine(x);
}

Вывод:

х: 1346013431
х: 1388845717
х: 1576754134
х: 1104067189
х: 1144789201
х: 1862076501
х: 1573781440
х: 646797592
х: 655632802
х: 1206819377

Хм, это не сработало, не так ли?

О чем GroupBy? Попробуем это:

var grouped = values.GroupBy(x => x, comparer);

foreach (IGrouping<Value> g in grouped)
{
    Console.WriteLine("[KEY: '{0}']", g);
    foreach (Value x in g)
    {
        Console.WriteLine(x);
    }
}

Вывод:

[KEY = 'x: 1346013431']
х: 1346013431
[KEY = 'x: 1388845717']
х: 1388845717
[KEY = 'x: 1576754134']
х: 1576754134
[KEY = 'x: 1104067189']
х: 1104067189
[KEY = 'x: 1144789201']
х: 1144789201
[KEY = 'x: 1862076501']
х: 1862076501
[KEY = 'x: 1573781440']
х: 1573781440
[KEY = 'x: 646797592']
х: 646797592
[KEY = 'x: 655632802']
х: 655632802
[KEY = 'x: 1206819377']
х: 1206819377

Опять же: не сработало.

Если вы думаете об этом, было бы разумно Distinctиспользовать HashSet<T>(или эквивалент) внутренне, а GroupByтакже использовать что-то вроде Dictionary<TKey, List<T>>внутреннего. Может ли это объяснить, почему эти методы не работают? Попробуем это:

var uniqueValues = new HashSet<Value>(values, comparer);

foreach (Value x in uniqueValues)
{
    Console.WriteLine(x);
}

Вывод:

х: 1346013431
х: 1388845717
х: 1576754134
х: 1104067189
х: 1144789201
х: 1862076501
х: 1573781440
х: 646797592
х: 655632802
х: 1206819377

Да ... начинает иметь смысл?

Надеюсь, из этих примеров понятно, почему включение подходящего GetHashCodeв любую IEqualityComparer<T>реализацию так важно.


Оригинальный ответ

Расширяя ответ orip :

Здесь можно сделать несколько улучшений.

  1. Во-первых, я бы взял Func<T, TKey>вместо Func<T, object>; это предотвратит упаковку ключей типа значения в само фактическое значение keyExtractor.
  2. Во-вторых, я бы добавил where TKey : IEquatable<TKey>ограничение; это предотвратит упаковку в Equalsвызове ( object.Equalsпринимает objectпараметр; вам нужна IEquatable<TKey>реализация, чтобы принимать TKeyпараметр без его упаковки). Ясно, что это может представлять слишком серьезное ограничение, поэтому вы можете создать базовый класс без ограничения и производный класс с ним.

Вот как может выглядеть полученный код:

public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
    protected readonly Func<T, TKey> keyExtractor;

    public KeyEqualityComparer(Func<T, TKey> keyExtractor)
    {
        this.keyExtractor = keyExtractor;
    }

    public virtual bool Equals(T x, T y)
    {
        return this.keyExtractor(x).Equals(this.keyExtractor(y));
    }

    public int GetHashCode(T obj)
    {
        return this.keyExtractor(obj).GetHashCode();
    }
}

public class StrictKeyEqualityComparer<T, TKey> : KeyEqualityComparer<T, TKey>
    where TKey : IEquatable<TKey>
{
    public StrictKeyEqualityComparer(Func<T, TKey> keyExtractor)
        : base(keyExtractor)
    { }

    public override bool Equals(T x, T y)
    {
        // This will use the overload that accepts a TKey parameter
        // instead of an object parameter.
        return this.keyExtractor(x).Equals(this.keyExtractor(y));
    }
}

1
Ваш StrictKeyEqualityComparer.Equalsметод выглядит таким же, как KeyEqualityComparer.Equals. Есть ли TKey : IEquatable<TKey>ограничение делает по- TKey.Equalsразному работает?
— Джастин Морган

2
@JustinMorgan: Да - в первом случае, поскольку это TKeyможет быть любой произвольный тип, компилятор будет использовать виртуальный метод, Object.Equalsкоторый потребует упаковки параметров типа значения, например int. Однако в последнем случае, поскольку TKeyреализация ограничена IEquatable<TKey>, TKey.Equalsбудет использоваться метод, который не требует упаковки.
— Дэн Тао

2
Очень интересно, спасибо за информацию. Я понятия не имел, что GetHashCode имеет эти последствия LINQ, пока не увидел эти ответы. Полезно знать для будущего использования.
— Джастин Морган

1
@JohannesH: Наверное! Устранил бы необходимость StringKeyEqualityComparer<T, TKey>тоже.
— Дэн Тао

1
+1 @DanTao: Запоздалое спасибо за отличное объяснение того, почему никогда не следует игнорировать хэш-коды при определении равенства в .Net.
— Марсело Кантос

118

Если вы хотите настроить проверку равенства, в 99% случаев вы заинтересованы в определении ключей для сравнения, а не в самом сравнении.

Это может быть элегантное решение (концепция из метода сортировки списков Python ).

Использование:

var foo = new List<string> { "abc", "de", "DE" };

// case-insensitive distinct
var distinct = foo.Distinct(new KeyEqualityComparer<string>( x => x.ToLower() ) );

KeyEqualityComparerКласс:

public class KeyEqualityComparer<T> : IEqualityComparer<T>
{
    private readonly Func<T, object> keyExtractor;

    public KeyEqualityComparer(Func<T,object> keyExtractor)
    {
        this.keyExtractor = keyExtractor;
    }

    public bool Equals(T x, T y)
    {
        return this.keyExtractor(x).Equals(this.keyExtractor(y));
    }

    public int GetHashCode(T obj)
    {
        return this.keyExtractor(obj).GetHashCode();
    }
}

3
Это намного лучше, чем ответ Аку.
— SLaks 09

Однозначно правильный подход. На мой взгляд, есть пара улучшений, которые я упомянул в своем собственном ответе.
— Дэн Тао

1
Это очень элегантный код, но он не отвечает на вопрос, поэтому вместо этого я принял ответ @aku. Мне нужна оболочка для Func <T, T, bool>, и мне не нужно извлекать ключ, поскольку ключ уже выделен в моем словаре.
— Марсело Кантос,

6
@Marcelo: Хорошо, ты можешь это сделать; но имейте в виду, что если вы собираетесь использовать подход @aku, вам действительно следует добавить, Func<T, int>чтобы предоставить хэш-код для Tзначения (как было предложено, например, в ответе Рубена ). В противном случае IEqualityComparer<T>реализация, с которой вы остались, будет совершенно нарушена, особенно в отношении ее полезности в методах расширения LINQ. См. Мой ответ, почему это так.
— Дэн Тао

Это хорошо, но если бы выбранный ключ был типом значения, не было бы ненужного бокса. Возможно, было бы лучше иметь TKey для определения ключа.
— Грэм Амброуз

48

Боюсь, из коробки такой обертки нет. Однако создать его несложно:

class Comparer<T>: IEqualityComparer<T>
{
    private readonly Func<T, T, bool> _comparer;

    public Comparer(Func<T, T, bool> comparer)
    {
        if (comparer == null)
            throw new ArgumentNullException("comparer");

        _comparer = comparer;
    }

    public bool Equals(T x, T y)
    {
        return _comparer(x, y);
    }

    public int GetHashCode(T obj)
    {
        return obj.ToString().ToLower().GetHashCode();
    }
}

...

Func<int, int, bool> f = (x, y) => x == y;
var comparer = new Comparer<int>(f);
Console.WriteLine(comparer.Equals(1, 1));
Console.WriteLine(comparer.Equals(1, 2));

1
Однако будьте осторожны с этой реализацией GetHashCode. Если вы действительно собираетесь использовать его в какой-то хеш-таблице, вам понадобится что-то более надежное.
— thecoop

46
в этом коде есть серьезная проблема! легко придумать класс, который имеет два объекта, которые равны в терминах этого компаратора, но имеют разные хэш-коды.
— empi

10
Чтобы исправить это, классу нужен другой член, private readonly Func<T, int> _hashCodeResolverкоторый также должен быть передан в конструктор и использован в GetHashCode(...)методе.
— herzmeister

6
Мне любопытно: почему вы используете obj.ToString().ToLower().GetHashCode()вместо obj.GetHashCode()?
— Джастин Морган

3
Места в структуре, которые IEqualityComparer<T>неизменно используют хеширование за кулисами (например, LINQ GroupBy, Distinct, Except, Join и т. Д.), И контракт MS относительно хеширования нарушается в этой реализации. Вот отрывок из документации MS: «Реализации необходимы, чтобы гарантировать, что если метод Equals возвращает true для двух объектов x и y, то значение, возвращаемое методом GetHashCode для x, должно быть равно значению, возвращаемому для y». См .: msdn.microsoft.com/en-us/library/ms132155
— devgeezer

22

То же, что и ответ Дэна Тао, но с некоторыми улучшениями:

  1. Используется EqualityComparer<>.Defaultдля фактического сравнения, чтобы избежать упаковки для structреализованных типов значений IEquatable<>.

  2. С момента EqualityComparer<>.Defaultиспользования не взрывается null.Equals(something).

  3. Предоставлена ​​статическая оболочка, вокруг IEqualityComparer<>которой будет статический метод для создания экземпляра компаратора - упрощает вызов. сравнить

    Equality<Person>.CreateComparer(p => p.ID);

    с участием

    new EqualityComparer<Person, int>(p => p.ID);
  4. Добавлена ​​перегрузка для указания IEqualityComparer<>ключа.

Класс:

public static class Equality<T>
{
    public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector)
    {
        return CreateComparer(keySelector, null);
    }

    public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector, 
                                                         IEqualityComparer<V> comparer)
    {
        return new KeyEqualityComparer<V>(keySelector, comparer);
    }

    class KeyEqualityComparer<V> : IEqualityComparer<T>
    {
        readonly Func<T, V> keySelector;
        readonly IEqualityComparer<V> comparer;

        public KeyEqualityComparer(Func<T, V> keySelector, 
                                   IEqualityComparer<V> comparer)
        {
            if (keySelector == null)
                throw new ArgumentNullException("keySelector");

            this.keySelector = keySelector;
            this.comparer = comparer ?? EqualityComparer<V>.Default;
        }

        public bool Equals(T x, T y)
        {
            return comparer.Equals(keySelector(x), keySelector(y));
        }

        public int GetHashCode(T obj)
        {
            return comparer.GetHashCode(keySelector(obj));
        }
    }
}

вы можете использовать это так:

var comparer1 = Equality<Person>.CreateComparer(p => p.ID);
var comparer2 = Equality<Person>.CreateComparer(p => p.Name);
var comparer3 = Equality<Person>.CreateComparer(p => p.Birthday.Year);
var comparer4 = Equality<Person>.CreateComparer(p => p.Name, StringComparer.CurrentCultureIgnoreCase);

Person - это простой класс:

class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}

3
+1 за реализацию, которая позволяет вам предоставить средство сравнения для ключа. Помимо большей гибкости, это также позволяет избежать упаковки типов значений как для сравнений, так и для хеширования.
— devgeezer

2
Это наиболее подробный ответ здесь. Я также добавил нулевую проверку. Полная.
— nawfal

11
public class FuncEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    public FuncEqualityComparer( Func<T, T, bool> comparer )
        : this( comparer, t => t.GetHashCode())
    {
    }

    public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> hash )
    {
        _comparer = comparer;
        _hash = hash;
    }

    public bool Equals( T x, T y )
    {
        return _comparer( x, y );
    }

    public int GetHashCode( T obj )
    {
        return _hash( obj );
    }
}

С расширениями: -

public static class SequenceExtensions
{
    public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer )
    {
        return first.SequenceEqual( second, new FuncEqualityComparer<T>( comparer ) );
    }

    public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer, Func<T, int> hash )
    {
        return first.SequenceEqual( second, new FuncEqualityComparer<T>( comparer, hash ) );
    }
}

@Sam (который больше не существует на момент этого комментария): очищенный код без изменения поведения (и +1). Добавлен Рифф на stackoverflow.com/questions/98033/…
— Рубен Бартелинк

6

Орип ответ великолепен.

Вот небольшой способ расширения, чтобы сделать его еще проще:

public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, object>    keyExtractor)
{
    return list.Distinct(new KeyEqualityComparer<T>(keyExtractor));
}
var distinct = foo.Distinct(x => x.ToLower())

2

Я отвечу на свой вопрос. Чтобы обрабатывать словари как наборы, кажется, самый простой метод - применить операции с наборами к dict.Keys, а затем преобразовать их обратно в словари с помощью Enumerable.ToDictionary (...).


2

Реализация в (немецкий текст) Реализация IEqualityCompare с лямбда-выражением заботится о нулевых значениях и использует методы расширения для создания IEqualityComparer.

Чтобы создать IEqualityComparer в объединении Linq, вам просто нужно написать

persons1.Union(persons2, person => person.LastName)

Компаратор:

public class LambdaEqualityComparer<TSource, TComparable> : IEqualityComparer<TSource>
{
  Func<TSource, TComparable> _keyGetter;

  public LambdaEqualityComparer(Func<TSource, TComparable> keyGetter)
  {
    _keyGetter = keyGetter;
  }

  public bool Equals(TSource x, TSource y)
  {
    if (x == null || y == null) return (x == null && y == null);
    return object.Equals(_keyGetter(x), _keyGetter(y));
  }

  public int GetHashCode(TSource obj)
  {
    if (obj == null) return int.MinValue;
    var k = _keyGetter(obj);
    if (k == null) return int.MaxValue;
    return k.GetHashCode();
  }
}

Вам также необходимо добавить метод расширения для поддержки вывода типа

public static class LambdaEqualityComparer
{
       // source1.Union(source2, lambda)
        public static IEnumerable<TSource> Union<TSource, TComparable>(
           this IEnumerable<TSource> source1, 
           IEnumerable<TSource> source2, 
            Func<TSource, TComparable> keySelector)
        {
            return source1.Union(source2, 
               new LambdaEqualityComparer<TSource, TComparable>(keySelector));
       }
   }

1

Всего одна оптимизация: мы можем использовать готовый EqualityComparer для сравнения значений, а не делегировать его.

Это также сделало бы реализацию более чистой, поскольку фактическая логика сравнения теперь остается в GetHashCode () и Equals (), которые вы, возможно, уже перегрузили.

Вот код:

public class MyComparer<T> : IEqualityComparer<T> 
{ 
  public bool Equals(T x, T y) 
  { 
    return EqualityComparer<T>.Default.Equals(x, y); 
  } 

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

Не забудьте перегрузить методы GetHashCode () и Equals () на вашем объекте.

Этот пост мне помог: c # сравнить два общих значения

Sushil


1
NB та же проблема, что и указанная в комментарии на stackoverflow.com/questions/98033/… - НЕ могу предположить, что obj.GetHashCode () имеет смысл
— Рубен Бартелинк

4
Я не понимаю цели этого. Вы создали компаратор равенства, эквивалентный компаратору проверки на равенство по умолчанию. Так почему бы вам не использовать его напрямую?
— CodesInChaos 07

1

Орип ответ великолепен. Расширяя ответ orip:

Я думаю, что ключом решения является использование «метода расширения» для передачи «анонимного типа».

    public static class Comparer 
    {
      public static IEqualityComparer<T> CreateComparerForElements<T>(this IEnumerable<T> enumerable, Func<T, object> keyExtractor)
      {
        return new KeyEqualityComparer<T>(keyExtractor);
      }
    }

Использование:

var n = ItemList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList();
n.AddRange(OtherList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList(););
n = n.Distinct(x=>new{Vchr=x.Vchr,Id=x.Id}).ToList();

0
public static Dictionary<TKey, TValue> Distinct<TKey, TValue>(this IEnumerable<TValue> items, Func<TValue, TKey> selector)
  {
     Dictionary<TKey, TValue> result = null;
     ICollection collection = items as ICollection;
     if (collection != null)
        result = new Dictionary<TKey, TValue>(collection.Count);
     else
        result = new Dictionary<TKey, TValue>();
     foreach (TValue item in items)
        result[selector(item)] = item;
     return result;
  }

Это позволяет выбрать свойство с лямбдой следующим образом: .Select(y => y.Article).Distinct(x => x.ArticleID);


-2

Я не знаю существующего класса, но что-то вроде:

public class MyComparer<T> : IEqualityComparer<T>
{
  private Func<T, T, bool> _compare;
  MyComparer(Func<T, T, bool> compare)
  {
    _compare = compare;
  }

  public bool Equals(T x, Ty)
  {
    return _compare(x, y);
  }

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

Примечание: на самом деле я еще не скомпилировал и не запускал это, поэтому может быть опечатка или другая ошибка.


1
NB та же проблема, что и указанная в комментарии на stackoverflow.com/questions/98033/… - НЕ могу предположить, что obj.GetHashCode () имеет смысл
— Рубен Бартелинк
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.