Хороший метод GetHashCode() для объектов списка Foo, соответствующих порядку
EnumerableObject : IEnumerable<Foo>
обертывает a List<Foo>
Если EnumerableObject a.SequenceEquals( EnumerableObject b)
, то они равны.
Следовательно, a GetHashCode
должен быть реализован. Проблема в том, что XOR каждый элемент в списке возвращает тот же хеш-код для любого списка со всеми и только с теми же элементами, независимо от порядка. Это нормально с точки зрения его работы, но приведет к многочисленным столкновениям, что замедлит поиск и т.д.
Что такое хороший, быстрый метод GetHashCode
для списков объектов, зависящих от порядка?
Ответы
Ответ 1
Я бы сделал это так же, как обычно, я совмещаю хэш-коды - с добавлением и умножением:
public override int GetHashCode()
{
unchecked
{
int hash = 19;
foreach (var foo in foos)
{
hash = hash * 31 + foo.GetHashCode();
}
return hash;
}
}
(Обратите внимание, что вы не должны добавлять что-либо в список после того, как это было использовано для ключа в хеш-таблице любого описания, поскольку хеш будет изменен. Это также предполагает, что нет нулевых записей - если бы будьте, вы должны это учитывать.)
Ответ 2
Во-первых, дважды проверьте, что вам нужен хэш-код вообще. Собираетесь ли вы переводить эти списки в структуру с хэш-отображением (например, словарь, hashset и т.д.)? Если нет, забудьте об этом.
Теперь, предполагая, что вы имеете в виду, что EnumerableObject уже переопределяет Equals(object)
(и, надеюсь, поэтому также реализует IEquatable<EnumerableObject>
) по какой-то причине, тогда это действительно необходимо. Вы хотите сбалансировать скорость и распределение бит.
Хорошей отправной точкой является mult + add или shift + xor, например:
public override int GetHashCode()
{
int res = 0x2D2816FE;
foreach(var item in this)
{
res = res * 31 + (item == null ? 0 : item.GetHashCode());
}
return res;
}
(Предполагается, что вы используете item.Equals() для сравнения равенства последовательностей, если вы используете IEqualityComparer равным, вам нужно будет вызвать его хэш-код).
Оттуда мы можем оптимизировать.
Если нулевые элементы запрещены, удалите нулевую проверку (будьте осторожны, это приведет к выбросу кода, если он когда-нибудь найдет нуль).
Если очень большие списки являются общими, нам нужно уменьшить число обследованных, пытаясь не приводить к большому количеству столкновений. Сравните следующие различные реализации:
public override int GetHashCode()
{
int res = 0x2D2816FE;
int max = Math.Min(Count, 16);
for(int i = 0, i != max; ++i)
{
var item = this[i];
res = res * 31 + (item == null ? 0 : item.GetHashCode());
}
return res;
}
public override int GetHashCode()
{
int res = 0x2D2816FE;
int min = Math.Max(-1, Count - 16);
for(int i = Count -1, i != min; --i)
{
var item = this[i];
res = res * 31 + (item == null ? 0 : item.GetHashCode());
}
return res;
}
public override int GetHashCode()
{
int res = 0x2D2816FE;
int step = Count / 16 + 1;
for(int i = 0, i < Count; i += step)
{
var item = this[i];
res = res * 31 + (item == null ? 0 : item.GetHashCode());
}
return res;
}
Каждый из них ограничивает общее количество рассмотренных элементов, что ускоряет выполнение, но снижает уровень хеширования. Какой (если таковой имеется) лучше всего зависит от того, являются ли коллекции с одним и тем же началом или с тем же концом более вероятными.
Изменение числа 16 выше регулирует баланс; меньше быстрее, но выше качество хеша с меньшим риском хеш-коллизий.
Изменить: теперь вы можете использовать реализацию SpookyHash v. 2:
public override int GetHashCode()
{
var hasher = new SpookyHash();//use methods with seeds if you need to prevent HashDos
foreach(var item in this)
hasher.Update(item.GetHashCode());//or relevant feeds of item, etc.
return hasher.Final().GetHashCode();
}
Это создаст гораздо лучшее распределение, чем mult + add или shift + xor, а также будет особенно быстрым (особенно в 64-битных процессах, поскольку алгоритм оптимизирован для этого, хотя он хорошо работает и на 32-битных).
Ответ 3
Обычно метод .GetHashCode()
возвращает хеш на основе ссылки на объект (адрес указателя). Это связано с тем, что вычисление хеш-кода каждого элемента в перечислимом списке может быть очень интенсивным. Вместо того, чтобы перезаписывать существующее поведение, я предпочитаю использовать метод расширения и использовать его только там, где необходимо определить детерминированный хэш-код:
public static class EnumerableExtensions
{
public static int GetSequenceHashCode<TItem>(this IEnumerable<TItem> list)
{
if (list == null) return 0;
const int seedValue = 0x2D2816FE;
const int primeNumber = 397;
return list.Aggregate(seedValue, (current, item) => (current * primeNumber) + (Equals(item, default(TItem)) ? 0 : item.GetHashCode()));
}
}
Ответ 4
Это практически ответ Jon Skeet, но с лучшей производительностью. Я обнаружил, что использование хеш-кода каждого элемента является дорогостоящим и ненужным для создания хорошего хеша. Эта версия использует только хэш-код всех элементов "мощности 2" (0, 1, 3, 7 и т.д.).
static int GetHashCode<T>(IReadOnlyList<T> list) {
unchecked {
int hash = 19 * list.Count;
int i = 1;
while (i <= list.Count) {
hash = (hash * 31) + list[i - 1].GetHashCode();
i *= 2;
}
return hash;
}
}