Загорніть делегата в IEqualityComparer


127

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

Зокрема, я хочу робити операції на Dictionarys, використовуючи лише Ключі для визначення членства (зберігаючи значення відповідно до різних правил).

Відповіді:


44

Зазвичай я вирішую це, коментуючи відповідь @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
Таким чином, нерозумно вважати, що довільний алгоритм у функці, який він постачається, не може повернути істину, незважаючи на те, що хеш-коди відрізняються. Ваша думка, що повернення нуля весь час просто не має хешування, є правдою. Ось чому виникає перевантаження, яка займає хеш-функцію, коли профілер повідомляє нам, що пошук недостатньо ефективний. Єдиним моментом у всьому цьому є те, що якщо у вас буде алгоритм хешування за замовчуванням, він повинен бути таким, який працює 100% часу і не має небезпечної поверхово правильної поведінки. І тоді ми можемо працювати над виставою!
Рубен Бартелінк

4
Іншими слова, так як ви використовуєте для користувача компаратор це не має нічого спільного з об'єктом по замовчуванням хеша - коді , пов'язаної з по замовчуванням компаратора, таким чином , ви не можете використовувати його.
Піет Брітс

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
Це набагато краще, ніж відповідь Аку.
Слакс

Однозначно правильний підхід. Є кілька вдосконалень, які можна зробити, на мою думку, про які я вже згадував у власній відповіді.
Дан Дао

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's 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);

Особа - простий клас:

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). Додано Riff на stackoverflow.com/questions/98033/…
Рубен Bartelink

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 # порівняти два загальних значення

Сушил


1
NB ж питання , як зазначено в коментарі на stackoverflow.com/questions/98033 / ... - Cant припустити obj.GetHashCode () має сенс
Рубен Bartelink

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

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 / ... - Cant припустити obj.GetHashCode () має сенс
Рубен Bartelink
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.