Про важливість 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
, просто розбитий прямо.