О важности 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 :
Здесь можно сделать несколько улучшений.
- Во-первых, я бы взял
Func<T, TKey>
вместо Func<T, object>
; это предотвратит упаковку ключей типа значения в само фактическое значение keyExtractor
.
- Во-вторых, я бы добавил
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));
}
}
IEqualityComparer<T>
что упускаетсяGetHashCode
, просто ломается.