Оберните делегата в IEqualityComparer - PullRequest
125 голосов
/ 19 сентября 2008

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

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

Ответы [ 13 ]

168 голосов
/ 15 сентября 2010

о важности 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);
}

Выход:

x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 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']
x: 1346013431
[KEY = 'x: 1388845717']
x: 1388845717
[KEY = 'x: 1576754134']
x: 1576754134
[KEY = 'x: 1104067189']
x: 1104067189
[KEY = 'x: 1144789201']
x: 1144789201
[KEY = 'x: 1862076501']
x: 1862076501
[KEY = 'x: 1573781440']
x: 1573781440
[KEY = 'x: 646797592']
x: 646797592
[KEY = 'x: 655632802']
x: 655632802
[KEY = 'x: 1206819377']
x: 1206819377

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

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

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

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

Выход:

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

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

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


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

Расширение orip's answer :

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

  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));
    }
}
117 голосов
/ 06 августа 2009

Если вы хотите настроить проверку на равенство, 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();
    }
}
48 голосов
/ 19 сентября 2008

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

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));
42 голосов
/ 15 сентября 2010

Обычно я решаю эту проблему, комментируя ответ @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 );
    }
}
20 голосов
/ 02 августа 2011

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

  1. Полагается на EqualityComparer<>.Default для выполнения фактического сравнения, чтобы избежать коробок для типов значений (struct s), которые реализовали 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);

Человек простой класс:

class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}
11 голосов
/ 06 ноября 2008
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 ) );
    }
}
6 голосов
/ 27 мая 2011

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

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

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 голосов
/ 29 июня 2013

Реализация в (немецкий текст) Реализация 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));
       }
   }
2 голосов
/ 19 сентября 2008

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

1 голос
/ 06 апреля 2012

orip's answer замечательно. Расширяя ответ 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();
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...