Який найкращий алгоритм переосмислення GetHashCode?


1448

У .NET GetHashCodeметод використовується у багатьох місцях у бібліотеках базового класу .NET. Правильне його виконання особливо важливо для швидкого пошуку предметів у колекції або при визначенні рівності.

Чи є стандартний алгоритм чи найкраща практика щодо того, як реалізувати GetHashCodeмої власні класи, щоб я не погіршував продуктивність?


38
Прочитавши це запитання та статтю нижче, я міг здійснити перебір GetHashCode. Я сподіваюся, що це буде корисно для інших. Настанови та правила для GetHashCode, написані Еріком Ліппертом
оновлення

4
"або визначити рівність": ні! Два об'єкти з однаковим хеш-кодом необов'язково рівні.
Томас Левеск

1
@ThomasLevesque Ви праві, два об'єкти з однаковим хеш-кодом необов'язково рівні. Але все GetHashCode()ще використовується в дуже багатьох реалізаціях Equals(). Це я мав на увазі під цим твердженням. GetHashCode()всередині Equals()часто використовується як ярлик для визначення нерівності , оскільки якщо два об'єкти мають різний хеш-код, вони повинні бути об'єктами, які не є рівними, а решта перевірки рівності не потрібно виконувати.
bitbonk

3
@bitbonk Зазвичай і те, GetHashCode()і інше Equals()потрібно переглянути всі поля обох об'єктів (рівним це потрібно зробити, якщо хеш-коди рівні або не перевірені). Через це дзвінок GetHashCode()всередину Equals()часто є зайвим і може знизити продуктивність. Equals()може також бути в змозі коротке замикання, що робить його набагато швидшим - проте в деяких випадках хеш-коди можуть бути кешовані, що робить GetHashCode()перевірку швидшою і такою вартістю. Детальніше див. У цьому питанні .
NotEnoughData

ОНОВЛЕННЯ СІЧНЯ 2020: Блог Еріка Ліпперта
Рік Девін

Відповіді:


1603

Зазвичай я йду з чимось на зразок реалізації, поданої у казковій ефективній Java Джоша Блоха . Це швидко і створює досить хороший хеш, який навряд чи спричинить зіткнення. Виберіть два різних простих числа, наприклад, 17 і 23, і виконайте:

public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = 17;
        // Suitable nullity checks etc, of course :)
        hash = hash * 23 + field1.GetHashCode();
        hash = hash * 23 + field2.GetHashCode();
        hash = hash * 23 + field3.GetHashCode();
        return hash;
    }
}

Як зазначається в коментарях, ви можете виявити, що краще вибрати велику розмноження, щоб помножити замість цього. Мабуть, 486187739 - це добре ... і хоча більшість прикладів, які я бачив з невеликою кількістю, як правило, використовують праймери, є принаймні подібні алгоритми, де часто використовуються непрості числа. В не quite- FNV приклад пізніше, наприклад, я використав цифри , які , мабуть , добре працюють - але початкове значення не є простим. (Константа множення є простим. Я не знаю, наскільки це важливо.)

Це краще, ніж звичайна практика використання XORхеш-кодів з двох основних причин. Припустимо, у нас є тип з двома intполями:

XorHash(x, x) == XorHash(y, y) == 0 for all x, y
XorHash(x, y) == XorHash(y, x) for all x, y

До речі, попередній алгоритм - це той, який зараз використовується компілятором C # для анонімних типів.

На цій сторінці представлено досить багато варіантів. Я думаю, що в більшості випадків вищесказане є "досить хорошим", і це неймовірно легко запам’ятати і правильно вийти. ФПНА альтернатива є аналогічно простий, але використовує різні константи і XORзамість того , щоб в ADDякості операції комбінування. Він виглядає приблизно так, як наведений нижче код, але звичайний алгоритм FNV працює на окремих байтах, тому це вимагатиме зміни для виконання однієї ітерації за байт, а не для 32-бітного хеш-значення. FNV також розроблений для змінної довжини даних, тоді як спосіб, яким ми їх тут використовуємо, завжди для однакової кількості значень поля. Зауваження до цієї відповіді свідчать про те, що код тут насправді не працює так само (як у випробуваному прикладі), як описано вище.

// Note: Not quite FNV!
public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = (int) 2166136261;
        // Suitable nullity checks etc, of course :)
        hash = (hash * 16777619) ^ field1.GetHashCode();
        hash = (hash * 16777619) ^ field2.GetHashCode();
        hash = (hash * 16777619) ^ field3.GetHashCode();
        return hash;
    }
}

Зауважте, що слід пам’ятати про те, що в ідеалі ви повинні запобігти зміні стану, що чутливий до рівності (і, отже, залежно від хеш-коду), після додавання його до колекції, що залежить від хеш-коду.

Відповідно до документації :

Ви можете замінити GetHashCode для незмінних типів посилань. Загалом, для змінних посилальних типів вам слід перекрити GetHashCode, лише якщо:

  • Ви можете обчислити хеш-код із полів, які не змінюються; або
  • Ви можете переконатися, що хеш-код змінного об'єкта не змінюється, поки об'єкт міститься у колекції, що спирається на його хеш-код.

8
Алгоритм, описаний у згаданій вами книзі, є дещо більш докладною, вона особливо описує, що робити для різних типів даних полів. Напр .: для полів типу тривалого використання (int) (поле ^ f >>> 32) замість того, щоб просто викликати GetHashcode. Чи long.GetHashCodes реалізовано саме так?
бітбонк

13
Так, Int64.GetHashCode робить саме це. На Яві, що вимагає боксу, звичайно. Це мені нагадує - час додати посилання на книгу ...
Джон Скіт

77
23 не є хорошим вибором, оскільки (на .net 3.5 SP1) Dictionary<TKey,TValue>передбачає хороший модуль розподілу певних прайменів. І 23 - один із них. Отже, якщо у вас є словник з місткістю 23, лише останній внесок GetHashCodeвпливає на складений хеш-код. Тому я вважаю за краще використовувати 29 замість 23.
CodesInChaos

23
@CodeInChaos: На останній внесок впливає лише останній внесок - так що, в гіршому випадку, доведеться переглянути всі 23 записи у словнику. Він все ще перевірятиме фактичний хеш-код кожного запису, який буде дешевим. Якщо у вас є невеликий словник, навряд чи це буде багато значення.
Джон Скіт

20
@ Vajda: Я зазвичай використовую 0 як ефективний хеш-код для null- що не те саме, що ігнорування поля.
Джон Скіт

431

Анонімний тип

Microsoft вже пропонує хороший загальний генератор HashCode: просто скопіюйте свої властивості / поля в анонімний тип і введіть хеш-код:

new { PropA, PropB, PropC, PropD }.GetHashCode();

Це буде працювати для будь-якої кількості властивостей. Тут не використовується бокс. Він просто використовує алгоритм, вже реалізований в рамках для анонімних типів.

ValueTuple - оновлення для C # 7

Як згадує @cactuaroid у коментарях, може використовуватися кордону значень. Це економить кілька натискань клавіш і, що ще важливіше, виконується чисто на стеку (без сміття):

(PropA, PropB, PropC, PropD).GetHashCode();

(Примітка. Оригінальна техніка, що використовує анонімні типи, здається, створює об’єкт на купі, тобто сміття, оскільки анонімні типи реалізуються як класи, хоча компілятор може це оптимізувати. Це було б цікаво порівняти ці параметри, але варіант кортежу повинен бути вищим.)


85
Так, анонімна GetHashCodeреалізація дуже ефективна (BTW - це те саме, що і у відповіді Джона Скіта), але єдина проблема цього рішення полягає в тому, що ви створюєте новий екземпляр при будь-якому GetHashCodeдзвінку. Це може бути дещо накладніше, зокрема, у випадку інтенсивного доступу до великих колекцій хеш ...
digEmВсі

5
@digEmAll Добре, я не замислювався над витратами на створення нового об’єкта. Відповідь Джона Скіта є найбільш ефективною і не використовуватиме бокс. (@Kumba. Щоб вирішити невірно встановлений в VB, просто використовуйте Int64 (довгий) і обрізайте його після обчислень.)
Rick Love

42
могли б просто сказати , new { PropA, PropB, PropC, PropD }.GetHashCode()теж
sehe

17
VB.NET повинен використовувати Key для створення анонімного типу: New With {Key PropA}.GetHashCode()інакше GetHashCode не поверне один і той же хеш-код для різних об'єктів з однаковими "ідентифікаційними" властивостями.
Девід Осборн

4
@Keith у такому випадку я б розглядав можливість збереження IEnumerable як значення списку десь замість того, щоб перераховувати його щоразу, коли хеш-код обчислюється. Калькулювання ToList кожного разу всередині GetHashCode може зашкодити продуктивності у багатьох ситуаціях.
Rick Love

105

Ось мій помічник хеш-коду.
Його перевага полягає в тому, що він використовує загальні аргументи типу і тому не спричиняє боксу:

public static class HashHelper
{
    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
         unchecked
         {
             return 31 * arg1.GetHashCode() + arg2.GetHashCode();
         }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            return 31 * hash + arg3.GetHashCode();
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, 
        T4 arg4)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            hash = 31 * hash + arg3.GetHashCode();
            return 31 * hash + arg4.GetHashCode();
        }
    }

    public static int GetHashCode<T>(T[] list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    public static int GetHashCode<T>(IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    /// <summary>
    /// Gets a hashcode for a collection for that the order of items 
    /// does not matter.
    /// So {1, 2, 3} and {3, 2, 1} will get same hash code.
    /// </summary>
    public static int GetHashCodeForOrderNoMatterCollection<T>(
        IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            int count = 0;
            foreach (var item in list)
            {
                hash += item.GetHashCode();
                count++;
            }
            return 31 * hash + count.GetHashCode();
        }
    }

    /// <summary>
    /// Alternative way to get a hashcode is to use a fluent 
    /// interface like this:<br />
    /// return 0.CombineHashCode(field1).CombineHashCode(field2).
    ///     CombineHashCode(field3);
    /// </summary>
    public static int CombineHashCode<T>(this int hashCode, T arg)
    {
        unchecked
        {
            return 31 * hashCode + arg.GetHashCode();   
        }
    }

Також у нього є метод розширення для забезпечення вільного інтерфейсу, тому ви можете використовувати його так:

public override int GetHashCode()
{
    return HashHelper.GetHashCode(Manufacturer, PartN, Quantity);
}

або так:

public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}

5
Не потрібно T[]окремо, як це вжеIEnumerable<T>
nawfal

5
Ви можете переробити ці методи та обмежити основну логіку однією функцією
nawfal

12
До речі, 31 - це зсув і віднімання процесора, що надзвичайно швидко.
Чуй Тей

4
@nightcoder ви могли б використовувати Params .
ANeves

6
@ChuiTey Це щось спільне у всіх мерсах Мерсенна .
Фарап

63

У бібліотеці Helper у мене є клас хешингу, який я використовую для цього.

/// <summary> 
/// This is a simple hashing function from Robert Sedgwicks Hashing in C book.
/// Also, some simple optimizations to the algorithm in order to speed up
/// its hashing process have been added. from: www.partow.net
/// </summary>
/// <param name="input">array of objects, parameters combination that you need
/// to get a unique hash code for them</param>
/// <returns>Hash code</returns>
public static int RSHash(params object[] input)
{
    const int b = 378551;
    int a = 63689;
    int hash = 0;

    // If it overflows then just wrap around
    unchecked
    {
        for (int i = 0; i < input.Length; i++)
        {
            if (input[i] != null)
            {
                hash = hash * a + input[i].GetHashCode();
                a = a * b;
            }
        }
    }

    return hash;
}

Потім просто ви можете використовувати його як:

public override int GetHashCode()
{
    return Hashing.RSHash(_field1, _field2, _field3);
}

Я не оцінював її ефективність, тому будь-які відгуки вітаються.


26
Ну, це спричинить бокс, якщо поля - це значення типів.
нічний кодер

5
"можна покращити пізніше, перехопивши OverflowException" Вся суть uncheckedполягає в тому, щоб уникнути виключень із переповнення, яке бажано GetHashCode. Тож це невірно, якщо значення переповнюється intі воно зовсім не шкодить.
Тім Шмелтер

1
Одне з питань цього алгоритму полягає в тому, що будь-який масив, заповнений нулями, завжди повертатиме 0, незалежно від його довжини
Натан Адамс

2
Цей хелперний метод також виділяє новий об’єкт []
Джеймс Ньютон-Кінг

1
Як згадує @NathanAdams, той факт, який nullпропускається повністю, може дати неочікувані результати. Замість того, щоб пропускати їх, слід просто використовувати якесь постійне значення, а не input[i].GetHashCode()коли input[i]null.
Девід Шварц

58

Ось мій клас помічників із застосуванням Джон Скіта .

public static class HashCode
{
    public const int Start = 17;

    public static int Hash<T>(this int hash, T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked((hash * 31) + h);
    }
}

Використання:

public override int GetHashCode()
{
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)
        .Hash(_field3);
}

Якщо ви не хочете писати метод розширення для System.Int32:

public readonly struct HashCode
{
    private readonly int _value;

    public HashCode(int value) => _value = value;

    public static HashCode Start { get; } = new HashCode(17);

    public static implicit operator int(HashCode hash) => hash._value;

    public HashCode Hash<T>(T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked(new HashCode((_value * 31) + h));
    }

    public override int GetHashCode() => _value;
}

Він все одно уникає будь-якого розподілу купи і використовується точно так само:

public override int GetHashCode()
{
    // This time `HashCode.Start` is not an `Int32`, it's a `HashCode` instance.
    // And the result is implicitly converted to `Int32`.
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)     
        .Hash(_field3);
}

Edit (травень 2018): EqualityComparer<T>.Defaultздобувач тепер JIT внутрішній - запит тягнути згадуються Стівен Toub в цьому блозі .


1
Я б змінив лінію з третинним оператором на:var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
Білл Баррі

Я вірю, що потрійний оператор з obj != nullбуде компілювати boxінструкцію, яка буде виділяти пам'ять, якщо Tце тип значення. Натомість ви можете використовувати, obj.Equals(null)який буде компілюватися у віртуальний виклик Equalsметоду.
Мартін Ліверсаж

Тому що this.hashCode != h. Це не поверне те саме значення.
Şafak Gür

На жаль, вдається видалити мій коментар, а не редагувати його. Чи вигідніше створити нову структуру, тоді змініть хеш-код на непрочитаний тільки і зробіть: "un галочку {this.hashCode ^ = h * 397;} поверніть це;" наприклад?
Ерік Карлссон

Незмінюваність має свої переваги ( Чому мутант ушкоджує зло? ). Щодо продуктивності, що я роблю, це досить дешево, оскільки не виділяє місця в купі.
Şafak Gür

30

.NET стандарт 2.1 і вище

Якщо ви використовуєте .NET Standard 2.1 або вище, ви можете використовувати структуру System.HashCode . Існує два способи його використання:

HashCode.Combine

CombineМетод може бути використаний для створення хеш - код, дані до восьми об'єктів.

public override int GetHashCode() => HashCode.Combine(this.object1, this.object2);

HashCode.Add

AddМетод допоможе вам впоратися з колекціями:

public override int GetHashCode()
{
    var hashCode = new HashCode();
    hashCode.Add(this.object1);
    foreach (var item in this.collection)
    {
        hashCode.Add(item);
    }
    return hashCode.ToHashCode();
}

GetHashCode зроблено легко

Ви можете прочитати повне повідомлення в блозі " GetHashCode Made Easy " для отримання більш детальної інформації та коментарів.

Приклад використання

public class SuperHero
{
    public int Age { get; set; }
    public string Name { get; set; }
    public List<string> Powers { get; set; }

    public override int GetHashCode() =>
        HashCode.Of(this.Name).And(this.Age).AndEach(this.Powers);
}

Впровадження

public struct HashCode : IEquatable<HashCode>
{
    private const int EmptyCollectionPrimeNumber = 19;
    private readonly int value;

    private HashCode(int value) => this.value = value;

    public static implicit operator int(HashCode hashCode) => hashCode.value;

    public static bool operator ==(HashCode left, HashCode right) => left.Equals(right);

    public static bool operator !=(HashCode left, HashCode right) => !(left == right);

    public static HashCode Of<T>(T item) => new HashCode(GetHashCode(item));

    public static HashCode OfEach<T>(IEnumerable<T> items) =>
        items == null ? new HashCode(0) : new HashCode(GetHashCode(items, 0));

    public HashCode And<T>(T item) => 
        new HashCode(CombineHashCodes(this.value, GetHashCode(item)));

    public HashCode AndEach<T>(IEnumerable<T> items)
    {
        if (items == null)
        {
            return new HashCode(this.value);
        }

        return new HashCode(GetHashCode(items, this.value));
    }

    public bool Equals(HashCode other) => this.value.Equals(other.value);

    public override bool Equals(object obj)
    {
        if (obj is HashCode)
        {
            return this.Equals((HashCode)obj);
        }

        return false;
    }

    public override int GetHashCode() => this.value.GetHashCode();

    private static int CombineHashCodes(int h1, int h2)
    {
        unchecked
        {
            // Code copied from System.Tuple a good way to combine hashes.
            return ((h1 << 5) + h1) ^ h2;
        }
    }

    private static int GetHashCode<T>(T item) => item?.GetHashCode() ?? 0;

    private static int GetHashCode<T>(IEnumerable<T> items, int startHashCode)
    {
        var temp = startHashCode;

        var enumerator = items.GetEnumerator();
        if (enumerator.MoveNext())
        {
            temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));

            while (enumerator.MoveNext())
            {
                temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));
            }
        }
        else
        {
            temp = CombineHashCodes(temp, EmptyCollectionPrimeNumber);
        }

        return temp;
    }
}

Що робить хороший алгоритм?

Швидкість

Алгоритм, який обчислює хеш-код, повинен бути швидким. Простий алгоритм, як правило, буде більш швидким.

Детермінований

Алгоритм хешування повинен бути детермінованим, тобто з одиничним входом він завжди повинен отримувати однаковий вихід.

Зменшити зіткнення

Алгоритм, який обчислює хеш-код, повинен підтримувати хеш-колізії до мінімуму. Хеш-зіткнення - це ситуація, яка виникає, коли два дзвінки GetHashCodeна два різних об'єкти створюють однакові хеш-коди. Зауважте, що колізії дозволені (деякі мають помилкові уявлення, що їх немає), але їх слід звести до мінімуму.

Хороша хеш-функція повинна максимально рівномірно відображати очікувані входи в межах її вихідного діапазону. Він повинен мати рівномірність.

Профілактика DoS

У .NET Core кожен раз при перезапуску програми ви отримуватимете різні хеш-коди. Це функція безпеки для запобігання атакам відмови в сервісі (DoS). Для .NET Framework слід активувати цю функцію, додавши наступний файл App.config:

<?xml version ="1.0"?>  
<configuration>  
   <runtime>  
      <UseRandomizedStringHashAlgorithm enabled="1" />  
   </runtime>  
</configuration>

Через цю особливість хеш-коди ніколи не повинні використовуватися за межами домену додатка, в якому вони створені, вони ніколи не повинні використовуватися як ключові поля в колекції, і вони ніколи не повинні зберігатися.

Детальніше про це читайте тут .

Криптографічно безпечний?

Алгоритм не повинен бути криптографічним хеш-функцією . Це означає, що він не повинен відповідати наступним умовам:

  • Генерувати повідомлення, яке дає задане хеш-значення, неможливо
  • Неможливо знайти два різних повідомлення з однаковим хеш-значенням
  • Невелика зміна повідомлення повинна настільки сильно змінити хеш-значення, що нове хеш-значення виявиться некоррельованим зі старим хеш-значенням (ефект лавини).

29

У більшості випадків, коли Equals () порівнює кілька полів, це насправді не має значення, якщо ваш GetHash () хешируется на одному полі чи на багатьох. Вам просто потрібно переконатися, що обчислення хеса є дуже дешевим ( без розподілу , будь ласка) та швидким ( Без великих обчислень і, звичайно, немає підключень до бази даних) та забезпечує хороший розподіл.

Важкий підйом повинен бути частиною методу рівних (); хеш повинен бути дуже дешевою операцією, щоб дозволити виклику рівнянь () якомога менше елементів.

І ще одна остання порада: не сподівайтеся на те, що GetHashCode () стабільний протягом кількох запусків заявок . Багато типів .et не гарантують, що їх хеш-коди залишаться однаковими після перезавантаження, тому вам слід використовувати лише значення GetHashCode () для структури даних пам'яті.


10
"У більшості випадків, коли Equals () порівнює кілька полів, це насправді не має значення, якщо ваш GetHash () хеширується на одному полі чи на багатьох." Це небезпечна порада, оскільки для об’єктів, які відрізняються лише неочищеними полями, ви отримаєте хеш-зіткнення. Якщо це трапляється часто, продуктивність колекцій на основі хешу (HashMap, HashSet тощо) погіршиться (до O (n) в гіршому випадку).
sleske

10
Це фактично сталося на Java: у ранніх версіях JDK String.hashCode () вважався лише початком рядка; це призведе до проблем з продуктивністю, якщо ви використовували Strings як ключі в HashMaps, які відрізнялися лише в кінці (що є загальним, наприклад, для URL-адрес). Тому алгоритм був змінений (я вважаю, JDK 1.2 або 1.3).
sleske

3
Якщо це одне поле "забезпечує хороший розподіл" (остання частина моєї відповіді), то одного поля достатньо. Якщо воно не забезпечує хорошого розподілу , то (і тільки тоді) вам потрібен інший розрахунок. (Наприклад, просто використовуйте інше поле, яке забезпечує хороший розподіл, або використовуйте кілька полів)
Bert Huijben,

Я не думаю, що існує проблема з GetHashCodeвиконанням розподілу пам'яті, за умови, що це робиться лише в перший раз, коли це використовується (з наступними викликами просто повертається кешований результат). Важливим є не те, що слід уникати великих зіткнень, а краще уникати "системних" зіткнень. Якщо тип має два intполя oldXі newXякі часто відрізняються між собою, хеш-значення oldX^newXпризначатиме 90% таких записів хеш-значень 1, 2, 4 або 8. Використання oldX+newX[неперевіреної арифметики] може генерувати більше зіткнень ...
supercat

1
... ніж більш досконала функція, але колекція 1 000 000 речей, що мають 500 000 різних хеш-значень, буде дуже добре, якщо у кожного значення хеша є дві пов'язані речі, і дуже погано, якщо один хеш-значення має 500,001 речі, а інші мають по одній.
supercat

23

Донедавна моя відповідь була б дуже близькою до Джона Скіта. Однак я нещодавно розпочав проект, в якому використовували хеш-таблиці потужності з двох потужностей, тобто хеш-таблиці, де розмір внутрішньої таблиці становить 8, 16, 32 і т.д. є деякі переваги і до потужності двох розмірів.

І це в значній мірі смоктало. Тому після невеликих експериментів та досліджень я почав повторно перебирати хеші з наступним:

public static int ReHash(int source)
{
  unchecked
  {
    ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
    ulong d = 0xE2ADBEEFDEADBEEF ^ c;
    ulong a = d += c = c << 15 | c >> -15;
    ulong b = a += d = d << 52 | d >> -52;
    c ^= b += a = a << 26 | a >> -26;
    d ^= c += b = b << 51 | b >> -51;
    a ^= d += c = c << 28 | c >> -28;
    b ^= a += d = d << 9 | d >> -9;
    c ^= b += a = a << 47 | a >> -47;
    d ^= c += b << 54 | b >> -54;
    a ^= d += c << 32 | c >> 32;
    a += d << 25 | d >> -25;
    return (int)(a >> 1);
  }
}

І тоді моя хеш-таблиця з потужністю більше не смоктала.

Це мене, однак, турбувало, тому що вищезгадане не повинно працювати. Або точніше, він не повинен працювати, якщо оригінал не GetHashCode()був дуже конкретним чином.

Повторне змішування хеш-коду не може поліпшити чудовий хеш-код, оскільки єдиний можливий ефект полягає в тому, що ми вводимо ще кілька зіткнень.

Повторне змішування хеш-коду не може поліпшити жахливий хеш-код, оскільки єдиний можливий ефект полягає в тому, що ми змінюємо, наприклад, велику кількість зіткнень зі значенням 53 на велику кількість значення 18,3487,291.

Повторне змішування хеш-коду може лише покращити хеш-код, який принаймні досить вдало уникнув абсолютних зіткнень у всьому його діапазоні (2 32 можливі значення), але погано уникнути зіткнень, коли модуль знизився для фактичного використання в хеш-таблиці. Хоча простіший модуль таблиці потужності два зробив це більш очевидним, він також мав негативний ефект у порівнянні з більш поширеними таблицями простих чисел, що це було не так очевидно (додаткова робота з переосмислення перевищила б користь , але користь все одно буде).

Редагувати: Я також використовував відкриту адресацію, що також підвищило б чутливість до зіткнення, можливо, тим більше, ніж факт, що це потужність двох.

І добре, це турбує, наскільки string.GetHashCode()реалізації в .NET (або тут можна вивчити ) можна поліпшити таким чином (на порядок тестів, які працюють приблизно в 20-30 разів швидше через меншу кількість зіткнень) і більше заважає, наскільки мої власні хеш-коди можна було б покращити (набагато більше, ніж це).

Усі реалізації GetHashCode (), які я кодував у минулому, і які фактично використовувались як основа відповідей на цьому сайті, були набагато гіршими, ніж я хотіла . Значну частину часу це було «досить добре» для більшості застосувань, але я хотів чогось кращого.

Тому я поставив цей проект на одну сторону (це все одно був проект для домашніх тварин) і почав дивитися на те, як швидко створити хороший, добре розподілений хеш-код у .NET.

Врешті-решт я вирішив перенести SpookyHash на .NET. Дійсно, наведений вище код є швидкою версією використання SpookyHash для отримання 32-бітного виходу з 32-бітного входу.

Тепер, SpookyHash - це не приємно швидко запам'ятати фрагмент коду. Мій порт його навіть менший, тому що я вручив багато його для кращої швидкості *. Але саме для цього використовується повторне використання коду.

Потім я поставив цей проект на одну сторону, тому що так само, як і в початковому проекті було поставлено питання про те, як створити кращий хеш-код, таким чином проект створив питання про те, як створити кращу метчі .NET.

Потім я повернувся і створив безліч перевантажень, щоб легко вводити майже decimalхеш-код майже всіх рідних типів (крім †).

Це швидко, для чого Боб Дженкінс заслуговує більшої частини заслуг, оскільки його оригінальний код, з якого я перенісся, все ще швидший, особливо на 64-бітних машинах, алгоритм яких оптимізований для ‡.

Повний код можна побачити на https://bitbucket.org/JonHanna/spookilysharp/src, але врахуйте, що наведений вище код є спрощеною його версією.

Однак, оскільки це вже написано, можна скористатися ним простіше:

public override int GetHashCode()
{
  var hash = new SpookyHash();
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

Він також приймає насінні значення, тому якщо вам потрібно мати справу з ненадійним входом і хочете захиститись від Hash DoS-атак, ви можете встановити насіння на основі тривалості роботи або подібного, і зробити результати зловмисниками непередбачуваними:

private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
  //produce different hashes ever time this application is restarted
  //but remain consistent in each run, so attackers have a harder time
  //DoSing the hash tables.
  var hash = new SpookyHash(hashSeed0, hashSeed1);
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

* Великою несподіванкою в цьому є те, що вручну вбудований метод обертання, який повертає (x << n) | (x >> -n)покращені речі. Я був би впевнений, що тремтіння підкреслило б це для мене, але профілювання показало інше.

decimalне є рідною з точки зору .NET, хоча це з C #. Проблема з цим полягає в тому, що її власні GetHashCode()розцінюють точність як важливу, а її Equals()- ні. Обидва є правильним вибором, але не змішуються так. Реалізуючи свою власну версію, вам потрібно вибрати одну чи іншу, але я не можу знати, що ви хочете.

‡ За допомогою порівняння. Якщо використовується на рядку, SpookyHash на 64 бітах значно швидший, ніж string.GetHashCode()на 32 бітах, що трохи швидше, ніж string.GetHashCode()на 64 бітах, що значно швидше, ніж SpookyHash на 32 бітах, хоча все ще досить швидкий, щоб бути розумним вибором.


При комбінуванні декількох значень хешу в одне я схильний використовувати longзначення для проміжних результатів, а потім з’єдную кінцевий результат до рівня int. Це здається гарною ідеєю? Моє занепокоєння полягає в тому, що можна використовувати, наприклад, хеш = (хеш * 31) + nextField, то пари відповідних значень впливатимуть лише на верхні 27 біт хеша. Дозволити обчислення поширитись на longта обертання матеріалів, це мінімізує цю небезпеку.
supercat

@supercat це залежить від розподілу вашого остаточного розміщення. Бібліотека SpookilySharp забезпечить, щоб розподіл був хорошим, в ідеалі (тому що він не потребує створення об'єкта), передавши вказівник на тип, що виблискує, або передавши один із перелічених даних, якими він безпосередньо обробляє, але якщо ви ще не маєте проглядання даних або відповідного перерахування, тоді виклик .Update()з кількома значеннями відповідно до відповіді вище.
Джон Ханна

@JonHanna, чи бажаєте ви бути більш точними щодо проблемної поведінки, з якою ви стикалися? Я намагаюсь реалізувати бібліотеку, яка робить реалізацію об'єктів цінності тривіальною ( ValueUtils ), і мені б хотілося, щоб тестовий набір демонстрував погану змішуваність хеш-сигналів у потужності двох хештелів.
Еймон Нербонна

@EamonNerbonne Я насправді не маю нічого більш точного, ніж "загальний час таким чином пройшов повільніше". Як я додав у редакції, той факт, що я використовував відкриту адресацію, може бути важливішим, ніж фактор потужності двох. Я планую зробити кілька тестових випадків для конкретного проекту, де я порівнюю декілька різних підходів, тому, можливо, після цього я маю кращу відповідь, хоча це не є першочерговим завданням (особистий проект без нагальної потреби , тож я доберуся до нього, коли доберуся до нього ...)
Джон Ханна

@JonHanna: так, я знаю, як проходить особистий графік проекту - удачі! У будь-якому випадку, я бачу, що я не чітко висловив цей останній коментар: я мав намір попросити проблематичний вклад, а не обов’язково деталі проблем, які виникли. Я хотів би використовувати це як тестовий набір (або натхнення для тестового набору). У будь-якому випадку - удачі з проектом вашого улюбленця :-).
Еймон Нербонна

13

Це добре:

/// <summary>
/// Helper class for generating hash codes suitable 
/// for use in hashing algorithms and data structures like a hash table. 
/// </summary>
public static class HashCodeHelper
{
    private static int GetHashCodeInternal(int key1, int key2)
    {
        unchecked
        {
           var num = 0x7e53a269;
           num = (-1521134295 * num) + key1;
           num += (num << 10);
           num ^= (num >> 6);

           num = ((-1521134295 * num) + key2);
           num += (num << 10);
           num ^= (num >> 6);

           return num;
        }
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="arr">An array of objects used for generating the 
    /// hash code.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode(params object[] arr)
    {
        int hash = 0;
        foreach (var item in arr)
            hash = GetHashCodeInternal(hash, item.GetHashCode());
        return hash;
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <param name="obj4">The fourth object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and
    /// data structures like a hash table.
    /// </returns>
    public static int GetHashCode<T1, T2, T3, T4>(T1 obj1, T2 obj2, T3 obj3,
        T4 obj4)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3, obj4));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2, T3>(T1 obj1, T2 obj2, T3 obj3)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2>(T1 obj1, T2 obj2)
    {
        return GetHashCodeInternal(obj1.GetHashCode(), obj2.GetHashCode());
    }
}

А ось як ним користуватися:

private struct Key
{
    private Type _type;
    private string _field;

    public Type Type { get { return _type; } }
    public string Field { get { return _field; } }

    public Key(Type type, string field)
    {
        _type = type;
        _field = field;
    }

    public override int GetHashCode()
    {
        return HashCodeHelper.GetHashCode(_field, _type);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is Key))
            return false;
        var tf = (Key)obj;
        return tf._field.Equals(_field) && tf._type.Equals(_type);
    }
}

1
Як визначаються ключі? GetHashCode () не приймає жодних параметрів, тому його потрібно викликати двома ключами, які потрібно якось визначити. Вибачте, без додаткових пояснень це виглядає лише розумно, але не так добре.
Майкл Стум

І навіщо вам потрібні загальні перевантаження? Тип не важливий (і не використовується у вашому коді), оскільки всі об'єкти мають GetHashCode()метод, тому ви завжди можете використовувати метод з paramsпараметром масиву. Або я щось тут пропускаю?
gehho

4
Коли ви використовуєте об'єкт замість дженерики, ви отримуєте бокси та асигнування пам'яті, чого ви не хочете в GetHashCode. Тож генерики - це шлях.
CodesInChaos

1
h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);Східні кроки зсуву / xor ( маємо код-код: вони не залежать від будь-яких вхідних даних і виглядають для мене жахливо.
sehe

1
@Magnus так, я вилучу свій оригінальний коментар. Лише невелика примітка, що це може бути не таким швидким, як деякі інші рішення тут, але, як ви кажете, це не має значення. Розподіл чудовий, кращий за більшість рішень тут, тому +1 від мене! :)
nawfal

11

Станом на https://github.com/dotnet/coreclr/pull/14863 , існує новий спосіб генерування хеш-кодів, який дуже простий! Просто напишіть

public override int GetHashCode()
    => HashCode.Combine(field1, field2, field3);

Це створить якісний хеш-код, не турбуючись про деталі реалізації.


Це виглядає як приємне доповнення ... будь-який спосіб дізнатися, яку версію .NET Core, що постачається?
Dan J

1
@DanJ Який щасливий збіг, HashCodeзміни для corefx були об'єднані за пару годин до вашого коментаря :) Тип планується для доставки в. NET Core 2.1.
Джеймс Ко

Це приголомшливо - і зовсім час повороту. Отримано. :)
Dan J

@DanJ Ще краща новина - вона повинна бути доступна зараз на нічних версіях CoreFX, розміщених на дотнет-ядрі MyGet.
Джеймс Ко

Солодке - це не допомагає мені на роботі, оскільки ми не зовсім такі кровоточиві, але добре знати. Ура!
Dan J

9

Ось ще одна вільна реалізація алгоритму, розміщеного вище Джоном Скітом , але який не включає асигнувань або бокс-операцій:

public static class Hash
{
    public const int Base = 17;

    public static int HashObject(this int hash, object obj)
    {
        unchecked { return hash * 23 + (obj == null ? 0 : obj.GetHashCode()); }
    }

    public static int HashValue<T>(this int hash, T value)
        where T : struct
    {
        unchecked { return hash * 23 + value.GetHashCode(); }
    }
}

Використання:

public class MyType<T>
{
    public string Name { get; set; }

    public string Description { get; set; }

    public int Value { get; set; }

    public IEnumerable<T> Children { get; set; }

    public override int GetHashCode()
    {
        return Hash.Base
            .HashObject(this.Name)
            .HashObject(this.Description)
            .HashValue(this.Value)
            .HashObject(this.Children);
    }
}

Компілятор гарантує HashValue, що не викликається класом через обмеження загального типу. Але підтримки для компілятора немає, HashObjectоскільки додавання загального аргументу також додає операцію боксу.


8

Ось мій спрощений підхід. Для цього я використовую класичний шаблон для будівельників. Це typesafe (без боксу / unboxing), а також сумісний з .NET 2.0 (відсутність методів розширення тощо).

Він використовується так:

public override int GetHashCode()
{
    HashBuilder b = new HashBuilder();
    b.AddItems(this.member1, this.member2, this.member3);
    return b.Result;
} 

А ось клас акутальних будівельників:

internal class HashBuilder
{
    private const int Prime1 = 17;
    private const int Prime2 = 23;
    private int result = Prime1;

    public HashBuilder()
    {
    }

    public HashBuilder(int startHash)
    {
        this.result = startHash;
    }

    public int Result
    {
        get
        {
            return this.result;
        }
    }

    public void AddItem<T>(T item)
    {
        unchecked
        {
            this.result = this.result * Prime2 + item.GetHashCode();
        }
    }

    public void AddItems<T1, T2>(T1 item1, T2 item2)
    {
        this.AddItem(item1);
        this.AddItem(item2);
    }

    public void AddItems<T1, T2, T3>(T1 item1, T2 item2, T3 item3)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
    }

    public void AddItems<T1, T2, T3, T4>(T1 item1, T2 item2, T3 item3, 
        T4 item4)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
    }

    public void AddItems<T1, T2, T3, T4, T5>(T1 item1, T2 item2, T3 item3, 
        T4 item4, T5 item5)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
        this.AddItem(item5);
    }        

    public void AddItems<T>(params T[] items)
    {
        foreach (T item in items)
        {
            this.AddItem(item);
        }
    }
}

ви можете уникнути створення об'єкта всередині функції gethashcode, як у відповіді Мангуса. Просто зателефонуйте чортовим статичним хеш-функціям (хто піклується про початковий хеш). Крім того, ви можете використовувати AddItems<T>(params T[] items)метод частіше в класі помічників (ніж дзвонити AddItem(T)кожен раз).
nawfal

І яку користь ви робите, this.result * Prime2 * item.GetHashCode()коли часто використовуєте this.result * Prime2 + item.GetHashCode()?
nawfal

Я не можу використовувати AddItems<T>(params T[] items)частіше, тому що typeof(T1) != typeof(T2)тощо
bitbonk

о так, я пропустив це.
nawfal

5

Користувачі ReSharper можуть генерувати GetHashCode, Equals та ін ReSharper -> Edit -> Generate Code -> Equality Members.

// ReSharper's GetHashCode looks like this
public override int GetHashCode() {
    unchecked {
        int hashCode = Id;
        hashCode = (hashCode * 397) ^ IntMember;
        hashCode = (hashCode * 397) ^ OtherIntMember;
        hashCode = (hashCode * 397) ^ (RefMember != null ? RefMember.GetHashCode() : 0);
        // ...
        return hashCode;
    }
}

4

Якщо у нас не більше 8 об'єктів (сподіваємось), ось ще одна альтернатива.

ValueTupleє структурою і, здається, має надійну GetHashCodeреалізацію.

Це означає, що ми могли просто зробити це:

// Yay, no allocations and no custom implementations!
public override int GetHashCode() => (this.PropA, this.PropB).GetHashCode();

Давайте розглянемо поточну реалізацію .NET Core для ValueTuples GetHashCode.

Це від ValueTuple:

    internal static int CombineHashCodes(int h1, int h2)
    {
        return HashHelpers.Combine(HashHelpers.Combine(HashHelpers.RandomSeed, h1), h2);
    }

    internal static int CombineHashCodes(int h1, int h2, int h3)
    {
        return HashHelpers.Combine(CombineHashCodes(h1, h2), h3);
    }

І це від HashHelper:

    public static readonly int RandomSeed = Guid.NewGuid().GetHashCode();

    public static int Combine(int h1, int h2)
    {
        unchecked
        {
            // RyuJIT optimizes this to use the ROL instruction
            // Related GitHub pull request: dotnet/coreclr#1830
            uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
            return ((int)rol5 + h1) ^ h2;
        }
    }

Англійською:

  • Лівий поворот (круговий зсув) h1 на 5 позицій.
  • Додайте результат і h1 разом.
  • XOR результат з h2.
  • Почніть з виконання зазначеної операції на {статичному випадковому насінні, h1}.
  • Для кожного наступного елемента виконайте операцію над попереднім результатом та наступним пунктом (наприклад, h2).

Було б непогано дізнатися більше про властивості цього алгоритму хеш-коду ROL-5.

На жаль, відкладатись на ValueTupleсебе GetHashCodeможе не так швидко, як ми хотіли б і очікуємо. Цей коментар у відповідній дискусії ілюструє, що безпосередньо дзвінок HashHelpers.Combineє більш ефективним. З іншого боку, це є внутрішнім, тому нам доведеться копіювати код, жертвуючи велику частину того, що ми отримали тут. Крім того, ми несемо відповідальність за те, щоб пам'ятати спочатку Combineз випадковим насінням. Я не знаю, які наслідки, якщо ми пропустимо цей крок.


Якщо припустити, що h1 >> 27це ігнорувати 0, то це h1 << 5дорівнює h1 * 32тому h1 * 33 ^ h2. За цією сторінкою вона називається "Модифікований Бернштейн".
кактуароїд

3

Більшість моєї роботи виконується з підключенням до бази даних, що означає, що всі мої класи мають унікальний ідентифікатор із бази даних. Я завжди використовую ідентифікатор із бази даних, щоб генерувати хеш-код.

// Unique ID from database
private int _id;

...    
{
  return _id.GetHashCode();
}

Це означає, що якщо у вас є об'єкти Person і Account, і вони мають і ID = 1, вони матимуть однаковий хеш-код. І це не нормально.
перо

15
Насправді вищевказаний коментар є невірним. Завжди буде можливість зіткнення хеш-коду (хеш-код розміщує лише відро, а не окремий об'єкт). Таким чином, така реалізація - для хеш-коду, що містить змішані об'єкти - призвела б до безлічі зіткнень, що небажано, але було б абсолютно чудово, якби у ваших хештелях ви коли-небудь мали об’єкти одного типу. Крім того, він не розподіляє рівномірно, однак також не реалізує базову реалізацію на system.object, тому я б не переживав про це занадто ...
piers7,

2
Хеш-код може бути просто ідентифікатором, оскільки id - ціле число. Не потрібно телефонувати на GetHashCode цілим числом (це функція ідентичності)
Darrel Lee

2
@DarrelLee, але томо його _id міг би бути Guid. Це хороша практика кодування, _id.GetHashCodeяк це зрозуміло.
nawfal

2
@ 1224, залежно від моделей використання, це може бути жахливо з причини, яку ви надаєте, але це також може бути чудово; якщо у вас є послідовність таких чисел без отворів, ви маєте ідеальний хеш, кращий, ніж будь-який алгоритм. Якщо ви знаєте, що це так, ви навіть можете розраховувати на нього і пропустити перевірку рівності.
Джон Ханна

3

Дуже схожа на рішення нічного кодера, за винятком того, що легше підняти праймери, якщо ви хочете.

PS: Це один з тих часів, коли ви трохи блукаєте в рот, знаючи, що це може бути перероблене на один метод з 9 за замовчуванням, але це буде повільніше, тому ви просто закриєте очі і намагаєтеся забути про це.

/// <summary>
/// Try not to look at the source code. It works. Just rely on it.
/// </summary>
public static class HashHelper
{
    private const int PrimeOne = 17;
    private const int PrimeTwo = 23;

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();
            hash = hash * PrimeTwo + arg10.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();

            return hash;
        }
    }
}

2
Не обробляє нулі.
JJS

1

Я зіткнувся з проблемою з поплавцями та десятковими знаками, використовуючи реалізацію, обрану як відповідь вище.

Цей тест не вдається (плаває; хеш - те саме, хоча я переключив 2 значення на негативні):

        var obj1 = new { A = 100m, B = 100m, C = 100m, D = 100m};
        var obj2 = new { A = 100m, B = 100m, C = -100m, D = -100m};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

Але цей тест проходить (з ints):

        var obj1 = new { A = 100m, B = 100m, C = 100, D = 100};
        var obj2 = new { A = 100m, B = 100m, C = -100, D = -100};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

Я змінив свою реалізацію, щоб не використовувати GetHashCode для примітивних типів і, здається, працює краще

    private static int InternalComputeHash(params object[] obj)
    {
        unchecked
        {
            var result = (int)SEED_VALUE_PRIME;
            for (uint i = 0; i < obj.Length; i++)
            {
                var currval = result;
                var nextval = DetermineNextValue(obj[i]);
                result = (result * MULTIPLIER_VALUE_PRIME) + nextval;

            }
            return result;
        }
    }



    private static int DetermineNextValue(object value)
    {
        unchecked
        {

                int hashCode;
                if (value is short
                    || value is int
                    || value is byte
                    || value is sbyte
                    || value is uint
                    || value is ushort
                    || value is ulong
                    || value is long
                    || value is float
                    || value is double
                    || value is decimal)
                {
                    return Convert.ToInt32(value);
                }
                else
                {
                    return value != null ? value.GetHashCode() : 0;
                }
        }
    }

1
У разі , якщо інший намір uncheckedНЕ впливає на Convert.ToInt32: uint, long, float, doubleі decimalвсе це може Переповнення тут.
Марк Херд

1

Майкрософт веде декілька способів хешування ...

//for classes that contain a single int value
return this.value;

//for classes that contain multiple int value
return x ^ y;

//for classes that contain single number bigger than int    
return ((int)value ^ (int)(value >> 32)); 

//for classes that contain class instance fields which inherit from object
return obj1.GetHashCode();

//for classes that contain multiple class instance fields which inherit from object
return obj1.GetHashCode() ^ obj2.GetHashCode() ^ obj3.GetHashCode(); 

Я можу здогадатися, що для кількох великих int ви можете використовувати це:

int a=((int)value1 ^ (int)(value1 >> 32));
int b=((int)value2 ^ (int)(value2 >> 32));
int c=((int)value3 ^ (int)(value3 >> 32));
return a ^ b ^ c;

І те саме для мульти-типів: усі перетворені спочатку в intвикористання, GetHashCode() тоді значення int будуть xor'ed, а результат - ваш хеш.

Для тих, хто використовує хеш як ID (я маю на увазі унікальне значення), хеш, природно, обмежений кількома цифрами, я думаю, це було 5 байтів для алгоритму хешування, принаймні MD5.

Ви можете перетворити кілька значень у хешоване значення, а деякі з них однакові, тому не використовуйте його як ідентифікатор. (можливо, якось я буду використовувати ваш компонент)


7
Хортінг цілих чисел для створення хеш-коду - добре відомий антипатерн, який, як правило, призводить до особливо великої кількості зіткнень зі значеннями реального світу.
Джон Ханна

Кожен тут використовує ціле число, і ніколи не було жодної гарантії того, що хеш буде однаковим, він просто намагався бути настільки ж різним, як мало трапляється зіткнень.
мертвийManN

Так, але ваш другий і п'ятий не намагаються уникати зіткнень.
Джон Ханна

1
Так, цей антипатерн досить поширений.
Джон Ханна

2
Досягти балансу. Використовуйте дійсно хороший хеш-код, як Spookyhash, і ви отримаєте набагато, набагато краще уникнення зіткнень, але це матиме набагато більше часу на обчислення, ніж будь-яке з них (але якщо мова йде про хешування дуже великих обсягів даних, Spookyhash надзвичайно швидко). Простий зсув на одне із значень перед xoring - це лише незначні додаткові витрати для гарного зменшення зіткнення. Множення основного числа збільшується як час, так і якість. Що краще між shift або mult, тому є дискусійним. Звичайний xor, хоча дуже часто стикається з реальними даними, і його найкраще уникати
Jon Hanna

1

Це статичний хелперний клас, який реалізує реалізацію Джоша Блоха; і забезпечує явні перевантаження для "запобігання" боксу, а також для реалізації хешу спеціально для довгих примітивів.

Ви можете пройти порівняння рядків, яке відповідає вашій рівній реалізації.

Оскільки вихід Hash завжди є цілим, ви можете просто ланцюжок Hash-дзвінків.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;


namespace Sc.Util.System
{
    /// <summary>
    /// Static methods that allow easy implementation of hashCode. Example usage:
    /// <code>
    /// public override int GetHashCode()
    ///     => HashCodeHelper.Seed
    ///         .Hash(primitiveField)
    ///         .Hsh(objectField)
    ///         .Hash(iEnumerableField);
    /// </code>
    /// </summary>
    public static class HashCodeHelper
    {
        /// <summary>
        /// An initial value for a hashCode, to which is added contributions from fields.
        /// Using a non-zero value decreases collisions of hashCode values.
        /// </summary>
        public const int Seed = 23;

        private const int oddPrimeNumber = 37;


        /// <summary>
        /// Rotates the seed against a prime number.
        /// </summary>
        /// <param name="aSeed">The hash's first term.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int rotateFirstTerm(int aSeed)
        {
            unchecked {
                return HashCodeHelper.oddPrimeNumber * aSeed;
            }
        }


        /// <summary>
        /// Contributes a boolean to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aBoolean">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, bool aBoolean)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (aBoolean
                                ? 1
                                : 0);
            }
        }

        /// <summary>
        /// Contributes a char to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aChar">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, char aChar)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aChar;
            }
        }

        /// <summary>
        /// Contributes an int to the developing HashCode seed.
        /// Note that byte and short are handled by this method, through implicit conversion.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aInt">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, int aInt)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aInt;
            }
        }

        /// <summary>
        /// Contributes a long to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aLong">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, long aLong)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (int)(aLong ^ (aLong >> 32));
            }
        }

        /// <summary>
        /// Contributes a float to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aFloat">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, float aFloat)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + Convert.ToInt32(aFloat);
            }
        }

        /// <summary>
        /// Contributes a double to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aDouble">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, double aDouble)
            => aSeed.Hash(Convert.ToInt64(aDouble));

        /// <summary>
        /// Contributes a string to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aString">The value to contribute.</param>
        /// <param name="stringComparison">Optional comparison that creates the hash.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(
                this int aSeed,
                string aString,
                StringComparison stringComparison = StringComparison.Ordinal)
        {
            if (aString == null)
                return aSeed.Hash(0);
            switch (stringComparison) {
                case StringComparison.CurrentCulture :
                    return StringComparer.CurrentCulture.GetHashCode(aString);
                case StringComparison.CurrentCultureIgnoreCase :
                    return StringComparer.CurrentCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.InvariantCulture :
                    return StringComparer.InvariantCulture.GetHashCode(aString);
                case StringComparison.InvariantCultureIgnoreCase :
                    return StringComparer.InvariantCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.OrdinalIgnoreCase :
                    return StringComparer.OrdinalIgnoreCase.GetHashCode(aString);
                default :
                    return StringComparer.Ordinal.GetHashCode(aString);
            }
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// Each element may be a primitive, a reference, or a possibly-null array.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, IEnumerable aArray)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (object item in aArray) {
                ++countPlusOne;
                if (item is IEnumerable arrayItem) {
                    if (!object.ReferenceEquals(aArray, arrayItem))
                        aSeed = aSeed.Hash(arrayItem); // recursive call!
                } else
                    aSeed = aSeed.Hash(item);
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// You must provide the hash function for each element.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <param name="hashElement">Required: yields the hash for each element
        /// in <paramref name="aArray"/>.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash<T>(this int aSeed, IEnumerable<T> aArray, Func<T, int> hashElement)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (T item in aArray) {
                ++countPlusOne;
                aSeed = aSeed.Hash(hashElement(item));
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null object to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, object aObject)
        {
            switch (aObject) {
                case null :
                    return aSeed.Hash(0);
                case bool b :
                    return aSeed.Hash(b);
                case char c :
                    return aSeed.Hash(c);
                case int i :
                    return aSeed.Hash(i);
                case long l :
                    return aSeed.Hash(l);
                case float f :
                    return aSeed.Hash(f);
                case double d :
                    return aSeed.Hash(d);
                case string s :
                    return aSeed.Hash(s);
                case IEnumerable iEnumerable :
                    return aSeed.Hash(iEnumerable);
            }
            return aSeed.Hash(aObject.GetHashCode());
        }


        /// <summary>
        /// This utility method uses reflection to iterate all specified properties that are readable
        /// on the given object, excluding any property names given in the params arguments, and
        /// generates a hashcode.
        /// </summary>
        /// <param name="aSeed">The developing hash code, or the seed: if you have no seed, use
        /// the <see cref="Seed"/>.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <param name="propertySelector"><see cref="BindingFlags"/> to select the properties to hash.</param>
        /// <param name="ignorePropertyNames">Optional.</param>
        /// <returns>A hash from the properties contributed to <c>aSeed</c>.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashAllProperties(
                this int aSeed,
                object aObject,
                BindingFlags propertySelector
                        = BindingFlags.Instance
                        | BindingFlags.Public
                        | BindingFlags.GetProperty,
                params string[] ignorePropertyNames)
        {
            if (aObject == null)
                return aSeed.Hash(0);
            if ((ignorePropertyNames != null)
                    && (ignorePropertyNames.Length != 0)) {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (!propertyInfo.CanRead
                            || (Array.IndexOf(ignorePropertyNames, propertyInfo.Name) >= 0))
                        continue;
                    aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            } else {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (propertyInfo.CanRead)
                        aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            }
            return aSeed;
        }


        /// <summary>
        /// NOTICE: this method is provided to contribute a <see cref="KeyValuePair{TKey,TValue}"/> to
        /// the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on the Key or Value here if that itself is a KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePair">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeyAndValue<TKey, TValue>(this int aSeed, KeyValuePair<TKey, TValue> keyValuePair)
            => aSeed.Hash(keyValuePair.Key)
                    .Hash(keyValuePair.Value);

        /// <summary>
        /// NOTICE: this method is provided to contribute a collection of <see cref="KeyValuePair{TKey,TValue}"/>
        /// to the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on a Key or Value here if that itself is a KeyValuePair or an Enumerable of
        /// KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePairs">The values to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeysAndValues<TKey, TValue>(
                this int aSeed,
                IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs)
        {
            if (keyValuePairs == null)
                return aSeed.Hash(null);
            foreach (KeyValuePair<TKey, TValue> keyValuePair in keyValuePairs) {
                aSeed = aSeed.HashKeyAndValue(keyValuePair);
            }
            return aSeed;
        }
    }
}

Yipes: Я знайшов помилку! HashKeysAndValuesМетод був зафіксований: він викликає HashKeyAndValue.
Стівен Коко

0

У випадку, якщо ви хочете зробити полів HashCodeзnetstandard2.1

public static class HashCode
{
    public static int Combine(params object[] instances)
    {
        int hash = 17;

        foreach (var i in instances)
        {
            hash = unchecked((hash * 31) + (i?.GetHashCode() ?? 0));
        }

        return hash;
    }
}

Примітка: Якщо використовується з struct, вона виділятиме пам'ять завдяки боксу

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.