Подтвердить что ты не робот

Хороший метод GetHashCode() для объектов списка Foo, соответствующих порядку

EnumerableObject : IEnumerable<Foo>

обертывает a List<Foo>

Если EnumerableObject a.SequenceEquals( EnumerableObject b), то они равны.

Следовательно, a GetHashCode должен быть реализован. Проблема в том, что XOR каждый элемент в списке возвращает тот же хеш-код для любого списка со всеми и только с теми же элементами, независимо от порядка. Это нормально с точки зрения его работы, но приведет к многочисленным столкновениям, что замедлит поиск и т.д.

Что такое хороший, быстрый метод GetHashCode для списков объектов, зависящих от порядка?

4b9b3361

Ответ 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;
    }
}