Оберните делегата в 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.