Runtime: Добавление типа HashCode для упрощения комбинирования хэш-кодов

Созданный на 25 апр. 2016  ·  206Комментарии  ·  Источник: dotnet/runtime

Замена долгого обсуждения с более чем 200 комментариями новой проблемой dotnet / corefx # 14354

Выпуск ЗАКРЫТ !!!


Мотивация

В Java есть Objects.hash для быстрого объединения хэш-кодов составляющих полей для возврата в Object.hashCode() . К сожалению, .NET не имеет такого эквивалента, и разработчики вынуждены использовать собственные хэши следующим образом :

public override int GetHashCode()
{
    unchecked
    {
        int result = 17;
        result = result * 23 + field1.GetHashCode();
        result = result * 23 + field2.GetHashCode();
        return result;
    }
}

Иногда люди даже прибегают к использованию для этого Tuple.Create(field1, field2, ...).GetHashCode() , что (очевидно) плохо, поскольку оно выделяет.

Предложение

  • Список изменений в текущем предложении (по сравнению с последней утвержденной версией https://github.com/dotnet/corefx/issues/8034#issuecomment-262331783):

    • Добавлено свойство Empty (как естественная отправная точка, аналогичная ImmutableArray )

    • Обновлены имена аргументов: hash -> hashCode , obj -> item

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public HashCode();

        public static HashCode Empty { get; }

        public static HashCode Create(int hashCode);
        public static HashCode Create<T>(T item);
        public static HashCode Create<T>(T item, IEqualityComparer<T> comparer);

        public HashCode Combine(int hashCode);
        public HashCode Combine<T>(T item);
        public HashCode Combine<T>(T item, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public override string ToString();
    }
}

Использование:

`` С #
int hashCode1 = HashCode.Create (f1) .Combine (f2) .Value;
int hashCode2 = hash.Aggregate (HashCode.Empty, (seed, hash) => seed.Combine (хеш));

var hashCode3 = HashCode.Empty;
foreach (int хеш в хешах) {hashCode3 = hashCode3.Combine (хеш); }
(интервал) hashCode3;
`` ''

Примечания

Реализация должна использовать алгоритм в HashHelpers .

Design Discussion api-needs-work area-System.Numerics

Самый полезный комментарий

[@redknightlois] Если нам нужно обоснование того, зачем покупать System я могу попробовать оправдание. Мы создали HashCode чтобы помочь в реализации object.GetHashCode() , кажется уместным, что оба будут совместно использовать пространство имен.

Это было обоснование, которое мы с @KrzysztofCwalina использовали. Продал!

Все 206 Комментарий

Если вам нужно что-то быстрое и грязное, вы можете использовать ValueTuple.Create(field1, field2).GetHashCode() . Это тот же алгоритм, что и в Tuple (который в этом отношении аналогичен таковому в Objects ) и не требует дополнительных затрат на выделение ресурсов.

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

@JonHanna Я думаю, что этот вопрос также применим, например, к string.GetHashCode() . Я не понимаю, почему предоставить Hash должно быть сложнее.

На самом деле, это должно быть проще, поскольку пользователи с особыми требованиями могут легко перестать использовать Hash , но прекратить использование string.GetHashCode() сложнее.

+1

У нас есть один из них в ASP.NET, https://github.com/aspnet/Common/blob/dev/src/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs. Это также инлайн дружелюбный.

Если вам нужно что-то быстрое и грязное, вы можете использовать ValueTuple.Create (field1, field2) .GetHashCode ().

Ах, хорошая идея, я не подумал о ValueTuple когда писал этот пост. К сожалению, я не думаю, что это будет доступно до C # 7 / следующего релиза фреймворка, или даже не знаю, будет ли он таким производительным (эти вызовы свойств / методов в EqualityComparer могут складываться). Но я не проводил никаких тестов, чтобы измерить это, поэтому я точно не знаю. Я просто думаю, что должен быть выделенный / простой класс для хеширования, который люди могли бы использовать без использования кортежей в качестве хакерского обходного пути.

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

Абсолютно согласен, но я не думаю, что большинство реализаций учитывают это, например, текущая реализация ArraySegment довольно наивна. Основная цель этого класса (наряду с предотвращением выделения памяти) - предоставить доступную реализацию для людей, мало разбирающихся в хешировании, чтобы они не могли сделать что-то вроде этой глупости. Люди, которым необходимо иметь дело с описанными вами ситуациями, могут реализовать свой собственный алгоритм хеширования.

К сожалению, я не думаю, что это будет доступно до C # 7 / следующей версии фреймворка.

Я думаю, вы можете использовать его с C # 2, только не со встроенной поддержкой.

или даже знать, будет ли он такой производительностью (эти вызовы свойств / методов в EqualityComparer могут складываться)

Что бы этот класс сделал по-другому? Если явный вызов obj == null ? 0 : obj.GetHashCode() происходит быстрее, его следует переместить в ValueTuple .

Я был бы склонен +1 к этому предложению пару недель назад, но я менее склонен в свете того, что ValueTuple сокращает накладные расходы на выделение трюка с использованием Tuple для этого, мне кажется, что это находится между двумя стульями: если вам не нужно что-то особо специализированное, вы можете использовать ValueTuple , но если вам нужно что-то сверх этого, то такой класс далеко не уйдет достаточно.

А когда у нас будет C # 7, в нем появится синтаксический сахар, который сделает его еще проще.

@JonHanna

Что бы этот класс сделал по-другому? Если явно вызвать obj == null? 0: obj.GetHashCode () работает быстрее, чем его следует переместить в ValueTuple.

Почему бы не использовать ValueTuple просто класс Hash для получения хеш-кодов? Это также значительно уменьшит LOC в файле (который сейчас составляет около 2000 строк).

редактировать:

Если вам не нужно что-то особенное, вы можете использовать ValueTuple

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

Что я действительно могу отстать.

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

Например, нам пришлось самостоятельно кодировать xxHash32, xxHash64, Metro128, а также понижать дискретизацию со 128 до 64 и с 64 до 32 бит. Наличие набора оптимизированных функций может помочь разработчикам избежать написания собственных неоптимизированных и / или ошибочных (я знаю, что мы тоже обнаружили несколько ошибок); но все же возможность выбора в зависимости от потребностей.

Мы с радостью пожертвуем наши реализации, если есть интерес, чтобы они могли быть проанализированы и оптимизированы специалистами.

@redknightlois Я был бы счастлив добавить свою реализацию SpookyHash к подобным усилиям.

@svick Осторожнее со string.GetHashCode (), это очень специфично, по очень веской причине, DoS-атаки с хешированием.

@terrajobst , как далеко он находится в очереди на проверку / проверку API? Я думаю, что это простой API, который мы всегда хотели добавить к платформе, и, возможно, теперь у нас достаточно критической массы, чтобы действительно это сделать?

Копия: @ellismg

Думаю, готов к рассмотрению в текущем состоянии.

@mellinoe Замечательно ! Я немного очистил предложение, чтобы сделать его более лаконичным, а также добавил несколько вопросов в конце, которые, я думаю, следует решить.

@jamesqo Также должно быть long .

@redknightlois , звучит разумно. Я обновил предложение, добавив long перегрузок из Combine .

Предложение @JonHanna недостаточно?

C# return ValueTuple.Create(a, b, c).GetHashCode();

Если нет достаточно веских причин, почему этого недостаточно, мы не думаем, что это помогает.

Помимо сгенерированного кода, который на несколько порядков хуже, я не могу придумать другой достаточно веской причины. Если, конечно, в новой среде выполнения нет оптимизаций, которые имеют дело с этим конкретным случаем, в этом случае этот анализ является спорным. Сказав, что я пробовал это на 1.0.1.

Позвольте мне проиллюстрировать это на примере.

Предположим, что мы берем реальный код, который используется для ValueTuple и используем константы для его вызова.

        internal static class HashHelpers
        {
            public static int Combine(int h1, int h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                uint shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryStaticCall()
        {
            return HashHelpers.Combine(10202, 2003);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryValueTuple()
        {
            return ValueTuple.Create(10202, 2003).GetHashCode();
        }
    }

Теперь при оптимизирующем компиляторе шансы, что никакой разницы быть не должно, но на самом деле она есть.

Это фактический код для ValueTuple

image
Итак, что можно здесь увидеть? Сначала мы создаем структуру в стеке, а затем вызываем фактический хеш-код.

Теперь сравните это с использованием HashHelper.Combine которое для всех целей может быть фактической реализацией Hash.Combine

image

Я знаю!!!
Но не будем останавливаться на достигнутом ... воспользуемся фактическими параметрами:

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryStaticCall(int h1, int h2)
        {
            return HashHelpers.Combine(h1, h2);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryValueTuple(int h1, int h2)
        {
            return ValueTuple.Create(h1, h2).GetHashCode();
        }

        static unsafe void Main(string[] args)
        {
            var g = new Random();
            int h1 = g.Next();
            int h2 = g.Next(); 
            Console.WriteLine(TryStaticCall(h1, h2));
            Console.WriteLine(TryValueTuple(h1, h2));
        }

image

Хорошо, что это очень стабильно. Но давайте сравним его с альтернативой:

image

А теперь давайте переборщить ...

        internal static class HashHelpers
        {
            public static int Combine(int h1, int h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                uint shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }
            public static int Combine(int h1, int h2, int h3, int h4)
            {
                return Combine(Combine(h1, h2), Combine(h3, h4));
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryStaticCall(int h1, int h2, int h3, int h4)
        {
            return HashHelpers.Combine(h1, h2, h3, h4);
        }

И результат довольно показательный

image

На самом деле я не могу проверить фактический код, который JIT генерирует для вызова, но достаточно пролога и эпилога, чтобы оправдать включение предложения.

image

Вывод из анализа прост: тип холдинга struct не означает, что он бесплатный :)

Во время встречи был поднят вопрос о перформансе. Вопрос в том, окажется ли этот API на горячем пути. Чтобы было ясно, я не говорю, что у нас не должно быть API. Я просто говорю, что, если нет конкретного сценария, сложнее разработать API, потому что мы не можем сказать, «он нам нужен для X, поэтому мерилом успеха является то, может ли X его использовать». Это важно для API, которые не позволяют вам делать что-то новое, а скорее делают то же самое в более оптимизированной форме.

Я думаю, что чем важнее иметь быстрый хэш хорошего качества, тем важнее настроить алгоритм, используемый для объекта (ов) и диапазона значений, которые могут быть видны, и, следовательно, тем больше вам нужно таких помощник, тем более не нужно использовать такой помощник.

@terrajobst , производительность была главной, но не единственной мотивацией этого предложения. Наличие выделенного типа поможет облегчить обнаружение; даже со встроенной поддержкой кортежей в C # 7 разработчики могут не знать, что они приравниваются по значению. Даже если они это сделают, они могут забыть о том, что кортежи переопределяют GetHashCode , и, скорее всего, в конечном итоге придется Google, как реализовать GetHashCode в .NET.

Кроме того, существует небольшая проблема с правильностью использования ValueTuple.Create.GetHashCode . Последние 8 элементов, хешируются только последние 8 элементов; остальные игнорируются.

@terrajobst В RavenDB производительность GetHashCode настолько сильно сказалась на нашей прибыли, что мы в итоге реализовали целый набор высокооптимизированных подпрограмм. Даже у Roslyn есть собственное внутреннее хеширование https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/InternalUtilities/Hash.cs, также проверьте обсуждение Roslyn специально здесь: https: // github .com / dotnet / coreclr / issues / 1619 ... Итак, когда производительность является КЛЮЧЕВОЙ, мы не можем использовать предоставленную платформу и должны использовать собственную (и платить за последствия).

Также проблема @jamesqo полностью актуальна. Необязательно объединять такое количество хэшей, но для 1 миллиона случаев есть кто-то, кто собирается перешагнуть через обрыв с этим.

@JonHanna

Я думаю, что чем важнее иметь быстрый хэш хорошего качества, тем важнее настроить алгоритм, используемый для объекта (ов) и диапазона значений, которые могут быть видны, и, следовательно, тем больше вам нужно таких помощник, тем более не нужно использовать такой помощник.

Итак, вы говорите, что добавление вспомогательного класса было бы плохим, поскольку это побудило бы людей просто добавить вспомогательную функцию, не думая о том, как сделать правильный хеш?

Похоже, что на самом деле было бы наоборот; Hash.Combine обычно должен улучшать реализации GetHashCode . Люди, которые знают, как выполнять хеширование, могут оценить Hash.Combine чтобы увидеть, подходит ли это их варианту использования. Новички, которые на самом деле не знают о хешировании, будут использовать Hash.Combine вместо простого xor-ing (или, что еще хуже, добавления) составляющих полей, потому что они не знают, как сделать правильный хеш.

Мы обсудили это еще немного, и вы нас убедили :-)

Еще несколько вопросов:

  1. Нам нужно решить, куда поставить этот тип. Представление нового пространства имен кажется странным; Хотя System.Numerics может работать. System.Collections.Generic также может работать, потому что у него есть компараторы, а хеширование чаще всего используется в контексте коллекций.
  2. Должны ли мы предоставить шаблон построения без выделения памяти для объединения неизвестного количества хэш-кодов?

В (2) @Eilon сказал следующее:

Для справки, ASP.NET Core (и его предшественники и связанные проекты) используют HashCodeCombiner: https://github.com/aspnet/Common/blob/dev/src/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs

( @David Fowler упомянул об этом в ветке GitHub несколько месяцев назад.)

Это пример использования: https://github.com/aspnet/Mvc/blob/760c8f38678118734399c58c2dac981ea6e47046/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ViewLocationCacheKey.cs4#L129 -LocationCacheKey.cs4#L129 -L129 -L

`` С #
var hashCodeCombiner = HashCodeCombiner.Start ();
hashCodeCombiner.Add (IsMainPage? 1: 0);
hashCodeCombiner.Add (ViewName, StringComparer.Ordinal);
hashCodeCombiner.Add (Имя контроллера, StringComparer.Ordinal);
hashCodeCombiner.Add (AreaName, StringComparer.Ordinal);

если (ViewLocationExpanderValues! = ноль)
{
foreach (элемент var в ViewLocationExpanderValues)
{
hashCodeCombiner.Add (item.Key, StringComparer.Ordinal);
hashCodeCombiner.Add (item.Value, StringComparer.Ordinal);
}
}

return hashCodeCombiner;
`` ''

Мы обсудили это еще немного, и вы нас убедили :-)

🎉

Представление нового пространства имен кажется странным; Хотя System.Numerics может работать.

Если мы решим не добавлять новое пространство имен, следует отметить, что любой код, имеющий класс с именем Hash и директиву using System.Numerics , не будет компилироваться из-за ошибки неоднозначного типа.

Должны ли мы предоставить шаблон построения без выделения памяти для объединения неизвестного количества хэш-кодов?

Звучит как отличная идея. В качестве пары начальных предложений, возможно, нам следует назвать его HashBuilder (а-ля StringBuilder ) и использовать return this после каждого метода Add чтобы упростить задачу. чтобы добавить хеши, например:

public override int GetHashCode()
{
    return HashBuilder.Create(_field1)
        .Add(_field2)
        .Add(_field3)
        .ToHash();
}

@jamesqo, пожалуйста, обновите предложение вверху, когда будет достигнут консенсус по теме. Затем мы можем провести окончательную проверку. Назначаю вам, пока вы будете управлять дизайном ;-)

Если мы решим не добавлять новое пространство имен, следует отметить, что любой код, имеющий класс с именем Hash и директиву using System.Numerics , не будет компилироваться из-за ошибки неоднозначного типа.

Зависит от реального сценария. Во многих случаях компилятор предпочтет ваш тип, поскольку определенная иерархия пространства имен единицы компиляции проходит до рассмотрения использования директив.

Но даже в этом случае: добавление API может стать серьезным изменением. Однако избегать этого непрактично, если мы хотим добиться прогресса 😄 Обычно мы стараемся избегать конфликтов, например, используя не слишком общие имена. Например, я не думаю, что нам следует называть тип Hash . Я думаю, что HashCode , наверное, было бы лучше.

В качестве пары начальных предложений, возможно, нам следует назвать его HashBuilder

В первом приближении я думал объединить статику и конструктор в один тип, например:

`` С #
пространство имен System.Collections.Generic
{
общедоступная структура HashCode
{
общедоступный статический int Combine (int hash1, int hash2);
общедоступный статический int Combine (int hash1, int hash2, int hash3);
общедоступный статический int Combine (int hash1, int hash2, int hash3, int hash4);
public static int Combine (int hash1, int hash2, int hash3, int hash4, int hash5);
public static int Combine (int hash1, int hash2, int hash3, int hash4, int hash5, int hash6);

    public static long Combine(long hash1, long hash2);
    public static long Combine(long hash1, long hash2, long hash3);
    public static long Combine(long hash1, long hash2, long hash3, long hash4);
    public static long Combine(long hash1, long hash2, long hash3, long hash4, long hash5);
    public static long Combine(long hash1, long hash2, long hash3, long hash4, long hash5, longhash6);

    public static int CombineHashCodes<T1, T2>(T1 o1, T2 o2);
    public static int CombineHashCodes<T1, T2, T3>(T1 o1, T2 o2, T3 o3);
    public static int CombineHashCodes<T1, T2, T3, T4>(T1 o1, T2 o2, T3 o3, T4 o4);
    public static int CombineHashCodes<T1, T2, T3, T4, T5>(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5);
    public static int CombineHashCodes<T1, T2, T3, T4, T5, T6>(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5, T6 o6);

    public void Combine(int hashCode);
    public void Combine(long hashCode);
    public void Combine<T>(T obj);
    public void Combine(string text, StringComparison comparison);

    public int Value { get; }
}

}

This allows for code like this:

``` C#
return HashCode.Combine(value1, value2);

а также:

`` С #
var hashCode = новый HashCode ();
hashCode.Combine (IsMainPage? 1: 0);
hashCode.Combine (ViewName, StringComparer.Ordinal);
hashCode.Combine (имя контроллера, StringComparer.Ordinal);
hashCode.Combine (Имя области, StringComparer.Ordinal);

если (ViewLocationExpanderValues! = ноль)
{
foreach (элемент var в ViewLocationExpanderValues)
{
hashCode.Combine (item.Key, StringComparer.Ordinal);
hashCode.Combine (item.Value, StringComparer.Ordinal);
}
}

return hashCode.Value;
`` ''

Мысли?

Мне нравится идея @jamesqo связанных вызовов (возврат this из методов экземпляра Combine ).

Я бы даже полностью удалил статические методы и оставил только методы экземпляра ...

Combine(long hashCode) просто сбрасывается до int . Мы действительно этого хотим?
Каков вариант использования перегрузок long в первую очередь?

@karelz Пожалуйста, не удаляйте их, структуры не бесплатны. Хэши можно использовать в очень горячих путях, вы, конечно, не хотите тратить зря инструкции, когда статический метод будет по существу бесплатным. Посмотрите на анализ кода, где я показал реальное влияние окружающей структуры.

Мы использовали статический класс Hashing чтобы избежать конфликтов имен, и код выглядит хорошо.

@redknightlois Интересно, стоит ли ожидать такой же «плохой» код и в случае неуниверсальной структуры с одним полем int.
Если это все еще «плохой» ассемблерный код, мне интересно, можем ли мы улучшить JIT, чтобы лучше выполнять здесь оптимизацию. Добавление API только для того, чтобы сохранить пару инструкций, должно быть нашим последним средством ИМО.

@redknightlois Любопытно, генерирует ли JIT худший код, если структура (в данном случае HashCode ) помещается в регистр? Это будет только int большой.

Кроме того, в последнее время я видел много запросов на вытягивание в coreclr для улучшения кода, сгенерированного вокруг структур, и похоже, что dotnet / coreclr # 8057 включит эту оптимизацию. Возможно, код, который генерирует JIT, станет лучше после этого изменения?

изменить: я вижу, что @karelz уже упоминал здесь мои баллы.

@karelz , я согласен с вами - предполагая, что JIT генерирует достойный код для структуры размером int (что, я считаю, да, например, ImmutableArray не имеет накладных расходов), тогда статические перегрузки избыточны и могут быть удалены.

@terrajobst У меня есть еще несколько идей:

  • Думаю, мы можем немного объединить ваши и мои идеи. HashCode кажется хорошим именем; это не обязательно должна быть изменяемая структура, соответствующая шаблону построителя. Вместо этого это может быть неизменяемая оболочка вокруг int , и каждая операция Combine может возвращать новое значение HashCode . Например
public struct HashCode
{
    private readonly int _hash;

    public HashCode Combine(int hash) => return new HashCode(CombineCore(_hash, hash));

    public HashCode Combine<T>(T item) => Combine(EqualityComparer<T>.Default.GetHashCode(item));
}

// Usage
HashCode combined = new HashCode(_field1)
    .Combine(_field2)
    .Combine(_field3);
  • У нас должен быть просто неявный оператор для преобразования в int чтобы людям не нужно было иметь последний вызов .Value .
  • Re Combine , это лучшее имя? Это звучит более наглядно, но Add короче и легче набирается. ( Mix - еще одна альтернатива, но набирать ее немного неудобно.)

    • public void Combine(string text, StringComparison comparison) : Я не думаю, что это действительно относится к одному и тому же типу, поскольку это не связано со строками. Кроме того, достаточно просто написать StringComparer.XXX.GetHashCode(str) для тех редких случаев, когда вам это нужно.

    • Мы должны удалить длинные перегрузки из этого типа и создать отдельный тип HashCode для long. Что-то вроде Int64HashCode или LongHashCode .

Я сделал небольшой образец реализации вещей на TryRoslyn: http://tinyurl.com/zej9yux

К счастью, это легко проверить. И хорошая новость в том, что он и так работает правильно 👍

image

У нас должен быть просто неявный оператор для преобразования в int, чтобы людям не приходилось иметь последний вызов .Value.

Вероятно, код не такой простой, неявное преобразование немного его очистит. Мне все еще нравится идея иметь интерфейс с несколькими параметрами.

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryHashCombiner(int h1, int h2, int h3, int h4)
        {
            var h = new HashCode(h1).Combine(h2).Combine(h3).Combine(h4);
            return h.Value;
        }

Re Combine, это лучшее имя? Это звучит более наглядно, но «Добавить» короче и легче набирать. (Другой вариант - Mix, но набирать его немного больно.)

Combine - это фактическое имя, которое используется в хеш-сообществе afaik. И это как бы дает вам четкое представление о том, что он делает.

@jamesqo Есть много хеш-функций, нам пришлось реализовать очень быстрые версии для RavenDB, от 32-битных, 64-битных до 128-битных (и мы используем каждую из них для разных целей).

Мы можем подумать об этом с помощью некоторого расширяемого механизма, подобного этому:

        internal interface IHashCode<T> where T : struct
        {
            T Combine(T h1, T h2);
        }

        internal struct RotateHashCode : IHashCode<int>, IHashCode<long>
        {
            long IHashCode<long>.Combine(long h1, long h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                ulong shift5 = ((ulong)h1 << 5) | ((ulong)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }

            int IHashCode<int>.Combine(int h1, int h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                uint shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }
        }

        internal struct HashCodeCombiner<T, W> where T : struct, IHashCode<W>
                                               where W : struct
        {
            private static T hasher;
            public W Value;

            static HashCodeCombiner()
            {
                hasher = new T();
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public HashCodeCombiner(W seed)
            {
                this.Value = seed;
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public HashCodeCombiner<T,W> Combine( W h1 )
            {
                Value = hasher.Combine(this.Value, h1);
                return this;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryHashCombinerT(int h1, int h2, int h3, int h4)
        {
            var h = new HashCodeCombiner<RotateHashCode, int>(h1).Combine(h2).Combine(h3).Combine(h4);
            return h.Value;
        }

Я не знаю, почему JIT создает для этого очень раздражающий код пролога. Этого не должно быть, поэтому его, вероятно, можно оптимизировать, мы должны попросить об этом разработчиков JIT. Но в остальном вы можете реализовать столько разных комбайнеров, сколько захотите, не тратя впустую ни одной инструкции. Сказав это, этот метод, вероятно, более полезен для реальных хеш-функций, чем для комбайнеров. Копия @CarolEidt @AndyAyersMS

РЕДАКТИРОВАТЬ: продумать здесь вслух общий механизм для объединения криптографических и некриптографических хэш-функций в рамках единой концепции хеширования.

@jamesqo

это не обязательно должна быть изменяемая структура, следующая за шаблоном построителя

О да. В таком случае меня устраивает этот шаблон. Мне вообще не нравится схема возврата экземпляров, если операция имела побочный эффект. Особенно плохо, если API следует неизменному шаблону WithXxx . Однако в этом случае шаблон, по сути, представляет собой неизменяемую структуру данных, поэтому этот шаблон будет работать нормально.

Думаю, мы можем немного объединить ваши и мои идеи.

👍, так что насчет:

`` С #
общедоступная структура HashCode
{
общедоступный статический HashCode Create(T obj);

[Pure] public HashCode Combine(int hashCode);
[Pure] public HashCode Combine(long hashCode);
[Pure] public HashCode Combine<T>(T obj);
[Pure] public HashCode Combine(string text, StringComparison comparison);

public int Value { get; }

public static implicit operator int(HashCode hashCode);

}

This allows for code like this:

``` C#
public override int GetHashCode()
{
    return HashCode.Create(value1).Combine(value2);
}

а также это:

`` С #
var hashCode = новый HashCode ()
.Combine (IsMainPage? 1: 0)
.Combine (ViewName, StringComparer.Ordinal)
.Combine (имя_контроллера, StringComparer.Ordinal)
.Combine (Имя области, StringComparer.Ordinal);

если (ViewLocationExpanderValues! = ноль)
{
foreach (элемент var в ViewLocationExpanderValues)
{
hashCode = hashCode.Combine (item.Key, StringComparer.Ordinal);
hashCode = hashCode.Combine (item.Value, StringComparer.Ordinal);
}
}

return hashCode.Value;
`` ''

@terrajobst Мысли:

  1. Заводской метод Create<T> следует удалить. В противном случае было бы два способа написать одно и то же: HashCode.Create(_val) или new HashCode().Combine(_val) . Кроме того, разные имена для Create / Combine не будут удобны для различий, поскольку если вы добавите новое первое поле, вам придется изменить 2 строки.
  2. Я не думаю, что сюда относится перегрузка, принимающая строку / StringComparison; HashCode имеет ничего общего со строками. Вместо этого, может быть, нам следует добавить GetHashCode(StringComparison) api в строку? (Также все это порядковые сравнения, что является поведением по умолчанию для string.GetHashCode .)
  3. Какой смысл иметь Value , если уже существует неявный оператор для преобразования в int ? Опять же, это привело бы к тому, что разные люди писали бы разные вещи.
  4. Нам нужно переместить перегрузку long в новый тип. HashCode будет иметь ширину только 32 бита; это не может поместиться долго.
  5. Давайте добавим несколько перегрузок, принимающих беззнаковые типы, поскольку они более распространены при хешировании.

Вот предлагаемый мной API:

public struct HashCode
{
    public HashCode Combine(int hash);
    public HashCode Combine(uint hash);
    public HashCode Combine<T>(T obj);

    public static implicit operator int(HashCode hashCode);
    public static implicit operator uint(HashCode hashCode);
}

public struct Int64HashCode
{
    public Int64HashCode Combine(long hash);
    public Int64HashCode Combine(ulong hash);

    public static implicit operator long(Int64HashCode hashCode);
    public static implicit operator ulong(Int64HashCode hashCode);
}

Используя только эти методы, пример из ASP.NET все еще можно записать как

var hashCode = new HashCode()
    .Combine(IsMainPage ? 1 : 0)
    .Combine(ViewName)
    .Combine(ControllerName)
    .Combine(AreaName);

if (ViewLocationExpanderValues != null)
{
    foreach (var item in ViewLocationExpanderValues)
    {
        hashCode = hashCode.Combine(item.Key);
        hashCode = hashCode.Combine(item.Value);
    }
}

return hashCode;

@jamesqo

Какой смысл иметь Value , если уже существует неявный оператор для преобразования в int ? Опять же, это привело бы к тому, что разные люди писали бы разные вещи.

В Руководстве по проектированию фреймворка для перегрузок операторов говорится:

РАССМАТРИВАЙТЕ предоставление методов с понятными именами, соответствующими каждому перегруженному оператору.

Многие языки не поддерживают перегрузку оператора. По этой причине рекомендуется, чтобы типы с операторами перегрузки включали вторичный метод с соответствующим доменным именем, обеспечивающим эквивалентную функциональность.

В частности, F # - один из языков, в котором неудобно вызывать операторы неявного преобразования.


Кроме того, я не думаю, что иметь только один способ делать что-то так важно. На мой взгляд, важнее сделать API удобным. Если я просто хочу объединить хэш-коды нескольких значений, я думаю, что HashCode.CombineHashCodes(value1, value2, value3) проще, короче и понятнее, чем new HashCode().Combine(value1).Combine(value2).Combine(value3) .

API метода экземпляра по-прежнему полезен для более сложных случаев, но я думаю, что в более распространенном случае должен быть более простой API статических методов.

@svick , ваше Value .

Я не думаю, что важно иметь только один способ делать что-то.

Это важно. Если кто-то делает это одним способом и читает код человека, который делает это по-другому, то ему / ей придется искать в Google то, что делает другой способ.

Если я просто хочу объединить хэш-коды нескольких значений, я думаю, что HashCode.CombineHashCodes (value1, value2, value3) проще, короче и понятнее, чем new HashCode (). Combine (value1) .Combine (value2) .Combine ( значение3).

  • Проблема со статическим методом заключается в том, что, поскольку не будет перегрузки params int[] , нам придется добавлять перегрузки для каждой различной арности, что гораздо меньше затрат. Гораздо лучше иметь один метод, охватывающий все варианты использования.
  • Вторую форму будет легко понять, если вы посмотрите ее один или два раза. Фактически, вы можете утверждать, что это более читабельно, поскольку его легче объединить по вертикали (и, таким образом, минимизировать различия при добавлении / удалении поля):
public override int GetHashCode()
{
    return new HashCode()
        .Combine(_field1)
        .Combine(_field2)
        .Combine(_field3)
        .Combine(_field4);
}

[@svick] Я не думаю, что важно иметь только один способ делать что-то.

Я думаю, что важно свести к минимуму количество способов сделать то же самое, потому что это позволяет избежать путаницы. В то же время наша цель - не быть на 100% свободным с дублированием, если это помогает реализовать другие цели, такие как обнаруживаемость, удобство, производительность или удобочитаемость. В общем, наша цель - минимизировать концепции, а не API. Например, несколько перегрузок менее проблематичны, чем использование нескольких разных методов с непересекающейся терминологией.

Причина, по которой я добавил фабричный метод, - прояснить, как получить начальный хеш-код. Создание пустой структуры, за которой следует Combine , не кажется интуитивно понятным. Логично было бы добавить .ctor, но чтобы избежать упаковки, он должен быть универсальным, чего нельзя сделать с .ctor. Обобщенный фабричный метод - следующая лучшая вещь.

Приятным побочным эффектом является то, что это очень похоже на то, как неизменяемые структуры данных выглядят во фреймворке. А в дизайне API мы сильно отдаем предпочтение согласованности почти всему остальному.

[@svick] Если я просто хочу объединить хеш-коды с несколькими значениями, я думаю, что HashCode.CombineHashCodes (value1, value2, value3) проще, короче и понятнее, чем new HashCode (). Combine (value1) .Combine (value2) ) .Комбинировать (значение3).

Я согласен с @jamesqo : что мне нравится в шаблоне компоновщика, так это то, что он масштабируется до произвольного количества аргументов с минимальными потерями производительности (если таковые имеются, в зависимости от того, насколько хорош наш инлайнер).

[@jamesqo] Я не думаю, что перегрузка, принимающая строку / StringComparison, здесь; HashCode не имеет ничего общего со строками

Честная оценка. Я добавил его, потому что на него есть ссылка в коде @Eilon . По своему опыту могу сказать, что струны очень распространены. С другой стороны, я не уверен, что указание сравнения. А пока оставим это.

[@jamesqo] Мы должны переместить длинную перегрузку в новый тип. HashCode будет иметь ширину только 32 бита; это не может поместиться долго.

Неплохо подмечено. Нужна ли нам вообще версия long ? Я оставил это только потому, что это было упомянуто выше, и я особо не думал об этом.

Теперь мне кажется, что нам следует оставить только 32-битную версию, потому что это то, о чем идет речь в .NET GetHashCode() . В этом ключе я даже не уверен, что нам следует добавлять версию uint . Если вы используете хеширование за пределами этой области, я думаю, что нормально указывать людям на более общие алгоритмы хеширования, которые есть в System.Security.Cryptography .

`` С #
общедоступная структура HashCode
{
общедоступный статический HashCode Create(T obj);

[Pure] public HashCode Combine(int hashCode);
[Pure] public HashCode Combine<T>(T obj);

public int Value { get; }

public static implicit operator int(HashCode hashCode);

}
`` ''

Теперь мне кажется, что нам следует оставить только 32-битную версию, потому что для этого и предназначена .NET GetHashCode (). В этом смысле я даже не уверен, что нам следует добавлять версию uint. Если вы используете хеширование за пределами этой области, я думаю, что можно указать людям на более общие алгоритмы хеширования, которые есть в System.Security.Cryptography.

@terrajobst Есть очень разные типы алгоритмов хеширования, настоящий зоопарк. Фактически, вероятно, 70% не являются криптографическими по своей природе. И, вероятно, более половины из них предназначены для работы с 64+ битами (общая цель - 128/256). Я уверен, что фреймворк решил использовать 32 бита (я там не был), потому что в то время x86 все еще был огромным потребителем, а хеши использовались повсюду, поэтому производительность на меньшем оборудовании была первостепенной.

Также, чтобы быть строгим, большинство хеш-функций действительно определены в домене uint , а не в int потому что правила сдвига разные. Фактически, если вы проверите код, который я опубликовал ранее, из-за этого int немедленно преобразуется в uint (и используется оптимизация ror/rol ). Укажите на случай, если мы хотим быть строгими, единственным хешем должен быть uint , это можно рассматривать как оплошность, что фреймворк возвращает int под этим светом.

Ограничить это до int не лучше, чем то, что мы имеем сегодня. Если бы это был мой звонок, я бы посоветовал команде дизайнеров изучить, как мы можем разместить поддержку 128 и 256 вариантов и различные хэш-функции (даже если бы мы подбросили альтернативу «не заставляй меня думать» под ваши отпечатки пальцев).

Проблемы, вызванные чрезмерным упрощением, иногда бывают хуже, чем проблемы дизайна, возникающие, когда приходится иметь дело со сложными вещами. Упрощение функциональности до такой степени, потому что разработчики воспринимают not being able to deal with having multiple options может легко привести к текущему состоянию SIMD. Большинство разработчиков, заботящихся о производительности, не могут его использовать, и все остальные тоже не будут использовать его, потому что большинство из них не имеют дело с приложениями, чувствительными к производительности, которые в любом случае имеют такие точные целевые показатели пропускной способности.

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

image

Кроме того, как только вам нужно иметь дело с более чем 75000 элементов, у вас есть 50% шансов на столкновение, и это плохо в большинстве сценариев (и это при условии, что у вас есть хорошо спроектированная хеш-функция). Вот почему 64 и 128 бит используются вне границ структур времени выполнения.

С дизайном, застрявшим на int мы только покрываем проблемы, вызванные отсутствием газеты Monday в 2000 году (так что теперь каждый пишет свое плохое хеширование самостоятельно), но мы не будем продвигаться даже на шаг к состоянию искусство тоже.

Это мои 2 цента за обсуждение.

@redknightlois , я думаю, мы понимаем ограничения int-хэшей. Но я согласен с @terrajobst : эта функция должна касаться API для вычисления хэшей с целью их возврата из переопределений Object.GetHashCode. Вдобавок у нас может быть отдельная библиотека для более современного хеширования, но я бы сказал, что это должно быть отдельное обсуждение, поскольку оно должно включать решение, что делать с Object.GetHashCode и всеми существующими структурами данных хеширования.

Если вы не считаете, что по-прежнему выгодно выполнить 128-битное объединение хешей, а затем преобразовать их в int, чтобы результат можно было вернуть из GetHahsCode.

@KrzysztofCwalina Я согласен, что это два разных подхода. Один - исправить проблему, возникшую в 2000 году; другой - решить общую проблему хеширования. Если мы все согласимся, что это решение для первого, обсуждение окончено. Тем не менее, что касается обсуждения дизайна для вехи «Будущее», у меня есть ощущение, что оно не будет выполнено, в основном потому, что то, что мы здесь будем делать, повлияет на дальнейшее обсуждение. Ошибки здесь окажут влияние.

@redknightlois , я бы предложил следующее: давайте спроектируем API так, как будто нам не нужно беспокоиться о будущем. Затем давайте обсудим, какие варианты дизайна, по нашему мнению, вызовут проблемы для будущих API. Кроме того, мы могли бы добавить API c2000 в corfx и параллельно попытаться поэкспериментировать с будущими API в corfxlab, которые должны выявить любые проблемы, связанные с такими дополнениями, если мы когда-нибудь захотим их сделать.

@redknightlois

Ошибки здесь окажут влияние.

Я думаю, что если в будущем мы захотим поддерживать более сложные сценарии, мы можем просто сделать это отдельным типом от HashCode . Решения здесь не должны влиять на эти дела.

Я создал другую проблему, чтобы начать ее решать.

@redknightlois : +1 :. Кстати, вы ответили, прежде чем я смог отредактировать свой комментарий, но я действительно опробовал вашу идею (см. Выше) о том, чтобы хеш работал с любым типом (int, long, decimal и т. Д.) И заключил основную логику хеширования в структуру: https://github.com/jamesqo/HashApi (пример использования был здесь ). Но наличие двух параметров универсального типа оказалось слишком сложным, и вывод типа компилятора не работал, когда я пытался использовать API. Так что да, сейчас неплохо было бы выделить более сложное хеширование в отдельный вопрос.

@terrajobst API кажется почти готовым, но есть еще 1 или 2 вещи, которые я хотел бы изменить.

  • Мне изначально не нужен статический фабричный метод, поскольку HashCode.Create(x) имеет тот же эффект, что и new HashCode().Combine(x) . Но я передумал, так как это означает 1 дополнительный хэш. Вместо этого, почему бы нам не переименовать Create в Combine ? Кажется раздражающим, что нужно вводить одно для первого поля, а другое - для второго.
  • Я думаю, нам нужно реализовать HashCode IEquatable<HashCode> и реализовать некоторые операторы равенства. Не стесняйтесь сообщить мне, если у вас есть возражения.

(Надеюсь) окончательное предложение:

public struct HashCode : IEquatable<HashCode>
{
    public static HashCode Combine(int hash);
    public static HashCode Combine<T>(T obj);

    public HashCode Combine(int hash);
    public HashCode Combine<T>(T obj);

    public int Value { get; }

    public static implicit operator int(HashCode hashCode);

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

    public override bool Equals(object obj);
    public override bool Equals(HashCode other);
    public override int GetHashCode();
}

// Usage:

public override int GetHashCode()
{
    return HashCode
        .Combine(_field1)
        .Combine(_field2)
        .Combine(_field3)
        .Combine(_field4);
}

@terrajobst сказал:

Честная оценка. Я добавил его, потому что на него есть ссылка в коде @Eilon . По своему опыту могу сказать, что струны очень распространены. С другой стороны, я не уверен, что указание сравнения. А пока оставим это.

На самом деле это очень важно: создание хэшей для строк часто включает в себя учет цели этой строки, которая включает как ее язык, так и ее чувствительность к регистру. StringComparer - это не сравнение как таковое, а скорее предоставление конкретных реализаций GetHashCode, учитывающих региональные особенности / регистр.

Без этого API вам пришлось бы делать что-то странное, например:

HashCode.Combine(str1.ToLowerInvariant()).Combine(str2.ToLowerInvariant())

И это переполнено распределениями, следует плохим моделям чувствительности к культуре и т. Д.

@Eilon в таком случае я ожидал бы, что код должен явно вызывать string.GetHashCode(StringComparison comparison) который учитывает культуру / регистр, и передавать результат как int в Combine .

c# HashCode.Combine(str1.GetHashCode(StringComparer.Ordinal)).Combine(...)

@Eilon , вы можете просто использовать StringComparer.InvariantCultureIgnoreCase.GetHashCode.

Они, безусловно, лучше с точки зрения распределения, но на эти вызовы не очень приятно смотреть ... Мы используем повсюду в ASP.NET, где хэши должны включать строки с учетом языка и регистра.

Достаточно честно, объединив все сказанное выше, как насчет этой формы:

`` С #
пространство имен System.Collections.Generic
{
общедоступная структура HashCode: IEquatable
{
общедоступный статический HashCode Combine (int hash);
общедоступный статический HashCode Combine(T obj);
общедоступный статический HashCode Combine (текст строки, сравнение StringComparison);

    public HashCode Combine(int hash);
    public HashCode Combine<T>(T obj);
    public HashCode Combine(string text, StringComparison comparison);

    public int Value { get; }

    public static implicit operator int(HashCode hashCode);

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

    public override bool Equals(object obj);
    public override bool Equals(HashCode other);
    public override int GetHashCode();
}

}

// Использование:

публичное переопределение int GetHashCode ()
{
вернуть HashCode.Combine (_field1)
.Combine (_field2)
.Combine (_field3)
.Combine (_field4);
}
`` ''

отправим его! :-)

@terrajobst _Удерживайте --_ нельзя ли Combine(string, StringComparison) просто реализовать как метод расширения?

public static class HashCodeExtensions
{
    public static HashCode Combine(this HashCode hashCode, string text, StringComparison comparison)
    {
        switch (comparison)
        {
            case StringComparison.Ordinal:
                return HashCode.Combine(StringComparer.Ordinal.GetHashCode(text));
            case StringComparison.OrdinalIgnoreCase:
                ...
        }
    }
}

Я бы предпочел, чтобы это был метод расширения, а не часть сигнатуры типа. Однако, если вы или @Elion абсолютно уверены, что это должен быть встроенный метод, я не буду блокировать это предложение.

( edit: Также System.Numerics , вероятно, является лучшим пространством имен, если у нас сегодня нет связанных с хешем типов в Collections.Generic, о которых я не знаю.)

LGTM. Я бы пошел на продление.

Да, это может быть метод расширения, но какую проблему он решает?

@terrajobst

Да, это может быть метод расширения, но какую проблему он решает?

Я предлагал в коде ASP.NET. Если это характерно для их случая использования, то это нормально, но это может быть неверно для других библиотек / приложений. Если позже выяснится, что это достаточно часто, мы всегда можем переоценить и решить добавить это в отдельное предложение.

В любом случае, это ядро. После определения он в любом случае будет частью подписи. Удалите комментарий. Это нормально, как есть.

Использование методов расширения полезно в случаях, когда:

  1. это существующий тип, который мы хотели бы расширить без необходимости отправлять обновление для самого типа
  2. решить проблемы с наложением
  3. отделять супер-распространенные API-интерфейсы от гораздо менее используемых API-интерфейсов.

Я не думаю, что здесь применимы (1) или (2). (3) поможет, только если мы переместим код в другую сборку, отличную от HashCode или если мы переместим его в другое пространство имен. Я бы сказал, что струны достаточно распространены, и это того не стоит. Фактически, я бы даже сказал, что они настолько распространены, что рассматривать их как первоклассные имеет больше смысла, чем пытаться искусственно разделить их по типу расширения.

@terrajobst , для ясности я предлагал полностью отказаться от string API и оставить ASP.NET написать собственный метод расширения для строк.

Я бы сказал, что струны достаточно распространены, и это того не стоит. Фактически, я бы даже сказал, что они настолько распространены, что рассматривать их как первоклассные имеет больше смысла, чем пытаться искусственно разделить их по типу расширения.

Да, но насколько часто кто-то хочет получить неординарный хеш-код строки, что является единственным сценарием, о котором не заботится существующая перегрузка Combine<T> ? (например, тот, кто называет StringComparer.CurrentCulture.GetHashCode в своих переопределениях?) Я могу ошибаться, но я не видел многих.

Приносим извинения за возражение по этому поводу; просто после добавления API пути назад уже не будет.

да, но насколько часто кто-то хочет получить неординарный хеш-код строки

Я могу быть предвзятым, но неизменность регистра довольно популярна. Конечно, не многие (если таковые имеются) заботятся о хэш-кодах, связанных с культурой, но хэш-коды, которые игнорируют регистр, я могу полностью видеть - и это похоже на то, что после @Eilon (то есть StringComparison.OrdinalIgnoreCase ).

Приносим извинения за возражение по этому поводу; просто после добавления API пути назад уже не будет.

Без шуток 😈 Согласен, но даже если API не используется, он полезен и не причиняет никакого вреда.

@terrajobst Хорошо, давайте добавим это: +1: Последняя проблема: я упомянул об этом выше, но можем ли мы сделать пространство имен Numerics, а не Collections.Generic? Если бы в будущем мы добавили больше типов, связанных с хешированием, как предлагает @redknightlois , я думаю, что они будут неправильно называться в Коллекциях.

Мне это нравится. 🍔

Я не думаю, что хеширование концептуально относится к коллекциям. А что насчет System.Runtime?

Я собирался предложить то же самое или даже System. Это тоже не цифры.

@karelz , System.Runtime может работать. Система @redknightlois была бы удобна, поскольку, скорее всего, вы уже импортировали это пространство имен. Не знаю, подходит ли это (опять же, если будет добавлено больше типов хеширования).

Мы не должны помещать его в System.Runtime как это для эзотерических и довольно специализированных случаев. Я разговаривал с @KrzysztofCwalina, и мы оба думаем, что это одно из двух:

  • System
  • System.Collections.*

Мы оба склоняемся к System .

Если нам нужно обоснование того, зачем использовать System я могу попробовать оправдание. Мы создали HashCode чтобы помочь в реализации object.GetHashCode() , кажется уместным, что оба будут совместно использовать пространство имен.

@terrajobst Тогда я думаю, что System должно быть пространством имен. Давайте: shipit:

Обновлена ​​спецификация API в описании.

[@redknightlois] Если нам нужно обоснование того, зачем покупать System я могу попробовать оправдание. Мы создали HashCode чтобы помочь в реализации object.GetHashCode() , кажется уместным, что оба будут совместно использовать пространство имен.

Это было обоснование, которое мы с @KrzysztofCwalina использовали. Продал!

@jamesqo

Полагаю, вы хотите предоставить PR и реализацию?

@terrajobst Да, определенно. Спасибо, что нашли время, чтобы просмотреть это!

Да, безусловно.

Сладкий. В таком случае я оставлю это вам. Это хорошо с тобой, @karelz?

Спасибо, что нашли время, чтобы просмотреть это!

Спасибо, что нашли время поработать с нами над формой API. Перемещение взад и вперед может быть болезненным процессом. Благодарим Вас за терпение!

И я с нетерпением жду возможности удалить реализацию ASP.NET Core и использовать ее вместо этого 😄

общедоступный статический HashCode Combine (текст строки, сравнение StringComparison);
общедоступный HashCode Combine (текст строки, сравнение StringComparison);

Nit: методы String которые принимают StringComparison (например, Equals , Compare , StartsWith , EndsWith и т. Д. .) используйте comparisonType в качестве имени параметра, а не comparison . Следует ли здесь называть параметр comparisonType чтобы он был согласован?

@justinvp , это больше похоже на недостаток именования в методах String; Type является избыточным. Я не думаю, что мы должны делать имена параметров в новых API более подробными только для того, чтобы «следовать прецеденту» со старыми.

В качестве другой точки данных xUnit также решил использовать comparisonType .

@justinvp Вы меня убедили. Теперь, когда я думаю об этом интуитивно, «нечувствительность к регистру» или «зависящая от культуры» - это «тип» сравнения. Я поменяю имя.

Я согласен с формой этого, но в отношении StringComparison возможная альтернатива:

Не включайте:

`` С #
общедоступный статический HashCode Combine (текст строки, сравнение StringComparison);
общедоступный HashCode Combine (текст строки, сравнение StringComparison);

Instead, add a method:

``` C#
public class StringComparer
{
    public static StringComparer FromComparison(StringComparison comparison);
    ...
}

Тогда вместо того, чтобы писать:

`` С #
публичное переопределение int GetHashCode ()
{
вернуть HashCode.Combine (_field1)
.Combine (_field2)
.Combine (_field3)
.Combine (_field4, _comparison);
}

you write:

``` C#
public override int GetHashCode()
{
    return HashCode.Combine(_field1)
                   .Combine(_field2)
                   .Combine(_field3)
                   .Combine(StringComparer.FromComparison(_comparison).GetHashCode(_field4));
}

Да, он немного длиннее, но он решает ту же проблему без использования двух специализированных методов в HashCode (который мы только что повысили до System), и вы получаете статический вспомогательный метод, который можно использовать в других, не связанных между собой ситуациях. Он также сохраняет его аналогично тому, как вы бы его использовали, если у вас уже есть StringComparer (поскольку мы не говорим о перегрузках компаратора):

C# public override int GetHashCode() { return HashCode.Combine(_field1) .Combine(_field2) .Combine(_field3) .Combine(_comparer.GetHashCode(_field4)); }

@stephentoub , FromComparison звучит как хорошая идея. Я фактически предложил вверх в потоке добавить string.GetHashCode(StringComparison) api, что делает ваш пример еще проще (при условии, что строка не равна нулю):

public override int GetHashCode()
{
    return HashCode.Combine(_field1)
                   .Combine(_field2)
                   .Combine(_field3)
                   .Combine(_field4.GetHashCode(_comparison));
}

@Elion сказал, что добавил слишком много звонков.

(изменить: сделал предложение для вашего api.)

Мне также не нравится добавлять 2 специализированных метода в HashCode для строки.
@Eilon, вы упомянули, что шаблон используется в самом ASP.NET Core. Как вы думаете, сколько сторонние разработчики будут его использовать?

@jamesqo спасибо за разработку дизайна! Как сказал @terrajobst , мы ценим вашу помощь и терпение. На итерацию фундаментальных небольших API-интерфейсов иногда может потребоваться время :).

Давайте посмотрим, где мы окажемся с этим последним отзывом API, а затем мы сможем продолжить реализацию.

Должен быть:

C# public static HashCode Combine<T>(T obj, IEqualityComparer<T> cmp);

?

(Приносим извинения, если это уже было отклонено, и мне это здесь не хватает).

@stephentoub сказал:

написать:

c# public override int GetHashCode() { return HashCode.Combine(_field1) .Combine(_field2) .Combine(_field3) .Combine(StringComparer.FromComparison(_comparison).GetHashCode(_field4)); }

Да, он немного длиннее, но он решает ту же проблему без использования двух специализированных методов в HashCode (который мы только что повысили до System), и вы получаете статический вспомогательный метод, который можно использовать в других, не связанных между собой ситуациях. Он также сохраняет его аналогично тому, как вы бы его использовали, если у вас уже есть StringComparer (поскольку мы не говорим о перегрузках компаратора):


Что ж, это не просто немного длиннее, это просто супер длиннее, и у него нулевая обнаруживаемость.

Какое сопротивление добавлению этого метода? Если он полезен, может быть четко реализован правильно, не имеет двусмысленности в том, что он делает, почему бы не добавить его?

Наличие дополнительного статического вспомогательного метода / метода преобразования - это нормально - хотя я не уверен, что буду его использовать, - но почему за счет удобных методов?

почему за счет удобных методов?

Потому что мне непонятно, действительно нужны удобные методы. Я понимаю, что ASP.NET делает это в разных местах. Сколько мест? И в скольких из этих мест на самом деле у вас есть переменная StringComparison, а не известное значение? В этом случае вам даже не нужен упомянутый мной помощник, и вы можете просто:

`` С #
.Combine (StringComparer.InvariantCulture.GetHashCode (_field4))

which in no way seems onerous to me or any more undiscoverable than knowing about StringComparison and doing:

``` C#
.Combine(_field4, StringComparison.InvariantCulture);

и на самом деле быстрее, поскольку нам не нужно выполнять ветвление внутри Combine, чтобы делать то же самое, что мог бы написать разработчик. Неужели дополнительный код доставляет столько неудобств, что для этого случая стоит добавить специальные перегрузки? Почему не перегружать StringComparer? Почему бы не перегружать EqualityComparer? Почему не перегрузки, требующие Func<T, int> ? В какой-то момент вы проводите черту и говорите: «ценность, которую предоставляет эта перегрузка, просто не стоит того», потому что все, что мы добавляем, имеет свою цену, будь то стоимость обслуживания, стоимость размера кода или стоимость всего , и если разработчику действительно нужен этот случай, разработчику очень мало дополнительного кода, чтобы справиться с меньшим количеством специализированных случаев. Итак, я предположил, что, возможно, правильное место для проведения линии - это до этих перегрузок, а не после (но, как я сказал в начале своего предыдущего ответа: «Я в порядке с формой этого», и предлагал альтернативу) .

Вот мой поиск: https://github.com/search?p=2&q=user%3Aaspnet+hashcodecombiner&type=Code&utf8=%E2%9C%93

Из ~ 100 совпадений, даже с первых нескольких страниц, почти в каждом варианте использования есть строки, а в некоторых случаях используются разные виды сравнения строк:

  1. Порядковый номер: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperAttributer.Descriptr.cs
  2. Порядковый + IgnoreCase: https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpersquired#AspNetCore.Razor/Compilation/TagHelpersquirecs/TagHelperDesigner.
  3. Порядковый номер: https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Chunks/Generators/AttributeBlockChunkL58Generator.
  4. Порядковый номер: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperDcriptesignTimeDesign.
  5. Порядковый номер: https://github.com/aspnet/Razor/blob/dbcb6901209859e471c9aa978912cf7d6c178668/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AttributeBlockChunkGenerator.cs#L56
  6. Порядковый номер: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/CaseSensitiveTag62Compact.
  7. Порядковый + IgnoreCase: https://github.com/aspnet/dnx/blob/bebc991012fe633ecac69675b2e892f568b927a5/src/Microsoft.Dnx.Tooling/NuGet/Core/PackageSource/PackL10Source.cs
  8. Порядковый номер: https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Tokenizer/Symbols/SymbolBase.cs#L52
  9. Порядковый номер: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/CaseSensitiveTribute39HelperAcs
  10. Порядковый номер: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperAttributeescriptDesign

(И десятки других.)

Таким образом, похоже, что в кодовой базе ASP.NET Core это чрезвычайно распространенный шаблон. Конечно, я не могу разговаривать ни с какой другой системой.

Из ~ 100 совпадений

Каждый из 10 перечисленных вами (я не просматривал остальную часть поиска) явно указывает сравнение строк, а не извлекает его из переменной, так что мы не просто говорим о разнице между, например:

`` С #
.Combine (Имя, StringComparison.OrdinalIgnoreCase)

``` C#
.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(Name))

? Это не "waaay super long" и более эффективно, если я чего-то не упускаю.

В любом случае, как я уже сказал, я просто предлагаю действительно подумать, нужны ли эти перегрузки. Если большинство людей так считают, а мы рассматриваем не только нашу собственную кодовую базу ASP.NET, хорошо.

В связи с этим, какое поведение мы планируем для нулевых входов? А как насчет int == 0? Я могу начать видеть больше преимуществ от перегрузки строки, если мы разрешим передачу null, поскольку я считаю, что StringComparer.GetHashCode обычно выбрасывает нулевой ввод, поэтому, если это действительно распространено, это начинает становиться более громоздким, если вызывающий в специальные значения NULL. Но тогда также возникает вопрос о том, каково будет поведение, если будет предоставлено значение null. Примешан ли 0 к хэш-коду, как и к любому другому значению? Считается ли это ошибкой, а хэш-код остается в покое?

Я думаю, что лучший общий подход к нулю - это смешать ноль. Для добавленного одного нулевого элемента было бы лучше иметь его в качестве nop, но если кто-то подает последовательность, тогда становится более выгодным иметь хеш с 10 нулевыми значениями, отличный от 20.

В самом деле, мой голос исходит из точки зрения кодовой базы ASP.NET Core, где было бы очень полезно иметь перегрузку, учитывающую строки. На самом деле меня больше не беспокоила длина строки, а скорее ее обнаруживаемость.

Если бы перегрузка с поддержкой строк была недоступна в системе, мы бы просто добавили внутренний метод расширения в ASP.NET Core и использовали бы его.

Если бы перегрузка с поддержкой строк была недоступна в системе, мы бы просто добавили внутренний метод расширения в ASP.NET Core и использовали бы его.

Я думаю, что это было бы отличным решением на данный момент, пока мы не увидим больше доказательств того, что такой API нужен в целом, в том числе за пределами базы кода ASP.NET Core.

Должен сказать, что не вижу смысла в удалении перегрузки string . Это не снижает сложности, не делает код более эффективным и не мешает нам улучшать другие области, такие как предоставление метода, возвращающего StringComparer из StringComparison . Синтаксический сахар имеет значение, потому что .NET всегда старался упростить общий случай. Мы также хотим направить разработчика, чтобы он поступил правильно и упал в яму успеха.

Мы должны понимать, что струны особенные и невероятно распространенные. Добавляя перегрузку, которая специализирует их, мы достигаем двух вещей:

  1. Мы делаем такие сценарии, как @Eilon , намного проще.
  2. Мы делаем очевидным, что сравнение строк важно, особенно корпуса.

Мы также должны учитывать, что общие стандартные помощники, такие как метод расширения @Eilon, упомянутый выше, - это не хорошо, а плохо. Это приводит к потраченным впустую часам на копирование и вставку вспомогательных методов и, вероятно, приведет к ненужному раздуванию кода и ошибкам, если это не будет выполнено должным образом.

Однако, если в первую очередь речь идет о специальном корпусе string , как насчет этого:

`` С #
общедоступная структура HashCode: IEquatable
{
общедоступный HashCode Combine(Т объект, IEqualityComparerкомпаратор);
}

// Использование
вернуть HashCode.Combine (_numberField)
.Combine (_stringField, StringComparer.OrdinalIgnoreCase);
`` ''

@terrajobst , ваш компромисс GetHashCode или вставлять дополнительный набор круглых скобок с настраиваемым компаратором.

(править: я думаю, я действительно должен поверить в это

@JonHanna: Да, мы также собираемся хешировать нулевые входные данные как 0.

Извините, что прерываю разговор здесь. Но где мне поставить новый шрифт? @mellinoe @ericstj @weshaggard , вы предлагаете мне создать новую сборку / пакет для этого типа, например System.HashCode , или мне следует добавить его в существующую сборку, например System.Runtime.Extensions ? Спасибо.

Недавно мы немного изменили компоновку сборки в .NET Core; Предлагаю разместить его там, где живут конкретные компараторы, которые, кажется, указывают на System.Runtime.Extensions .

@weshaggard?

@terrajobst Что касается самого предложения, я только что обнаружил, что, к сожалению, мы не можем назвать и статические перегрузки, и перегрузки экземпляра Combine . 😢

Следующее приводит к ошибке компилятора, поскольку имена экземпляров и статических методов не могут совпадать:

using System;
using System.Collections.Generic;

public struct HashCode
{
    public void Combine(int i)
    {
    }

    public static void Combine(int i)
    {
    }
}

Теперь у нас есть 2 варианта:

  • Переименуйте статические перегрузки во что-нибудь другое, например Create , Seed и т. Д.
  • Переместите статические перегрузки в другой статический класс:
public static class Hash
{
    public static HashCode Combine(int hash);
}

public struct HashCode
{
    public HashCode Combine(int hash);
}

// Usage:
return Hash.Combine(_field1)
           .Combine(_field2)
           .Combine(_field3);

Я предпочитаю второй. Очень жаль, что нам приходится работать над этой проблемой, но ... мысли?

Разделение логики на 2 типа звучит для меня странно - чтобы использовать HashCode вам нужно установить соединение и вместо этого начать с Hash class.

Я бы предпочел добавить Create method (или Seed или Init ).
Я бы добавил также перегрузку без аргументов HashCode.Create().Combine(_field1).Combine(_field2) .

@karelz , я не думаю, что нам стоит добавлять фабричный метод, если у него new , поскольку он более естественен. Кроме того, e не может помешать людям писать new HashCode().Combine поскольку это структура.

public override int GetHashCode()
{
    return new HashCode()
        .Combine(_field1)
        ...
}

Это дополнительная комбинация с хеш-кодом 0 и _field1 вместо инициализации непосредственно из хеш-кода. Однако побочным эффектом текущего хеша, который мы используем , является то, что 0 передается в качестве первого параметра, он будет повернут до нуля и добавлен к нулю. И когда 0 ставится на первый хэш-код, он просто создает первый хэш-код. Так что, если JIT хорошо справляется со сворачиванием констант (и я считаю, что он оптимизирует этот xor), по сути, это должно быть эквивалентно прямой инициализации.

Предлагаемый API (обновленная спецификация):

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public HashCode Combine(int hash);
        public HashCode Combine<T>(T obj);
        public HashCode Combine<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public override bool Equals(object obj);
        public override bool Equals(HashCode other);
        public override int GetHashCode();
    }
}

@redknightlois @JonHanna @stephentoub @Eilon , у вас есть мнение о фабричном методе и использовании конструктора по умолчанию? Я обнаружил, что компилятор не допускает статическую перегрузку Combine поскольку это конфликтует с методами экземпляра, поэтому у нас есть возможность либо

HashCode.Create(field1).Combine(field2) // ...

// or, using default constructor

new HashCode().Combine(field1).Combine(field2) // ...

Преимущество первого в том, что он немного короче. Преимущество второго состоит в том, что у него будет согласованное именование, поэтому вам не нужно писать что-то другое для первого поля.

Другая возможность - два разных типа: один с фабрикой Combine , другой с экземпляром Combine (или второй как расширение первого типа).

Я не уверен, что предпочел бы TBH.

@JonHanna , ваша вторая идея с перегрузками экземпляров, являющимися методами расширения, звучит великолепно. Тем не менее, hc.Combine(obj) в этом случае пытается уловить статическую перегрузку:

Я предложил использовать статический класс в качестве точки входа в нескольких комментариях выше, что напоминает мне ... @karelz , вы сказали

Разделение логики на 2 типа звучит для меня странно - чтобы использовать HashCode, вам нужно установить соединение и вместо этого начать с класса Hash.

Какая связь должна быть между людьми? Разве мы не познакомим их сначала с Hash , а затем оттуда они смогут перейти к HashCode ? Я не думаю, что добавление нового статического класса будет проблемой.

Разделение логики на 2 типа звучит для меня странно - чтобы использовать HashCode, вам нужно установить соединение и вместо этого начать с класса Hash.

Мы могли бы сохранить тип верхнего уровня HashCode и просто вложить структуру. Это позволит желаемое использование при сохранении «точки входа» API для одного типа верхнего уровня, например:

`` С #
пространство имен System
{
общедоступный статический класс HashCode
{
общедоступный статический HashCodeValue Combine (int hash);
общедоступный статический HashCodeValue Combine(T obj);
общедоступный статический HashCodeValue Combine(Т объект, IEqualityComparerкомпаратор);

    public struct HashCodeValue : IEquatable<HashCodeValue>
    {
        public HashCodeValue Combine(int hash);
        public HashCodeValue Combine<T>(T obj);
        public HashCodeValue Combine<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCodeValue hashCode);

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

        public bool Equals(HashCodeValue other);
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
}

}
`` ''

Изменить: хотя, вероятно, потребуется лучшее имя, чем HashCodeValue для вложенного типа, если мы пойдем по этому пути, поскольку HashCodeValue.Value немного избыточно, а не то, что Value будет использоваться очень довольно часто. Возможно, нам даже не нужно свойство Value - вы можете получить Value через GetHashCode() если не хотите преобразовывать в int .

@justinvp Но в чем вообще проблема с наличием двух разных типов? Эта система, похоже, нормально работает, например, для LinkedList<T> и LinkedListNode<T> .

Но в чем проблема с двумя отдельными типами?

Есть две проблемы с двумя типами верхнего уровня:

  1. Какой тип является «точкой входа» для API? Если это имена Hash и HashCode , с какого из них вы начнете? По этим именам не ясно. С LinkedList<T> и LinkedListNode<T> довольно ясно, какая из них является основной точкой входа, LinkedList<T> , а какая - вспомогательной.
  2. Загрязнение пространства имен System . Это не такая большая проблема, как (1), но о чем следует помнить, поскольку мы рассматриваем возможность раскрытия новых функций в пространстве имен System .

Вложенность помогает смягчить эти проблемы.

@justinvp

Какой тип является «точкой входа» для API? Если имена Hash и HashCode, с какого из них вы начнете? По этим именам не ясно. С LinkedListи LinkedListNodeдовольно ясно, какая из них является основной точкой входа, LinkedList, и который является помощником.

Хорошо, достаточно справедливо. Что, если бы мы назвали типы Hash и HashValue , а не типы вложенности? Означает ли это, что между этими двумя типами достаточно подчиненных отношений?

Если мы это сделаем, то фабричный метод станет еще кратким: Hash.Combine(field1).Combine(field2) . Кроме того, использование самого типа структуры по-прежнему практично. Например, кто-то может захотеть собрать список хэшей и сообщить об этом читателю, используя List<HashValue> вместо List<int> . Это может не сработать, если мы сделаем тип вложенным: List<HashCode.HashCodeValue> (даже List<Hash.Value> на первый взгляд сбивает с толку).

Загрязнение пространства имен System. Это не такая большая проблема, как (1), но о чем следует помнить, поскольку мы рассматриваем возможность раскрытия новых функций в пространстве имен System.

Я согласен, но я также считаю, что важно соблюдать правила и не жертвовать простотой использования. Например, единственный интерфейс BCL API, о котором я могу думать, где у нас есть вложенные типы (неизменяемые коллекции не учитываются, они не являются строго частью структуры), это List<T>.Enumerator , где мы активно хотим скрыть вложенные типы. type, потому что он предназначен для использования компилятором. В данном случае мы не хотим этого делать.

Возможно, нам даже не нужно свойство Value - вы можете получить Value через GetHashCode (), если не хотите приводить к int.

Я думал об этом раньше. Но как тогда пользователь узнает, что тип переопределяет GetHashCode или что у него есть неявный оператор?

Предлагаемый API

public static class Hash
{
    public static HashValue Combine(int hash);
    public static HashValue Combine<T>(T obj);
    public static HashValue Combine<T>(T obj, IEqualityComparer<T> comparer);
}

public struct HashValue : IEquatable<HashValue>
{
    public HashValue Combine(int hash);
    public HashValue Combine<T>(T obj);
    public HashValue Combine<T>(T obj, IEqualityComparer<T> comparer);

    public int Value { get; }

    public static implicit operator int(HashValue hashValue);

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

    public override bool Equals(object obj);
    public bool Equals(HashValue other);
    public override int GetHashCode();
}

Что, если бы мы назвали типы Hash и HashValue, а не типы вложенности?

Hash мне кажется слишком общим названием. Я думаю, нам нужно иметь HashCode в имени API точки входа, потому что его предполагаемая цель - помочь реализовать GetHashCode() , а не GetHash() .

кто-то может захотеть собрать список хэшей и передать его читателю Listиспользуется вместо списка. Это могло бы не сработать, если бы мы сделали тип вложенным: List(даже Списокна первый взгляд сбивает с толку).

Это кажется маловероятным вариантом использования - не уверен, что нам следует оптимизировать дизайн для этого.

единственные API-интерфейсы BCL, о которых я могу думать, где у нас есть вложенные типы

TimeZoneInfo.AdjustmentRule и TimeZoneInfo.TransitionTime - это примеры в BCL, которые были намеренно добавлены как вложенные типы.

@justinvp

Я думаю, что нам нужно иметь HashCode в имени API точки входа, потому что его предполагаемая цель - помочь реализовать GetHashCode (), а не GetHash ().

👍 Понятно.

Я подумал о вещах немного больше. Кажется разумным иметь вложенную структуру; как вы упомянули, большинство людей никогда не увидят настоящий тип. Только одно: я думаю, что тип должен называться Seed , а не HashCodeValue . Контекст его имени уже подразумевается содержащим классом.

Предлагаемый API

namespace System
{
    public static class HashCode
    {
        public static Seed Combine(int hash);
        public static Seed Combine<T>(T obj);
        public static Seed Combine<T>(T obj, IEqualityComparer<T> comparer);

        public struct Seed : IEquatable<Seed>
        {
            public Seed Combine(int hash);
            public Seed Combine<T>(T obj);
            public Seed Combine<T>(T obj, IEqualityComparer<T> comparer);

            public int Value { get; }

            public static implicit operator int(Seed seed);

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

            public bool Equals(Seed other);
            public override bool Equals(object obj);
            public override int GetHashCode();
        }
    }
}

@jamesqo Возникли возражения или проблемы с реализацией из-за того, что вместо public readonly int Value ? Проблема с Seed заключается в том, что технически это не семя после первого объединения.

Также согласитесь с @justinvp , Hash следует зарезервировать для работы с хешами. Вместо этого было введено упрощение работы с HashCode .

@redknightlois Для ясности, мы говорили об имени структуры, а не об имени свойства.

        public struct Seed : IEquatable<Seed>
        {
            public Seed Combine(int hash);
            public Seed Combine<T>(T obj);
            public Seed Combine<T>(T obj, IEqualityComparer<T> comparer);

            public int Value { get; }

            public static implicit operator int(Seed seed);

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

            public bool Equals(Seed other);
            public override bool Equals(object obj);
            public override int GetHashCode();
        }

Использование:
c# int hashCode = HashCode.Combine(field1).Combine(name, StringComparison.OrdinalIgnoreCase).Value; int hashCode = (int)HashCode.Combine(field1).Combine(field2);

Проблема с семенами в том, что они технически не являются семенами после первого комбайна.

Это посевной материал для следующего комбайна, который дает новое зерно.

Есть ли возражения или проблемы с реализацией с использованием вместо этого public readonly int Value?

Почему? int Value { get; } более идиоматичен и может быть легко встроен.

Это посевной материал для следующего комбайна, который дает новое зерно.

Разве это не был бы саженец? ;)

@jamesqo По моему опыту, когда я окружен сложным кодом, свойства, как правило, генерируют худший код, чем поля (в том числе не встроенные). Также поле readonly одного int в структуре транслируется прямо в регистр и, в конечном итоге, когда JIT использует readonly для оптимизации (которая еще не смогла найти его использования в отношении генерации кода); есть оптимизации, которые могут быть разрешены, потому что это может означать, что он доступен только для чтения. С точки зрения использования, на самом деле нет отличий от одного геттера.

РЕДАКТИРОВАТЬ: Кроме того, это также подталкивает к мысли, что эти структуры действительно неизменны.

По моему опыту, когда я окружен сложным кодом, свойства имеют тенденцию генерировать худший код, чем поля (в том числе не встроенные).

Если вы обнаружите одну сборку без отладки, в которой автоматически реализуемое свойство не всегда встроено, то это проблема JIT, и ее обязательно нужно исправить.

Также поле только для чтения одного int в структуре переводится прямо в регистр

есть оптимизации, которые могут быть разрешены, потому что это может означать, что он доступен только для чтения.

Поле поддержки этой структуры будет доступно только для чтения; API будет аксессуаром.

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

@jamesqo Я буду помнить об этом, когда найду их. Для кода, чувствительного к производительности, я просто больше не использую свойства из-за этого (на данный момент мышечная память).

Вы можете рассмотреть возможность вызова вложенной структуры «Состояние», а не «Семя»?

@ellismg Конечно, спасибо за предложение. Я изо всех сил пытался придумать хорошее имя для внутренней структуры.

@karelz Я думаю, что этот API наконец-то

@jamesqo @JonHanna, зачем нам Combine<T>(T obj) вместо Combine(object o) ?

зачем нам комбайн(T obj) вместо Combine (object o)?

Последний выделил бы, если бы экземпляр был структурой.

да, спасибо за разъяснения.

Нам не нравится вложенный тип, потому что он, кажется, усложняет дизайн. Основная проблема заключалась в том, что мы не можем называть статику и нестатику одним и тем же. У нас есть два варианта: избавиться от статики или переименовать. Мы думаем, что переименование в Create имеет наибольший смысл, поскольку оно создает довольно читаемый код по сравнению с использованием конструктора по умолчанию.

Если нет серьезных возражений, мы остановились на этом дизайне:

`` С #
пространство имен System
{
общедоступная структура HashCode: IEquatable
{
общедоступный статический HashCode Create (int hashCode);
общедоступный статический HashCode Create(T obj);
общедоступный статический HashCode Create(Т объект, IEqualityComparerкомпаратор);

    public HashCode Combine(int hashCode);
    public HashCode Combine<T>(T obj);
    public HashCode Combine<T>(T obj, IEqualityComparer<T> comparer);

    public int Value { get; }

    public static implicit operator int(HashCode hashCode);

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

    public bool Equals(HashCode other);
    public override bool Equals(object obj);
    public override int GetHashCode();
}

}
`` ''

Давайте подождем пару дней, чтобы получить дополнительные отзывы, чтобы узнать, есть ли сильные отзывы по утвержденному предложению. Тогда мы сможем наверстать упущенное.

Почему усложняет дизайн? Я мог понять, как было бы плохо, если бы нам действительно пришлось использовать HashCode.State в коде (например, для определения типа переменной), но ожидаем ли мы, что это часто так? В большинстве случаев я либо возвращаю значение сразу, либо конвертирую его в int и сохраняю его.

Я думаю, что комбинация Create и Combine хуже.

См. Https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653.

@terrajobst

Мы думаем, что переименование в Create имеет наибольший смысл, поскольку оно создает довольно читаемый код по сравнению с использованием конструктора по умолчанию.

Если нет серьезных возражений, мы остановились на этом дизайне:

Я слышу вас, но у меня возникла в последнюю минуту мысль, когда я работал над реализацией ... не могли бы мы просто добавить статическое свойство Zero / Empty в HashCode , а потом люди будут звонить оттуда в Combine ? Это освободило бы нас от необходимости иметь отдельные методы Combine / Create .

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public static HashCode Empty { get; }

        public HashCode Combine(int hashCode);
        public HashCode Combine<T>(T obj);
        public HashCode Combine<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
}

int GetHashCode()
{
    return HashCode.Empty
        .Combine(_1)
        .Combine(_2);
}

Кто-нибудь еще думает, что это хорошая идея? (А пока я отправлю PR, и, если люди так думают, я изменю его в PR.)

@jamesqo , мне нравится идея Empty / Zero.

Меня бы это устроило ( нет четкого предпочтения между Empty и Create factory) ... @weshaggard @bartonjs @stephentoub @terrajobst что вы, ребята, думаете?

Я лично считаю, что Create () лучше; но мне нравится HashCode.Empty больше, чем new HashCode() .

Поскольку он допускает версию, в которой отсутствует оператор new, и не исключает принятия решения позже, что мы действительно хотим Create в качестве загрузчика ... :: shrug ::.

Это полная степень моего сопротивления (иначе говоря, не очень).

FWIW Я бы проголосовал за Create а не за Empty / Zero . Я бы предпочел начать с фактического значения, чем вешать все на Empty / Zero . Это просто кажется странным.

Это также отвращает людей от посева с нулевым семенем, которое, как правило, является плохим семенем.

Я предпочитаю Create, а не Empty. Это согласуется с тем, как я думаю об этом: я хочу создать хеш-код и добавить дополнительные значения. Я бы тоже согласился с вложенным подходом.

Хотя я собирался сказать, что называть его Пустым было не очень хорошей идеей (и об этом уже было сказано), после третьей мысли я все еще думаю, что это неплохое решение. Как насчет чего-нибудь вроде Builder. Хотя использовать ноль по-прежнему можно, это слово как бы отговаривает вас использовать его сразу.

@JonHanna, чтобы прояснить: вы имели в виду голосование за Create , верно?

И на четвертую мысль, как насчет With вместо Create.

HashCode.With (a) .Combine (b). Объединить (c)

Пример использования, основанный на последнем обсуждении (с возможностью замены Create альтернативным именем):

`` С #
публичное переопределение int GetHashCode () =>
HashCode.Create (_field1) .Combine (_field2) .Combine (_field3);

We went down the path of this chaining approach, but didn't reconsider earlier proposals when the static & instance `Combine` methods didn't pan out...

Are we sure we don't want something like the existing `Path.Combine` pattern, that was proposed previously, with a handful of generic `Combine` overloads? e.g.:

```c#
public override int GetHashCode() =>
    HashCode.Combine(_field1, _field2, _field3);

@justinvp приведет к непоследовательному коду + большему количеству джиттинга, я думаю, b / c более общих комбинаций. Мы всегда можем вернуться к этому в другом выпуске, если окажется, что это будет желательно.

Как бы то ни было, я предпочитаю изначально предложенную версию, по крайней мере, в использовании (не уверен в комментариях относительно размера кода, джиттинга и т. Д.). Кажется излишним иметь дополнительную структуру и 10+ разных членов для чего-то, что можно было бы выразить как один метод с несколькими перегрузками разной степени. Я также не поклонник API свободного стиля в целом, так что, возможно, это окрашивает мое мнение.

Я не собирался упоминать об этом, потому что это немного необычно, и я до сих пор не уверен, что я к этому отношу, но вот еще одна идея, просто чтобы убедиться, что все альтернативы были рассмотрены ...

Что, если бы мы сделали что-то вроде изменяемого HashCodeCombiner "builder" ASP.NET Core с аналогичными методами Add , но также включили поддержку синтаксиса инициализатора коллекции?

Использование:

`` С #
публичное переопределение int GetHashCode () =>
новый HashCode {_field1, _field2, _field3};

With a surface area something like:

```c#
namespace System
{
    public struct HashCode : IEquatable<HashCode>, IEnumerable
    {
        public void Add(int hashCode);
        public void Add<T>(T obj);
        public void Add<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();

        IEnumerator IEnumerable.GetEnumerator();
    }
}

Он должен был бы реализовать как минимум IEnumerable вместе с хотя бы одним методом Add чтобы включить синтаксис инициализатора коллекции. IEnumerable может быть реализовано явно, чтобы скрыть его от intellisense, а GetEnumerator может либо выбросить NotSupportedException или вернуть значение хэш-кода в виде единого комбинированного элемента в перечисляемом списке, если кто-то случайно использовать его (что бывает редко).

@justinvp , у вас есть интересная идея. Однако я с уважением не согласен; Я думаю, что HashCode следует сохранить неизменным, чтобы избежать ошибок с изменяемыми структурами. Также необходимость реализовать IEnumerable для этого кажется своего рода искусственным / ненадежным; если у кого-то есть директива using System.Linq в файле, тогда Cast<> и OfType<> будут отображаться как методы расширения, если они поставят точку рядом с HashCode . Думаю, нам следует придерживаться нынешнего предложения.

@jamesqo , я согласен - поэтому я не

@MadsTorgersen , @jaredpar , почему инициализатор коллекции требует реализации IEnumerable \Третий комментарий @ justinvp выше.

@jamesqo , я согласен, что лучше оставить это неизменным (а не IEnumerable \

@mellinoe Я думаю, что это сделало бы простой случай немного проще, но также сделало бы все, кроме этого, более сложным (и менее ясным в отношении того, что следует делать).

Это включает:

  1. больше предметов, чем у вас есть перегрузки для
  2. условия
  3. петли
  4. используя компаратор

Рассмотрим код из ASP.NET, опубликованный ранее в этой теме (обновленный до текущего предложения):

`` С #
var hashCode = HashCode
.Create (IsMainPage)
.Combine (ViewName, StringComparer.Ordinal)
.Combine (имя_контроллера, StringComparer.Ordinal)
.Combine (Имя области, StringComparer.Ordinal);

если (ViewLocationExpanderValues! = ноль)
{
foreach (элемент var в ViewLocationExpanderValues)
{
hashCode = hashCode
.Combine (item.Key, StringComparer.Ordinal)
.Combine (item.Value, StringComparer.Ordinal);
}
}

return hashCode;

How would this look with the original `Hash.CombineHashCodes`? I think it would be:

```c#
var hashCode = Hash.CombineHashCodes(
    IsMainPage,
    StringComparer.Ordinal.GetHashCode(ViewName),
    StringComparer.Ordinal.GetHashCode(ControllerName),
    StringComparer.Ordinal.GetHashCode(AreaName));

if (ViewLocationExpanderValues != null)
{
    foreach (var item in ViewLocationExpanderValues)
    {
        hashCode = Hash.CombineHashCodes(
            hashCode
            StringComparer.Ordinal.GetHashCode(item.Key),
            StringComparer.Ordinal.GetHashCode(item.Value));
    }
}

return hashCode;

Даже если вы проигнорируете вызов GetHashCode() для пользовательских компараторов, я считаю, что передавать предыдущее значение hashCode в качестве первого параметра непросто.

@KrzysztofCwalina Согласно примечанию @ericlippert в языке программирования C # 1 , это потому, что инициализаторы коллекций (что неудивительно) предназначены для синтаксического сахара для создания коллекций, а не для арифметики (что было другим распространенным использованием метода с именем Add ).

1 Из-за того, как работает Google Книги, эта ссылка может работать не для всех.

@KrzysztofCwalina , и обратите внимание, для этого требуется не общий IEnumerable , а не IEnumerable<T> .

@svick , второстепенная .Combine будет .Create с текущим предложением. Если только мы не используем вложенный подход.

@svick

это также сделало бы что-либо, кроме этого, более сложным (и менее ясным в отношении того, что делать правильно)

Я не знаю, второй пример почти не отличается от первого в целом, и это не более сложный ИМО. При втором / исходном подходе вы просто передаете кучу хэш-кодов (я думаю, что первый параметр на самом деле должен быть IsMainPage.GetHashCode() ), поэтому мне это кажется простым. Но похоже, что я здесь в меньшинстве, поэтому я не буду настаивать на оригинальном подходе. У меня нет твердого мнения; оба этих примера кажутся мне достаточно разумными.

@justinvp Спасибо, обновлено. (Я пошел с первым предложением в первом посте и не понял, что оно устарело, вероятно, кто-то должен его обновить.)

@mellinoe проблема в том, что второй может генерировать незаметные ошибки. Это актуальный код одного из наших проектов.

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int GetHashCode(PageFromScratchBuffer obj)
        {
            int v = Hashing.Combine(obj.NumberOfPages, obj.ScratchFileNumber);
            int w = Hashing.Combine(obj.Size.GetHashCode(), obj.PositionInScratchBuffer.GetHashCode());
            return Hashing.Combine(v, w);            
        }

Мы живем с этим, но каждый день имеем дело с вещами очень низкого уровня; так что не средний разработчик, это точно. Однако здесь не то же самое, что комбинировать v с w, чем w с v ... то же самое между v и w комбинирует. Комбинации хешей не являются коммутативными, поэтому их последовательное связывание может фактически избавить от целого набора ошибок на уровне API.

Я пошел с первым предложением в первом посте и не понял, что оно устарело, вероятно, кто-то должен его обновить.

Сделанный.
Кстати: за этим предложением очень сложно следить, особенно за голосами ... так много вариантов (что, я думаю, хорошо ;-))

@karelz Если мы добавим Create API, я думаю, мы все равно сможем добавить Empty . Как сказал @bartonjs, это не обязательно должно быть одно или другое. Предложенный

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public HashCode();

        public static HashCode Empty { get; }

        public static HashCode Create(int hashCode);
        public static HashCode Create<T>(T value);
        public static HashCode Create<T>(T value, IEqualityComparer<T> comparer);

        public HashCode Combine(int hashCode);
        public HashCode Combine<T>(T value);
        public HashCode Combine<T>(T value, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public override string ToString();
    }
}

@JonHanna

Это также отвращает людей от посева с нулевым семенем, которое, как правило, является плохим семенем.

Алгоритм хеширования, который мы выберем, будет таким же, как сегодня в HashHelpers , что означает, что hash(0, x) == x . HashCode.Empty.Combine(x) даст те же результаты, что и HashCode.Create(x) , поэтому объективно разницы нет.

@jamesqo, вы забыли добавить дополнительные Zero в свое последнее предложение. Если это было упущением, можете ли вы его обновить? Затем мы можем попросить людей проголосовать за ваше последнее предложение. Похоже, что другие альтернативы (см. Верхний пост, который я обновил) не получают так много подписчиков ...

@karelz Спасибо за обнаружение, исправлено.

@KrzysztofCwalina, чтобы убедиться, что вы имеете в виду «Добавить» в смысле добавления в коллекцию, а не в каком-то другом смысле. Не знаю, нравится ли мне это ограничение, но тогда мы так решили.

public static HashCode Create(int hash);
public HashCode Combine(int hash);

Должен ли параметр называться hashCode вместо hash поскольку переданное значение будет хеш-кодом, вероятно, полученным при вызове GetHashCode() ?

Empty / Zero

Если мы в конечном итоге сохраним это, другое имя, которое следует рассмотреть, будет Default .

@justinvp

Должен ли параметр называться hashCode вместо hash, поскольку переданное значение будет хэш-кодом, вероятно, полученным при вызове GetHashCode ()?

Я хотел назвать параметры int hash и HashCode параметры hashCode . Однако, если подумать, я считаю, что hashCode было бы лучше, потому что, как вы упомянули, hash довольно расплывчато. Я обновлю API.

Если мы в конечном итоге сохраним это, другое имя, которое следует рассмотреть, будет Default.

Когда я слышу Default я думаю, что «это обычный способ сделать что-то, когда вы не знаете, какой вариант выбрать», а не «значение по умолчанию для структуры». например, что-то вроде Encoding.Default имеет совершенно другой оттенок.

Выбранный нами алгоритм хеширования будет таким же, как сегодня в HashHelpers, который имеет эффект hash (0, x) == x. HashCode.Empty.Combine (x) даст те же результаты, что и HashCode.Create (x), поэтому объективно разницы нет.

Как человек, мало разбирающийся в этом устройстве, мне очень нравится простота HashCode.Create(x).Combine(...) . Create очень очевиден, потому что он используется во многих других местах.

Если Empty / Zero / Default не обеспечивает какого-либо алгоритмического использования, его там не должно быть, ИМО.

PS: очень интересная ветка !! Молодец! 👍

@ cwe1ss

Если Empty / Zero / Default не обеспечивает какого-либо алгоритмического использования, его там не должно быть, IMO.

Наличие поля Empty действительно обеспечивает алгоритмическое использование. Он представляет собой «начальное значение», из которого вы можете комбинировать хеши. Например, если вы хотите объединить массив хешей строго с помощью Create , это довольно болезненно:

int CombineRange(int[] hashes)
{
    if (hashes.Length == 0)
    {
        return 0;
    }

    var result = HashCode.Create(hashes[0]);

    for (int i = 1; i < hashes.Length; i++)
    {
        result = result.Combine(hashes[i]);
    }

    return result;
}

Если у вас есть Empty , это становится намного естественнее:

int CombineRange(int[] hashes)
{
    var result = HashCode.Empty;

    for (int i = 0; i < hashes.Length; i++)
    {
        result = result.Combine(hashes[i]);
    }

    return result;
}

// or

int CombineRange(int[] hashes)
{
    return hashes.Aggregate(HashCode.Empty, (hc, next) => hc.Combine(next));
}

@terrajobst Для меня этот тип очень ImmutableArray<T> . Пустой массив сам по себе не очень полезен, но очень полезен в качестве «отправной точки» для других операций, и поэтому у нас есть для него свойство Empty . Я думаю, что было бы разумно иметь еще один за HashCode ; мы сохраняем Create .

@jamesqo Я заметил, что вы молча / случайно изменили имя аргумента obj на value в своем предложении https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653. Я переключил его обратно на obj который IMO лучше отражает то, что вы получаете. Имя value больше связано с самим значением хеш-функции "int" в этом контексте.
Я открыт для дальнейшего обсуждения имени аргумента, если это необходимо, но давайте изменим его намеренно и будем отслеживать разницу с последним одобренным предложением.

Я обновил предложение вверху. Я также вызвал diff против последней одобренной версии предложения.

Выбранный нами алгоритм хеширования будет таким же, как сегодня в HashHelpers.

Почему стоит выбрать именно такой алгоритм, который следует использовать везде? Какое предположение будет сделано относительно комбинирования хэш-кодов? Если он будет использоваться повсеместно, откроет ли он новые возможности для DDoS-атак? (Обратите внимание, что в прошлом мы были обожжены этим из-за хеширования строк.)

Что, если бы мы сделали что-то вроде изменяемого «построителя» ASP.NET Core HashCodeCombiner

Я думаю, что это правильный образец для использования. Хороший универсальный комбайнер хэш-кода обычно может использовать больше состояний, чем то, что помещается в сам хэш-код, но тогда плавный шаблон ломается, потому что передача более крупных структур является проблемой производительности.

Почему стоит выбрать именно такой алгоритм, который следует использовать везде?

Его не следует использовать везде. См. Мой комментарий на https://github.com/dotnet/corefx/issues/8034#issuecomment -260790829; в основном он нацелен на людей, мало разбирающихся в хешировании. Люди, которые знают, что делают, могут оценить это, чтобы увидеть, соответствует ли это их потребностям.

Какое предположение будет сделано относительно комбинирования хэш-кодов? Если он будет использоваться повсеместно, откроет ли он новые возможности для DDoS-атак?

Одна из проблем с текущим хешем заключается в том, что hash(0, x) == x . Таким образом, если в хэш подается последовательность нулей или нулей, он останется равным нулю. См. Код . Это не означает, что нули не учитываются, но ни один из начальных нулей не учитывается. Я подумываю использовать что-то более надежное (но немного более дорогое), как здесь , которое добавляет магическую константу, чтобы избежать сопоставления нуля с нулем.

Я думаю, что это правильный образец для использования. Хороший универсальный комбайнер хэш-кода обычно может использовать больше состояний, чем то, что помещается в сам хэш-код, но тогда плавный шаблон ломается, потому что передача более крупных структур является проблемой производительности.

Я не думаю, что должен быть универсальный комбайнер с большим размером структуры, который пытается соответствовать каждому варианту использования. Вместо этого я представлял себе отдельные типы хэш-кода, которые имеют размер int ( FnvHashCode и т. Д.) И все имеют свои собственные методы Combine . Кроме того, эти типы "строителей" в любом случае будут храниться в одном методе, а не передаваться.

Я не думаю, что должен быть универсальный комбайнер с большим размером структуры, который пытается соответствовать каждому варианту использования.

Сможет ли ASP.NET Core заменить свой собственный комбайнер хэш-кода , который в настоящее время имеет 64-битное состояние, на этот?

Я представлял себе отдельные типы хэш-кода, которые имеют размер int (FnvHashCode и т. Д.).

Разве это не приводит к комбинаторному взрыву? Он должен быть частью предложения API, чтобы прояснить, к чему ведет этот дизайн API.

@jkotas Подобные возражения я высказал в начале обсуждения. Работа с хэш-функциями требует знания предметной области. Но я понимаю и поддерживаю решение проблемы, возникшей в 2001 году, с введением хэш-кодов в самый корень фреймворка и не прописываю рецепт для объединения хешей. Этот дизайн направлен на решение этой проблемы в 99% случаев (когда знания предметной области недоступны или даже необходимы, поскольку статистические свойства хэша достаточно хороши). ASP.Net Core должен иметь возможность включать такие комбайнеры в структуру общего назначения на несистемной сборке, подобной той, которая предлагается для обсуждения здесь: https://github.com/dotnet/corefx/issues/13757

Я согласен с тем, что иметь комбайнер хэш-кода, который легко использовать в 99% случаев, - это хорошая идея. Однако он должен допускать большее внутреннее состояние, чем просто 32-битное.

Кстати: ASP.NET изначально использовал свободный шаблон для объединения хэш-кода, но прекратил это делать, потому что это приводило к ошибкам, которые легко пропустить: https://github.com/aspnet/Razor/pull/537

@jkotas относительно безопасности хэш-флуда.
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Не эксперт (вам следует проконсультироваться с одним, и у MS есть более чем несколько по этому вопросу) .

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

РЕДАКТИРОВАТЬ: Вы написали, пока я писал. В свете этого, что дает вам 64-битное состояние?

@jkotas Я изучил проблему, с которой вы

Реакция на aspnet / Common # 40

Описание https://github.com/aspnet/Common/issues/40 :

Найдите ошибку:

public class TagBuilder
{
    private Dictionary<string, string> _attributes;
    private string _tagName;
    private string _innerContent;

    public override int GetHashCode()
    {
        var hash = HashCodeCombiner.Start()
            .Add(_tagName, StringComparer.Ordinal)
            .Add(_innerContent, StringComparer.Ordinal);

        foreach (var kvp in _attributes)
        {
            hash.Add(kvp.Key, StringComparer.Ordinal).Add(kvp.Value, StringComparer.Ordinal);
        }

        return hash.Build();
    }
}

Ну давай же. Этот аргумент подобен тому, что string должен быть изменяемым, поскольку люди не понимают, что Substring возвращает новую строку. Изменяемые структуры гораздо хуже с точки зрения ошибок; Я думаю, мы должны сохранить структуру неизменной.

относительно безопасности хэш-флуда.

У этого есть две стороны: правильная конструкция (надежные структуры данных и т. Д.); и смягчение проблем в существующем дизайне. Оба они важны.

@karelz По поводу наименования параметров

Я заметил, что вы молча / случайно изменили имя аргумента obj на значение в своем предложении dotnet / corefx # 8034 (комментарий). Я переключил его обратно на obj, который IMO лучше отражает то, что вы получаете. В этом контексте значение имени больше связано с самим значением хеш-функции "int".
Я открыт для дальнейшего обсуждения имени аргумента, если это необходимо, но давайте изменим его намеренно и будем отслеживать разницу с последним одобренным предложением.

В одном из будущих предложений я рассматриваю возможность добавления API для массового объединения значений. Например: CombineRange(ReadOnlySpan<T>) . Если бы мы назвали это obj , нам пришлось бы назвать там параметр objs , что звучит очень неудобно. Поэтому вместо этого мы должны назвать его item ; в будущем мы можем назвать параметр диапазона items . Обновил предложение.

@jkotas согласен, но дело в том, что мы ничего не смягчаем на уровне объединителя ...

Единственное, что мы можем сделать, это иметь случайное начальное число, которое для всех состояний и целей я помню, когда видел код в string и он фиксируется для каждой сборки. (могу ошибаться, потому что это было очень давно). Правильная реализация случайных начальных чисел - единственное смягчение, которое здесь можно применить.

Это сложная задача, дайте мне свою лучшую строку или хеш-функцию памяти с фиксированным случайным начальным числом, и я пойду и построю набор из 32-битных хэш-кодов, который будет генерировать только коллизии. Я не боюсь бросать такой вызов, потому что это довольно легко сделать, теория вероятностей на моей стороне. Я бы даже пошел и сделал ставку, но я знаю, что выиграю, так что, по сути, это больше не ставка.

Более того ... более глубокий анализ показывает, что даже если смягчение последствий - это возможность иметь эти «случайные начальные числа», встроенные для каждого прогона, более запутанный комбайнер не нужен. Потому что, по сути, вы смягчили проблему у источника.

Допустим, у вас есть M1 и M2 с разными случайными начальными числами rs1 и rs2 ....
M1 выдаст h1 = hash('a', rs1) и h2=hash('b', rs1)
M2 выдаст h1' = hash('a', rs2) и h2'=hash('b', rs2)
Ключевым моментом здесь является то, что h1 и h1' будут отличаться с вероятностью 1/ (int.MaxInt-1) (если hash достаточно хороши), что для всех целей равно хорошо, как это будет.
Следовательно, все c(x,y) вы решите использовать (если оно достаточно хорошее), уже учитывает встроенное в источнике снижение риска.

РЕДАКТИРОВАТЬ: Я нашел код, вы используете Marvin32, который сейчас изменяется в каждом домене. Таким образом, смягчение для строк заключается в использовании случайных начальных чисел за запуск. Что, как я уже сказал, достаточно хорошее смягчение.

@jkotas

Сможет ли ASP.NET Core заменить свой собственный комбайнер хэш-кода, который в настоящее время имеет 64-битное состояние, на этот?

Абсолютно; он использует тот же алгоритм хеширования. Я только что сделал это тестовое приложение для измерения количества столкновений и запустил его 10 раз. Нет существенной разницы от использования 64 бит.

Я представлял себе отдельные типы хэш-кода, которые имеют размер int (FnvHashCode и т. Д.).

Разве это не приводит к комбинаторному взрыву? Он должен быть частью предложения API, чтобы прояснить, к чему ведет этот дизайн API.

@jkotas , не будет. Дизайн этого класса не станет камнем для будущих API-интерфейсов хеширования. Их следует рассматривать как более сложные сценарии, они должны входить в другое предложение, такое как dotnet / corefx # 13757, и будут иметь другое обсуждение дизайна. Я считаю, что гораздо важнее иметь простой API для общего алгоритма хеширования, для новичков, которые борются с переопределением GetHashCode .

Я согласен с тем, что иметь комбайнер хэш-кода, который легко использовать в 99% случаев, - это хорошая идея. Однако он должен допускать большее внутреннее состояние, чем просто 32-битное.

Когда нам понадобится больше внутреннего состояния, чем 32 бита? edit: Если это нужно, чтобы люди могли подключать пользовательскую логику хеширования, я думаю (снова), это следует рассматривать как расширенный сценарий и обсуждать в dotnet / corefx # 13757.

вы используете Marvin32, который сейчас меняется на каждом домене

Правильно, в .NET Core по умолчанию включено предотвращение рандомизации строкового хэш-кода. Он не включен по умолчанию для автономных приложений в полной .NET Framework из-за совместимости; он включается только через причуды (например, в средах с высоким риском).

У нас все еще есть код для нерандомизированного хеширования в .NET Core, но его можно удалить. Не думаю, что он нам снова понадобится. Это также сделало бы вычисление хэш-кода строки немного быстрее, потому что больше не будет проверяться, использовать ли нерандомизированный путь.

Алгоритм Marvin32, используемый для вычисления рандомизированных строковых хэш-кодов, имеет 64-битное внутреннее состояние. Это было выбрано профильными специалистами MS. Я почти уверен, что у них была веская причина использовать 64-битное внутреннее состояние, и они не использовали его только для замедления работы.

Комбайнер хешей общего назначения должен продолжать развивать это смягчение: он должен использовать случайное начальное число и достаточно сильный алгоритм комбинирования хэш-кода. В идеале он должен использовать тот же Marvin32 в качестве рандомизированного хеширования строк.

Алгоритм Marvin32, используемый для вычисления рандомизированных строковых хэш-кодов, имеет 64-битное внутреннее состояние. Это было выбрано профильными специалистами MS. Я почти уверен, что у них была веская причина использовать 64-битное внутреннее состояние, и они не использовали его только для замедления работы.

@jkotas , объединитель хэш-кода, с которым вы связались, не использует Marvin32. Он использует тот же наивный алгоритм DJBx33x, что и нерандомизированный string.GetHashCode .

Комбайнер хешей общего назначения должен продолжать развивать это смягчение: он должен использовать случайное начальное число и достаточно сильный алгоритм комбинирования хэш-кода. В идеале он должен использовать тот же Marvin32 в качестве рандомизированного хеширования строк.

Этот тип не предназначен для использования в местах, подверженных хэш-атакам DoS. Это нацелено на людей, которые не знают, что лучше добавить / xor, и поможет предотвратить такие вещи, как https://github.com/dotnet/coreclr/pull/4654.

Комбайнер хешей общего назначения должен продолжать развивать это смягчение: он должен использовать случайное начальное число и достаточно сильный алгоритм комбинирования хэш-кода. В идеале он должен использовать тот же Marvin32 в качестве рандомизированного хеширования строк.

Затем мы должны поговорить с командой C #, чтобы они реализовали смягчающий алгоритм хеширования ValueTuple . Потому что этот код также будет использоваться в средах с высоким риском. И, конечно же, Tuple https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Tuple.cs#L60 или System.Numerics.HashHelpers (используется во всем место).

Теперь, прежде чем мы решим, как его реализовать, я бы посмотрел на тех же предметных экспертов, стоит ли платить за полностью рандомизированный алгоритм комбинирования хэш-кода (если он существует, конечно), даже если это не повлияет на то, как API разработан либо (в соответствии с предложенным API вы можете использовать состояние 512 бит и по-прежнему иметь тот же общедоступный API, если вы, конечно, готовы заплатить за это).

Это нацелено на людей, которые не знают, что добавить / xor

Именно поэтому так важно, чтобы он был надежным. Ключевое значение .NET состоит в том, что он решает проблемы для людей, которые не знают лучшего.

И пока мы это делаем, давайте не будем забывать о IntPtr https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/IntPtr.cs#L119
Это особенно неприятно, xor, вероятно, самый худший из наших, потому что bad будет конфликтовать с dab .

реализовать смягчающий алгоритм хеширования ValueTuple

Хорошая точка зрения. Я не уверен, была ли поставлена ​​ValueTuple или пришло время это сделать. Открыл https://github.com/dotnet/corefx/issues/14046.

не забываем про IntPtr

Это прошлые ошибки ... планка для их исправления намного выше.

@jkotas

Это прошлые ошибки ... планка для их исправления намного выше.

Я думал, что одним из достоинств .Net Core является то, что планка для таких «мелких» изменений должна быть намного ниже. Если кто-то зависит от реализации IntPtr.GetHashCode (чего на самом деле не следует), он может отказаться от обновления своей версии .Net Core.

планка для таких "мелких" изменений должна быть намного ниже

Да, по сравнению с полной версией .NET Framework. Но вам все равно нужно проделать работу, чтобы изменения прошли через систему, и вы можете обнаружить, что это просто не стоит усилий. Недавний пример - изменение в алгоритме хеширования Tuple<T> , которое было отменено из-за поломки F #: https://github.com/dotnet/coreclr/pull/6767#issuecomment -256896016

@jkotas

Если бы мы сделали HashCode 64-битным, думаете ли вы, что неизменный дизайн убьет производительность в 32-битных средах? Я согласен с другими читателями, шаблон строителя кажется намного хуже.

Убить перфоманс - нет. Штраф за производительность, заплаченный за синтаксический сахар - да.

Штраф за производительность, заплаченный за синтаксический сахар - да.

Может ли JIT оптимизировать это в будущем?

Убивает перфоманс - нет.
Штраф за производительность, заплаченный за синтаксический сахар - да.

Это больше, чем синтаксический сахар. Если бы мы хотели сделать HashCode классом, это было бы синтаксическим сахаром. Но изменяемый тип значения - это ферма ошибок.

Цитируя вас ранее:

Именно поэтому так важно, чтобы он был надежным. Ключевое значение .NET состоит в том, что он решает проблемы для людей, которые не знают лучшего.

Я бы сказал, что изменяемый тип значения не является надежным API для большинства людей, которые не знают лучше.

Я бы сказал, что изменяемый тип значения не является надежным API для большинства людей, которые не знают лучше.

Соглашаться. Думаю, мне жаль, что это относится к изменяемым типам построителей структур. Я использую их все на время из - за их хорошо и плотно. [MustNotCopy] аннотации кому-нибудь?

MustNotCopy - это сбывшаяся мечта любителя структур. @jaredpar?

MustNotCopy похож на стек, но его еще сложнее использовать 😄

Я предлагаю не создавать никаких классов, а создавать методы расширения для объединения хешей

static class HashHelpers
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash(this int hash1, int hash2);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, T value);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, T value, IEqualityComparer<T> comparer);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, IEnumerable<T> values);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, IEnumerable<T> values, IEqualityComparer<T> comparer);
}

Вот и все! Это быстро и легко.

@AlexRadch Мне не нравится, что это загрязняет список методов для всех целых чисел , а не только для тех, которые предназначены для хешей.

Кроме того, у вас есть методы, которые продолжают цепочку вычисления хэш-кода, но как ее запустить? Вам нужно сделать что-то неочевидное, например, начать с нуля? Т.е. 0.CombineHash(this.FirstName).CombineHash(this.LastName) .

Обновление: в соответствии с комментарием в dotnet / corefx # 14046 было решено, что существующая формула хеширования будет сохранена для ValueTuple :

@jamesqo Спасибо за помощь.
Судя по последнему обсуждению с @VSadov , мы можем продвигаться вперед с рандомизацией / заполнением, но предпочли бы воздержаться от принятия более дорогостоящей хеш-функции.
Рандомизация позволяет изменить хеш-функцию в будущем, если возникнет такая необходимость.

@jkotas , можем ли мы просто сохранить текущий хеш на основе ROL 5 для HashCode и уменьшить его до 4 байтов? Это устранило бы все проблемы с копированием структуры. У нас может быть HashCode.Empty представляющее случайное хеш-значение.

@svick
Да, это загрязняет методы для всех целых чисел, но его можно поместить в отдельное пространство имен, и если вы не работаете с хешами, вы не включите его и не увидите.

0.CombineHash(this.FirstName).CombineHash(this.LastName) следует писать как this.FirstName.GetHash().CombineHash(this.LastName)

Для реализации, начиная с семени, у него может быть следующий статический метод

static class HashHelpers
{
    public static int ClassSeed<T>();
}

class SomeClass
{
    int GetHash()
    {
        return HashHelpers.ClassSeed<SomeClass>().CombineHash(value1).CombineHash(value2);
    }
}

Таким образом, каждый класс будет иметь разное начальное число для рандомизации хэшей.

@jkotas , можем ли мы тогда просто сохранить текущий хэш на основе ROL 5 для HashCode и уменьшить его до 4 байтов?

Я думаю, что помощник по построению хэш-кода общедоступной платформы должен использовать 64-битное состояние, чтобы быть надежным. Если он только 32-битный, он будет иметь тенденцию давать плохие результаты, когда он используется, в частности, для хеширования большего количества элементов, массивов или коллекций. Как вы пишете документацию, когда лучше использовать ее, а не нет? Да, это дополнительные инструкции, потраченные на смешивание битов, но я не думаю, что это имеет значение. Такие инструкции выполняются очень быстро. Мой опыт показывает, что лучше делать больше битового микширования, чем меньше, потому что эффекты слишком маленького битового микширования намного более серьезны, чем слишком много.

Кроме того, у меня все еще есть опасения по поводу предлагаемой формы API. Я считаю, что проблему следует рассматривать как построение хеш-кода, а не объединение хеш-кода. Возможно, еще преждевременно добавлять это в качестве API платформы, и нам лучше подождать и посмотреть, появится ли для этого лучший шаблон. Это не мешает кому-либо публиковать (исходный) пакет nuget с этим API или corefx, используя его в качестве внутреннего помощника.

@jkotas, имеющий 64-битное состояние, не гарантирует, что ваш вывод будет иметь правильные статистические свойства, сама функция комбинирования должна быть разработана для использования внутреннего 64-битного состояния. Кроме того, если функция комбинирования хороша (с точки зрения статистики), нет такой вещи, как более чем меньшее перемешивание. Если хеширование имеет рандомизацию, лавину и другие интересующие статистические свойства, смешение учитывается, поскольку технически это специально созданная хеш-функция.

Посмотрите, что делает хорошую хеш-функцию (некоторые из них явно похожи на xor : http://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and -speed и https://research.neustar.biz/2012/02/02/choosing-a-good-hash-function-part-3/

@jamesqo Кстати, я только что понял, что комбайнер не будет работать в случае: «Я на самом деле комбинирую хеши (не хеши времени выполнения), потому что начальное число будет меняться каждый раз». ... публичный конструктор с семенем?

@jkotas

Я думаю, что помощник по построению хэш-кода общедоступной платформы должен использовать 64-битное состояние, чтобы быть надежным. Если он только 32-битный, он будет иметь тенденцию давать плохие результаты, когда он используется, в частности, для хеширования большего количества элементов, массивов или коллекций.

Имеет ли это значение, когда в итоге он будет сжат до одного int ?

@jamesqo Не совсем, размер состояния зависит только от функции, а не от надежности. Фактически, вы действительно можете ухудшить свою хеш-функцию, если комбайн не предназначен для такой работы, и в лучшем случае вы тратите ресурсы, потому что вы не можете получить случайность от принуждения.

Следствие: если вы согласовываете, убедитесь, что функция статистически превосходна, иначе вы почти гарантированно ухудшите ее.

Это зависит от того, есть ли корреляция между элементами. Если корреляции нет, 32-битное состояние и простой rotl (или даже xor) работают нормально. Если есть корреляция, это зависит от обстоятельств.

Подумайте, не использовал ли кто-нибудь это для построения строкового хэш-кода из отдельных символов. Не то чтобы кто-то действительно сделал это для строки, но это демонстрирует проблему:

for (int i = 0; i < str.Length; i++)
   hashCodeBuilder.Add(str[i]);

Это дало бы плохие результаты для строк с 32-битным состоянием и простым вращением, потому что символы в реальных строках имеют тенденцию быть коррелированными. Как часто элементы, для которых это используется, будут коррелироваться, и насколько плохие результаты это даст? Трудно сказать, хотя в реальной жизни все происходит неожиданным образом.

Было бы здорово добавить следующий метод в поддержку API рандомизации хэша.

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
       // add this
       public static HashCode CreateRandomized(Type type);
       // or add this
       public static HashCode CreateRandomized<T>();
    }
}

@jkotas Я не тестировал его, поэтому надеюсь, что вы это сделали. Но это определенно говорит о функции, которую мы собираемся использовать. Это просто недостаточно хорошо, если вы хотите обменять скорость на надежность (никто не может делать с ней глупости). На этот раз я предпочитаю дизайн, согласно которому это не некриптографическая хеш-функция, а быстрый способ комбинировать некоррелированные хэш-коды (которые настолько случайны, насколько и получаются).

Если мы хотим, чтобы никто не делал с ним глупостей, использование 64-битного состояния ничего не исправляет, мы просто скрываем проблему. По-прежнему можно создать ввод, который будет использовать эту корреляцию. Что еще раз указывает нам на тот же аргумент, который я выдвинул 18 дней назад. См. Https://github.com/dotnet/corefx/issues/8034#issuecomment -261301533

Я на этот раз одобряю дизайн, согласно которому это не некриптографическая хеш-функция, а быстрый способ комбинировать некоррелированные хэш-коды.

Самый быстрый способ комбинировать некоррелированные хэш-коды - xor ...

Верно, но мы знаем, что в прошлый раз все получилось не так (мне на ум приходит IntPtr). Вращение и XOR (текущее) так же быстро, без потерь, если кто-то действительно вставит что-то коррелированное.

Добавьте рандомизацию хэш-кода с помощью public static HashCode CreateRandomized(Type type); или с помощью методов public static HashCode CreateRandomized<T>(); или с обоими.

@jkotas Думаю, я нашел для этого лучший образец. Что, если бы мы использовали возврат ссылок C # 7? Вместо того, чтобы каждый раз возвращать HashCode , мы вернем ref HashCode которое помещается в регистр.

public struct HashCode
{
    private readonly long _value;

    public ref HashCode Combine(int hashCode)
    {
        CombineCore(ref _value, hashCode); // note: modifies the struct in-place
        return ref this;
    }
}

Использование остается таким же, как и раньше:

return HashCode.Combine(1)
    .Combine(2).Combine(3);

Единственным недостатком является то, что мы снова вернулись к изменяемой структуре. Но я не думаю, что есть способ одновременно отказаться от копирования и неизменности.

( ref this пока не работает, но я вижу PR в Roslyn, чтобы включить его здесь. )


@AlexRadch Я не думаю, что разумно больше комбинировать хэш с типом, так как получение хеш-кода типа стоит дорого.

@jamesqo public static HashCode CreateRandomized<T>(); не получают хэш-код типа. Он создает случайный HashCode для этого типа.

@jamesqo " ref this пока не работает". Даже после того, как проблема с Roslyn будет исправлена, ref this какое-то время не будет доступен для репозитория corefx (я не уверен, как долго, @stephentoub, вероятно, сможет установить ожидания).

Обсуждение дизайна здесь не сходится. Более того, за 200 комментариями очень сложно следить.
Мы планируем получить @jkotas на следующей неделе и опубликовать предложение в обзоре API в следующий вторник. Затем мы опубликуем предложение здесь для дальнейших комментариев.

На стороне: я предлагаю закрыть этот вопрос и создать новый с «благословенным предложением», когда оно будет у нас на следующей неделе, чтобы снизить нагрузку на последующее долгое обсуждение. Дай мне знать, если ты думаешь, что это плохая идея.

@jcouv Я Unsafe .)

@karelz ОК: smile: Я

@karelz Я ref this для ссылочных типов, а не для типов значений. ref this нельзя безопасно вернуть из структур; см. здесь, почему. Так что компромисс с возвратом референса работать не будет.

В любом случае закрою этот вопрос. Я открыл здесь еще одну проблему: https://github.com/dotnet/corefx/issues/14354

Должна быть возможность вернуть ref "this" из сообщения о методе расширения типа значения https://github.com/dotnet/roslyn/pull/15650, хотя я предполагаю, что C # vNext ...

@benaadams

Должна быть возможность вернуть ref "this" из метода расширения типа значения post dotnet / roslyn # 15650, хотя я предполагаю, что C # vNext ...

Правильный. Можно вернуть this из метода расширения ref this . Однако невозможно вернуть this из обычного метода экземпляра структуры. Есть много кровавых подробностей о том, почему это так :(

@redknightlois

если мы хотим быть строгими, единственным хешем должен быть uint , это можно рассматривать как недосмотр, что фреймворк возвращает int в этом свете.

CLS-соответствие? Целые числа без знака несовместимы с CLS.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

btecu picture btecu  ·  3Комментарии

iCodeWebApps picture iCodeWebApps  ·  3Комментарии

omariom picture omariom  ·  3Комментарии

matty-hall picture matty-hall  ·  3Комментарии

bencz picture bencz  ·  3Комментарии