Runtime: Ajout d'un type HashCode pour aider à combiner les codes de hachage

Créé le 25 avr. 2016  ·  206Commentaires  ·  Source: dotnet/runtime

Remplacement de la longue discussion avec plus de 200 commentaires par le nouveau problème dotnet/corefx#14354

Ce problème est FERMÉ !!!


Motivation

Java a Objects.hash pour combiner rapidement les codes de hachage des champs constitutifs pour retourner dans Object.hashCode() . Malheureusement, .NET n'a pas d'équivalent et les développeurs sont obligés de lancer leurs propres hachages comme ceci :

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

Parfois, les gens ont même recours à Tuple.Create(field1, field2, ...).GetHashCode() pour cela, ce qui est mauvais (évidemment) car il alloue.

Proposition

  • Liste des changements dans la proposition actuelle (par rapport à la dernière version approuvée https://github.com/dotnet/corefx/issues/8034#issuecomment-262331783) :

    • Empty propriété ajoutée (comme point de départ naturel analogue à ImmutableArray )

    • Noms des arguments mis à jour : 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();
    }
}

Usage:

```c#
int hashCode1 = HashCode.Create(f1).Combine(f2).Value;
int hashCode2 = hashes.Aggregate(HashCode.Empty, (seed, hash) => seed.Combine(hash));

var hashCode3 = HashCode.Empty;
foreach (int hash in hashs) { hashCode3 = hashCode3.Combine(hash); }
(int) hashCode3;
```

Remarques

L'implémentation doit utiliser l'algorithme dans HashHelpers .

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

Commentaire le plus utile

[@redknightlois] Si nous avons besoin d'une justification pour choisir System je peux essayer une justification. Nous avons construit HashCode pour aider dans les implémentations de object.GetHashCode() , il semble approprié que les deux partagent l'espace de noms.

C'est la justification que @KrzysztofCwalina et moi avons également utilisée. Vendu!

Tous les 206 commentaires

Si vous voulez quelque chose de rapide et sale, vous pouvez utiliser ValueTuple.Create(field1, field2).GetHashCode() . C'est le même algorithme que celui utilisé dans Tuple (qui d'ailleurs est similaire à celui de Objects ) et n'a pas de surcoût d'allocation.

Sinon, il y a des questions sur la qualité d'un hachage dont vous aurez besoin, quelles seront les valeurs de champ probables (ce qui affecte les algorithmes qui donneront de bons ou de mauvais résultats), y a-t-il une probabilité d'attaques par hashDoS, les collisions modulo un binaire- le nombre pair fait mal (comme ils le font avec les tables de hachage binaires paires), et ainsi de suite, ce qui rend inapplicable une solution unique.

@JonHanna Je pense que ces questions s'appliquent également, par exemple, à string.GetHashCode() . Je ne vois pas pourquoi fournir Hash devrait être plus difficile que cela.

En fait, cela devrait être plus simple, car les utilisateurs ayant des exigences particulières peuvent facilement arrêter d'utiliser Hash , mais arrêter d'utiliser string.GetHashCode() est plus difficile.

Si vous voulez quelque chose de simple et rapide, vous pouvez utiliser ValueTuple.Create(field1, field2).GetHashCode().

Ah, bonne idée, je n'avais pas pensé à ValueTuple en faisant ce post. Malheureusement, je ne pense pas que cela sera disponible avant C# 7/la prochaine version du framework, ni même si ce sera aussi performant (ces appels de propriété/méthode à EqualityComparer peuvent s'additionner). Mais je n'ai pris aucune référence pour mesurer cela, donc je ne saurais pas vraiment. Je pense juste qu'il devrait y avoir une classe dédiée/simple pour le hachage que les gens pourraient utiliser sans utiliser de tuples comme solution de contournement.

Sinon, il y a des questions sur la qualité d'un hachage dont vous aurez besoin, quelles seront les valeurs de champ probables (ce qui affecte les algorithmes qui donneront de bons ou de mauvais résultats), y a-t-il une probabilité d'attaques par hashDoS, les collisions modulo un binaire- le nombre pair fait mal (comme ils le font avec les tables de hachage binaires paires), et ainsi de suite, ce qui rend inapplicable une solution unique.

Tout à fait d'accord, mais je ne pense pas que la majorité des implémentations en tiennent compte, par exemple l' implémentation actuelle de ArraySegment est assez naïve. Le but principal de cette classe (en plus d'éviter les allocations) serait de fournir une implémentation de référence pour les personnes qui ne connaissent pas grand-chose au hachage, pour les empêcher de faire quelque chose de stupide comme celui-ci . Les personnes qui doivent faire face aux situations que vous avez décrites peuvent implémenter leur propre algorithme de hachage.

Malheureusement, je ne pense pas que cela sera disponible avant C# 7/la prochaine version du framework

Je pense que vous pouvez l'utiliser avec C# 2, mais pas avec le support intégré.

ou même savoir si ce sera aussi performant (ces appels de propriété/méthode à EqualityComparer peuvent s'additionner)

Que ferait cette classe différemment ? Si l'appel explicite de obj == null ? 0 : obj.GetHashCode() est plus rapide, cela devrait être déplacé dans ValueTuple .

J'aurais été enclin à donner +1 à cette proposition il y a quelques semaines, mais je suis moins enclin à la lumière de ValueTuple réduisant les frais généraux d'allocation de l'astuce consistant à utiliser Tuple pour cela, cela me semble se situer entre deux tabourets : si vous n'avez pas besoin de quelque chose de particulièrement spécialisé, vous pouvez utiliser ValueTuple , mais si vous avez besoin de quelque chose au-delà, alors un cours comme celui-ci ne va pas aller loin assez.

Et quand nous aurons C#7, il y aura le sucre syntaxique pour le rendre encore plus facile.

@JonHanna

Que ferait cette classe différemment ? Si vous appelez explicitement obj == null ? 0 : obj.GetHashCode() est plus rapide que cela devrait être déplacé dans ValueTuple.

Pourquoi ne pas utiliser ValueTuple simplement la classe Hash pour obtenir des codes de hachage ? Cela réduirait également considérablement le LOC dans le fichier (qui est actuellement d'environ 2 000 lignes).

Éditer:

Si vous n'avez pas besoin de quelque chose de particulièrement spécialisé, vous pouvez utiliser ValueTuple

C'est vrai, mais le problème est que beaucoup de gens pourraient ne pas s'en rendre compte et mettre en œuvre leur propre fonction de hachage naïve inférieure (comme celle que j'ai liée ci-dessus).

Que je pouvais effectivement prendre du retard.

Probablement en dehors du cadre de ce problème. Mais avoir un espace de noms de hachage où nous pouvons trouver des hachages cryptographiques et non cryptographiques hautes performances écrits par des experts serait une victoire ici.

Par exemple, nous avons dû coder nous-mêmes xxHash32, xxHash64, Metro128 et également sous-échantillonner de 128 à 64 et de 64 à 32 bits. Avoir un éventail de fonctions optimisées peut aider les développeurs à éviter d'écrire leurs propres fonctions non optimisées et/ou boguées (je sais, nous avons également trouvé quelques bogues dans les nôtres) ; mais toujours en mesure de choisir en fonction des besoins.

Nous serions ravis de faire don de nos implémentations s'il y a un intérêt, afin qu'elles puissent être examinées et optimisées davantage par des experts.

@redknightlois Je serais heureux d'ajouter mon implémentation SpookyHash à un effort comme celui-ci.

@svick Attention avec string.GetHashCode(), c'est très spécifique, pour une très bonne raison, les attaques Hash DoS.

@terrajobst , jusqu'où est-ce dans la file d'attente de triage/révision de l'API ? Je pense que c'est une API simple que nous avons toujours voulu ajouter à la plate-forme et peut-être que nous avons maintenant suffisamment de masse critique pour le faire réellement ?

cc: @ellismg

Je pense qu'il est prêt à réviser dans son état actuel.

@mellinoe C'est super ! J'ai un peu nettoyé la proposition pour la rendre plus laconique, et j'ai également ajouté quelques questions à la fin qui devraient, selon moi, être abordées.

@jamesqo Il devrait y avoir aussi long basé.

@redknightlois , long de Combine .

La suggestion de elle pas assez bonne ?

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

À moins qu'il n'y ait de bonnes raisons pour lesquelles ce n'est pas assez bon, nous ne pensons pas que cela fasse l'affaire.

Au-delà du code généré pour être pire de quelques ordres de grandeur, je ne peux penser à aucune autre raison suffisante. À moins bien sûr qu'il y ait des optimisations dans le nouveau runtime qui traitent ce cas particulier à l'esprit, auquel cas cette analyse est sans objet. Cela dit, j'ai essayé cela sur 1.0.1.

Permettez-moi d'illustrer avec un exemple.

Supposons que nous prenions le code réel utilisé pour ValueTuple et que nous utilisions des constantes pour l'appeler.

        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();
        }
    }

Maintenant, sous un compilateur optimisé, il y a de fortes chances qu'il ne devrait pas y avoir de différence, mais en réalité il y en a.

Ceci est le code réel pour ValueTuple

image
Alors maintenant, que peut-on voir ici? Nous créons d'abord une structure dans la pile, puis nous appelons le code de hachage réel.

Maintenant, comparez-le avec l'utilisation de HashHelper.Combine qui, à toutes fins utiles, pourrait être l'implémentation réelle de Hash.Combine

image

Je sais!!!
Mais ne nous arrêtons pas là... utilisons les paramètres réels :

        [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

La bonne chose, c'est extrêmement stable. Mais comparons-le avec l'alternative :

image

Passons maintenant à l'excès...

        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);
        }

Et le résultat est assez illustratif

image

Je ne peux pas vraiment inspecter le code réel que le JIT génère pour l'appel, mais seuls le prologue et l'épilogue suffisent pour justifier l'inclusion de la proposition.

image

Le point à retenir de l'analyse est simple : que le type de détention soit un struct ne signifie pas qu'il est gratuit :)

La performance a été évoquée au cours de la réunion. La question est de savoir si cette API est susceptible d'être sur le chemin chaud. Pour être clair, je ne dis pas que nous ne devrions pas avoir l'API. Je dis simplement qu'à moins qu'il n'y ait un scénario concret, il est plus difficile de concevoir l'API car nous ne pouvons pas dire "nous en avons besoin pour X, donc la mesure du succès est de savoir si X peut l'utiliser". C'est important pour les API qui ne vous permettent pas de faire quelque chose de nouveau, mais plutôt de faire la même chose de manière plus optimisée.

Je pense que plus il est important d'avoir un hachage rapide et de bonne qualité, plus il est important d'adapter l'algorithme utilisé aux objets et à la plage de valeurs susceptibles d'être vues, et donc plus vous en avez besoin une aide, plus vous avez besoin de ne pas utiliser une telle aide.

@terrajobst , la performance était une motivation majeure pour cette proposition mais pas la seule. Avoir un type dédié aidera à la découvrabilité ; même avec la prise en charge intégrée des tuples en C# 7, les développeurs ne savent pas nécessairement qu'ils sont assimilés à la valeur. Même s'ils le font, ils peuvent oublier que les tuples remplacent GetHashCode , et finiront probablement par devoir Google comment implémenter GetHashCode dans .NET.

En outre, il existe un problème d'exactitude subtil avec l'utilisation de ValueTuple.Create.GetHashCode . Au-delà des 8 éléments, seuls les 8 derniers éléments sont hachés ; le reste est ignoré.

@terrajobst Chez RavenDB, les performances de GetHashCode ont eu un tel https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/InternalUtilities/Hash.cs consultez également la discussion sur Roslyn spécifiquement ici : https://github .com/dotnet/coreclr/issues/1619 ... Ainsi, lorsque la performance est CLÉ, nous ne pouvons pas utiliser la plate-forme fournie et devons lancer la nôtre (et en payer les conséquences).

De plus, le problème

@JonHanna

Je pense que plus il est important d'avoir un hachage rapide et de bonne qualité, plus il est important d'adapter l'algorithme utilisé aux objets et à la plage de valeurs susceptibles d'être vues, et donc plus vous en avez besoin une aide, plus vous avez besoin de ne pas utiliser une telle aide.

Donc, vous dites que l'ajout d'une classe d'assistance serait mauvais, car cela encouragerait les gens à simplement ajouter la fonction d'assistance sans réfléchir à la manière de faire un hachage approprié ?

Il semble que le contraire serait vrai, en fait ; Hash.Combine devrait généralement améliorer les implémentations de GetHashCode . Les personnes qui savent faire du hachage peuvent évaluer Hash.Combine pour voir si cela correspond à leur cas d'utilisation. Les débutants qui ne connaissent pas vraiment le hachage utiliseront Hash.Combine au lieu de simplement xor-ing (ou pire, d'ajouter) les champs constitutifs car ils ne savent pas comment faire un hachage approprié.

Nous en avons discuté un peu plus et vous nous avez convaincus :-)

Encore quelques questions :

  1. Nous devons décider où mettre ce type. L'introduction d'un nouvel espace de noms semble étrange ; System.Numerics pourrait fonctionner cependant. System.Collections.Generic peut également fonctionner, car il possède les comparateurs et le hachage est le plus souvent utilisé dans le contexte des collections.
  2. Devrions-nous fournir un modèle de générateur sans allocation pour combiner un nombre inconnu de codes de hachage ?

Sur (2) @Eilon avait ceci à dire :

Pour référence, ASP.NET Core (et ses prédécesseurs et projets associés) utilise un HashCodeCombiner : https://github.com/aspnet/Common/blob/dev/src/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs

( @David Fowler l'a mentionné dans le fil GitHub il y a plusieurs mois.)

Et ceci est un exemple d'utilisation : https://github.com/aspnet/Mvc/blob/760c8f38678118734399c58c2dac981ea6e47046/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ViewLocationCacheKey.cs#L129 -L144

``` C#
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(IsMainPage ? 1 : 0);
hashCodeCombiner.Add(ViewName, StringComparer.Ordinal);
hashCodeCombiner.Add(ControllerName, StringComparer.Ordinal);
hashCodeCombiner.Add(AreaName, StringComparer.Ordinal);

if (ViewLocationExpanderValues ​​!= null)
{
foreach (élément var dans ViewLocationExpanderValues)
{
hashCodeCombiner.Add(item.Key, StringComparer.Ordinal);
hashCodeCombiner.Add(item.Value, StringComparer.Ordinal);
}
}

return hashCodeCombiner;
```

Nous en avons discuté un peu plus et vous nous avez convaincus :-)

??

L'introduction d'un nouvel espace de noms semble étrange ; System.Numerics peut cependant fonctionner.

Si nous décidons de ne pas ajouter de nouvel espace de noms, il convient de noter que tout code qui a une classe nommée Hash et une directive using System.Numerics échouera à compiler avec une erreur de type ambiguë.

Devrions-nous fournir un modèle de générateur sans allocation pour combiner un nombre inconnu de codes de hachage ?

Cela semble être une excellente idée. Comme quelques suggestions initiales, nous devrions peut-être l'appeler HashBuilder (à la StringBuilder ) et l'avoir return this après chaque méthode Add pour le rendre plus facile pour ajouter des hachages, comme ceci :

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

@jamesqo, veuillez mettre à jour la proposition en haut lorsqu'il y a consensus sur le fil. Nous pouvons alors faire l'examen final. A vous de vous confier pour l'instant pendant que vous pilotez la conception ;-)

Si nous décidons de ne pas ajouter de nouvel espace de noms, il convient de noter que tout code qui a une classe nommée Hash et une directive using System.Numerics échouera à compiler avec une erreur de type ambiguë.

Dépend du scénario réel. Dans de nombreux cas, le compilateur préférera votre type car la hiérarchie d'espace de noms définie de l'unité de compilation est parcourue avant d'envisager d'utiliser des directives.

Mais même ainsi : l'ajout d'API peut être un changement majeur dans la source. Cependant, il n'est pas pratique d'éviter cela, en supposant que nous voulons progresser Nous nous efforçons généralement d'éviter les conflits en utilisant, par exemple, des noms qui ne sont pas trop généraux. Par exemple, je ne pense pas que nous devrions appeler le type Hash . Je pense que HashCode serait probablement mieux.

Comme quelques suggestions initiales, nous devrions peut-être l'appeler HashBuilder

En première approximation, je pensais combiner la statique et le constructeur en un seul type, comme ceci :

``` C#
espace de noms System.Collections.Generic
{
structure publique HashCode
{
public static int Combine(int hash1, int hash2) ;
public static int Combine(int hash1, int hash2, int hash3) ;
public static 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 statique int Combine(int hachage1, int hachage2, int hachage3, int hachage4, int hachage5, int hachage6) ;

    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);

aussi bien que:

``` C#
var hashCode = new HashCode();
hashCode.Combine(IsMainPage ? 1 : 0);
hashCode.Combine (ViewName, StringComparer.Ordinal);
hashCode.Combine(ControllerName, StringComparer.Ordinal);
hashCode.Combine(AreaName, StringComparer.Ordinal);

if (ViewLocationExpanderValues ​​!= null)
{
foreach (élément var dans ViewLocationExpanderValues)
{
hashCode.Combine(item.Key, StringComparer.Ordinal);
hashCode.Combine(item.Value, StringComparer.Ordinal);
}
}

return hashCode.Value;
```

Les pensées?

J'aime l'idée de @jamesqo des appels enchaînés (retourner this des méthodes d'instance Combine ).

J'irais même jusqu'à supprimer complètement les méthodes statiques et ne garder que les méthodes d'instance ...

Combine(long hashCode) sera simplement lancé à int . Voulons-nous vraiment cela ?
Quel est le cas d'utilisation des surcharges de long en premier lieu ?

@karelz Veuillez ne pas les supprimer, les structures ne sont pas gratuites. Les hachages peuvent être utilisés dans des chemins très chauds, vous ne voulez certainement pas gaspiller des instructions alors que la méthode statique serait essentiellement gratuite. Regardez l'analyse du code où j'ai montré l'impact réel de la structure englobante.

Nous avons utilisé la classe statique Hashing pour éviter les conflits de noms et le code semble bon.

@redknightlois Je me demande si nous devrions nous attendre au même "mauvais" code également dans le cas d'une structure non générique avec un champ entier.
S'il s'agit toujours de "mauvais" code assembleur, je me demande si nous pourrions améliorer JIT pour faire un meilleur travail d'optimisation ici. L'ajout d'API juste pour enregistrer quelques instructions devrait être notre dernier recours IMO.

@redknightlois Curieux, le JIT génère-t-il un code pire si la structure (dans ce cas HashCode ) peut tenir dans un registre ? Ce ne sera qu'un gros int .

De plus, j'ai récemment vu beaucoup de demandes d'extraction dans coreclr pour améliorer le code généré autour des structures, et il semble que dotnet/coreclr#8057 permettra ces optimisations. Peut-être que le code généré par le JIT sera meilleur après ce changement ?

edit: je vois que @karelz a déjà mentionné mes points ici.

@karelz , je suis d'accord avec vous - en supposant que le JIT génère un code décent pour une structure de taille int (ce que je pense, ImmutableArray n'a pas de frais généraux par exemple) alors les surcharges statiques sont redondant et peut être supprimé.

@terrajobst Quelques autres idées que j'ai :

  • Je pense que nous pouvons combiner un peu vos et mes idées. HashCode semble être un bon nom ; il n'est pas nécessaire qu'il s'agisse d'une structure mutable suivant le modèle du constructeur. Au lieu de cela, il peut s'agir d'un wrapper immuable autour d'un int , et chaque opération Combine peut renvoyer une nouvelle HashCode . Par example
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);
  • Nous devrions juste avoir un opérateur implicite pour la conversion en int afin que les gens n'aient pas à passer ce dernier .Value .
  • Re Combine , est-ce le meilleur nom ? Cela semble plus descriptif, mais Add est plus court et plus facile à saisir. ( Mix est une autre alternative, mais c'est un peu pénible à taper.)

    • public void Combine(string text, StringComparison comparison) : Je ne pense pas que cela appartienne vraiment au même type, car ce n'est pas lié aux chaînes. De plus, il est assez facile d'écrire StringComparer.XXX.GetHashCode(str) pour les rares fois où vous en avez besoin.

    • Nous devrions supprimer les surcharges longues de ce type et avoir un type HashCode séparé pour les longs. Quelque chose comme Int64HashCode , ou LongHashCode .

J'ai fait un petit exemple d'implémentation de choses sur TryRoslyn : http://tinyurl.com/zej9yux

Heureusement, c'est facile à vérifier. Et la bonne nouvelle c'est qu'il fonctionne correctement tel quel

image

Nous devrions juste avoir un opérateur implicite pour la conversion en int afin que les gens n'aient pas à avoir ce dernier appel .Value.

Le code n'est probablement pas aussi simple, une conversion implicite le nettoierait un peu. J'aime toujours l'idée de pouvoir avoir une interface à plusieurs paramètres aussi.

        [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, c'est le meilleur nom ? Cela semble plus descriptif, mais Add est plus court et plus facile à saisir. (Mix est une autre alternative, mais c'est un peu douloureux à taper.)

Combine est le nom réel qui est utilisé dans la communauté de hachage. Et cela vous donne en quelque sorte une idée claire de ce qu'il fait.

@jamesqo Il existe de nombreuses fonctions de hachage, nous avons dû implémenter des versions très rapides, de 32 bits, 64 bits à 128 bits pour RavenDB (et nous utilisons chacune à des fins différentes).

Nous pouvons penser à l'avenir dans cette conception avec un mécanisme extensible comme celui-ci :

        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;
        }

Je ne sais pas pourquoi le JIT crée un code prologue très ennuyeux pour cela. Cela ne devrait pas, donc cela peut probablement être optimisé, nous devrions demander aux développeurs JIT de le faire. Mais pour le reste, vous pouvez implémenter autant de Combineurs différents que vous le souhaitez sans perdre une seule instruction. Cela dit, cette méthode est probablement plus utile pour les fonctions de hachage réelles que pour les combineurs. cc @CarolEidt @AndyAyersMS

EDIT: Penser à haute voix ici à un mécanisme général pour combiner les fonctions de hachage crypto et non crypto sous un seul concept de hachage.

@jamesqo

il n'est pas nécessaire que ce soit une structure mutable suivant le modèle de constructeur

Ah oui. Dans ce cas, je suis d'accord avec ce modèle. Je n'aime généralement pas le modèle de retour d'instances si l'opération a eu un effet secondaire. C'est particulièrement mauvais si l'API suit le modèle immuable WithXxx . Dans ce cas cependant, le modèle est essentiellement une structure de données immuable, de sorte que ce modèle fonctionnerait correctement.

Je pense que nous pouvons combiner un peu vos et mes idées.

, alors qu'en est-il :

``` C#
structure publique HashCode
{
HashCode statique public Créer(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);
}

ainsi que cette:

``` C#
var hashCode = nouveau HashCode()
.Combine(EstPageMain ? 1 : 0)
.Combine(ViewName, StringComparer.Ordinal)
.Combine(ControllerName, StringComparer.Ordinal)
.Combine(AreaName, StringComparer.Ordinal);

if (ViewLocationExpanderValues ​​!= null)
{
foreach (élément var dans ViewLocationExpanderValues)
{
hashCode = hashCode.Combine(item.Key, StringComparer.Ordinal);
hashCode = hashCode.Combine(item.Value, StringComparer.Ordinal);
}
}

return hashCode.Value;
```

Réflexions de @terrajobst :

  1. La méthode d'usine Create<T> doit être supprimée. Sinon, il y aurait 2 façons d'écrire la même chose, HashCode.Create(_val) ou new HashCode().Combine(_val) . De plus, avoir des noms différents pour Create / Combine ne serait pas compatible avec les différences puisque si vous ajoutiez un nouveau premier champ, vous devriez changer 2 lignes.
  2. Je ne pense pas que la surcharge acceptant une chaîne/StringComparison appartienne ici; HashCode n'a rien à voir avec les chaînes. Au lieu de cela, nous devrions peut-être ajouter une API GetHashCode(StringComparison) à la chaîne ? (Il s'agit également de comparaisons ordinales, ce qui correspond au comportement par défaut de string.GetHashCode .)
  3. Quel est l'intérêt d'avoir Value , s'il existe déjà un opérateur implicite pour la conversion en int ? Encore une fois, cela conduirait à différentes personnes à écrire des choses différentes.
  4. Nous devons déplacer la surcharge long vers un nouveau type. HashCode n'aura qu'une largeur de 32 bits ; ça ne peut pas tenir longtemps.
  5. Ajoutons quelques surcharges prenant des types non signés, car elles sont plus courantes dans le hachage.

Voici ma proposition d'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);
}

Avec uniquement ces méthodes, l'exemple d'ASP.NET peut toujours être écrit sous la forme

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

Quel est l'intérêt d'avoir Value , s'il existe déjà un opérateur implicite pour la conversion en int ? Encore une fois, cela conduirait à différentes personnes à écrire des choses différentes.

Les directives de conception de cadre pour les surcharges d'opérateur disent :

ENVISAGEZ de fournir des méthodes avec des noms conviviaux qui correspondent à chaque opérateur surchargé.

De nombreuses langues ne prennent pas en charge la surcharge des opérateurs. Pour cette raison, il est recommandé que les types qui surchargent les opérateurs incluent une méthode secondaire avec un nom spécifique au domaine approprié qui fournit des fonctionnalités équivalentes.

Plus précisément, F# est l'un des langages qui rend difficile l'invocation d'opérateurs de conversion implicites.


De plus, je ne pense pas qu'avoir une seule façon de faire les choses soit si important. À mon avis, il est plus important de rendre l'API pratique. Si je veux juste combiner des codes de hachage de quelques valeurs, je pense que HashCode.CombineHashCodes(value1, value2, value3) est plus simple, plus court et plus facile à comprendre que new HashCode().Combine(value1).Combine(value2).Combine(value3) .

L'API de méthode d'instance est toujours utile pour les cas plus compliqués, mais je pense que le cas le plus courant devrait avoir l'API de méthode statique plus simple.

@svick , votre remarque sur les autres langages qui ne Value .

Je ne pense pas qu'il soit si important d'avoir une seule façon de faire les choses.

C'est important. Si quelqu'un le fait d'une manière et lit le code d'une personne qui le fait d'une autre manière, alors il/elle devra rechercher sur Google ce que fait l'autre manière.

Si je veux juste combiner des codes de hachage de quelques valeurs, je pense que HashCode.CombineHashCodes(value1, value2, value3) est plus simple, plus court et plus facile à comprendre que le nouveau HashCode().Combine(value1).Combine(value2).Combine( valeur3).

  • Le problème avec une méthode statique est que, puisqu'il n'y aura pas params int[] surcharge
  • La deuxième forme sera facile à comprendre une fois que vous la verrez une ou deux fois. En fait, on pourrait dire que c'est plus lisible, car il est plus facile de chaîner verticalement (et minimise ainsi les différences lorsqu'un champ est ajouté/supprimé) :
public override int GetHashCode()
{
    return new HashCode()
        .Combine(_field1)
        .Combine(_field2)
        .Combine(_field3)
        .Combine(_field4);
}

[@svick] Je ne pense pas qu'il soit si important d'avoir une seule façon de faire les choses.

Je pense qu'il est important de minimiser le nombre de façons dont vous pouvez faire la même chose car cela évite toute confusion. Dans le même temps, notre objectif n'est pas d'être 100 % sans chevauchement si cela aide à atteindre d'autres objectifs, tels que la découvrabilité, la commodité, les performances ou la lisibilité. En général, notre objectif est de minimiser les concepts, plutôt que les API. Par exemple, les surcharges multiples sont moins problématiques que d'avoir plusieurs méthodes différentes avec une terminologie disjointe.

La raison pour laquelle j'ai ajouté la méthode d'usine est de préciser comment obtenir un code de hachage initial. Créer la structure vide suivie de Combine ne semble pas très intuitif. La chose logique serait d'ajouter .ctor mais afin d'éviter la boxe, il devrait être générique, ce que vous ne pouvez pas faire avec un .ctor. Une méthode d'usine générique est la deuxième meilleure chose.

Un effet secondaire intéressant est qu'il ressemble beaucoup à l'apparence des structures de données immuables dans le framework. Et dans la conception d'API, nous privilégions fortement la cohérence par rapport à presque tout le reste.

[@svick] Si je veux juste combiner des codes de hachage de quelques valeurs, je pense que HashCode.CombineHashCodes(value1, value2, value3) est plus simple, plus court et plus facile à comprendre que le nouveau HashCode().Combine(value1).Combine(value2 ).Combine(valeur3).

Je suis d'accord avec @jamesqo : ce que j'aime dans le modèle de constructeur qu'il adapte à un nombre arbitraire d'arguments avec une pénalité de performance minimale (le cas échéant, en fonction de la qualité de notre inliner).

[@jamesqo] Je ne pense pas que la surcharge acceptant une chaîne/StringComparison appartienne ici; HashCode n'a rien à voir avec les chaînes

Point juste. Je l'ai ajouté car il était référencé dans le code de @Eilon . Par expérience, je dirais que les cordes sont super courantes. D'un autre côté, je ne suis pas sûr que spécifier une comparaison le soit. Laissons cela de côté pour le moment.

[@jamesqo] Nous devons déplacer la surcharge longue vers un nouveau type. HashCode n'aura qu'une largeur de 32 bits ; ça ne peut pas tenir longtemps.

C'est un bon point. Avons-nous même besoin d'une version long ? Je ne l'ai laissé que parce qu'il a été mentionné ci-dessus et je n'y ai pas vraiment pensé.

Maintenant que je le suis, il semble que nous ne devrions laisser que 32 bits parce que c'est ce qu'est le .NET GetHashCode() . Dans cette veine, je ne suis même pas sûr que nous devrions ajouter la version uint . Si vous utilisez le hachage en dehors de ce domaine, je pense qu'il est correct de diriger les gens vers les algorithmes de hachage plus généraux que nous avons dans System.Security.Cryptography .

```C#
structure publique HashCode
{
HashCode statique public Créer(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);

}
```

Maintenant que je le suis, il semble que nous ne devrions laisser que 32 bits, car c'est l'objet du .NET GetHashCode(). Dans cette veine, je ne suis même pas sûr que nous devrions ajouter la version uint. Si vous utilisez le hachage en dehors de ce domaine, je pense qu'il est acceptable de diriger les gens vers les algorithmes de hachage plus généraux que nous avons dans System.Security.Cryptography.

@terrajobst Il existe des types d'algorithmes de hachage très différents, un vrai zoo. En fait, probablement 70% ne sont pas cryptographiques par conception. Et probablement plus de la moitié d'entre eux sont conçus pour gérer plus de 64 bits (la cible commune est 128/256). Je parie que le framework a décidé d'utiliser du 32 bits (je n'y suis pas allé) parce qu'à l'époque, x86 était encore un gros consommateur et les hachages sont utilisés partout, donc les performances sur du matériel moins puissant étaient primordiales.

Aussi, pour être strict, la plupart des fonctions de hachage sont vraiment définies sur le domaine uint , et non sur le int car les règles de décalage sont différentes. En fait, si vous vérifiez le code que j'ai posté auparavant, le int est immédiatement converti en uint cause de cela (et utilisez l'optimisation ror/rol ). Au cas où, si nous voulons être stricts, le seul hachage devrait être uint , cela peut être considéré comme un oubli que le framework renvoie int sous cette lumière.

Restreindre cela à int n'est pas mieux que ce que nous avons aujourd'hui. Si c'était mon appel, je pousserais l'équipe de conception à examiner comment nous pourrions prendre en charge les variantes 128 et 256 et différentes fonctions de hachage (même si nous lancerions une alternative ne me faisant pas réfléchir sous vos empreintes digitales).

Les problèmes causés par la simplification excessive sont parfois pires que les problèmes de conception introduits lorsqu'ils sont obligés de traiter des choses complexes. Simplifier les fonctionnalités à un tel degré parce que les développeurs sont perçus comme not being able to deal with having multiple options peut facilement conduire à la voie de l'état actuel de SIMD. La plupart des développeurs soucieux des performances ne peuvent pas l'utiliser, et tout le monde ne l'utilisera pas non plus, car la plupart ne traitent pas de toute façon des applications sensibles aux performances qui ont des objectifs de débit aussi fins.

Le cas du hachage est similaire, les domaines où vous utiliseriez du 32 bits sont très restreints (la plupart sont déjà couverts par le framework lui-même), pour le reste vous n'avez pas de chance.

image

De plus, dès que vous devez gérer plus de 75 000 éléments, vous avez 50% de chances d'avoir une collision, ce qui est mauvais dans la plupart des scénarios (et cela en supposant que vous ayez une fonction de hachage bien conçue). C'est pourquoi 64 bits et 128 bits sont ainsi utilisés en dehors des limites des structures d'exécution.

Avec un design collé sur int nous ne couvrons que les problèmes causés par le fait de ne pas avoir le journal du lundi en 2000 (donc maintenant tout le monde écrit son pauvre hachage par lui-même) mais nous n'avancerons même pas d'un pas l'état du l'art non plus.

C'est mes 2 cents dans la discussion.

@redknightlois , je pense que nous comprenons les limites des hachages int. Mais je suis d'accord avec @terrajobst : cette fonctionnalité devrait concerner les API pour calculer les hachages dans le but de les renvoyer à partir des substitutions Object.GetHashCode. Nous pourrions en outre avoir une bibliothèque distincte pour un hachage plus moderne, mais je dirais que cela devrait être une discussion distincte, car elle doit inclure la décision de savoir quoi faire avec Object.GetHashCode et toutes les structures de données de hachage existantes.

À moins que vous ne pensiez qu'il est toujours avantageux de combiner des hachages en 128 bits, puis de les convertir en int afin que le résultat puisse être renvoyé à partir de GetHahsCode.

@KrzysztofCwalina Je suis d'accord pour dire qu'il s'agit de deux approches différentes. L'une consiste à résoudre un problème causé sur 2000 ; un autre est de s'attaquer au problème général de hachage. Si nous sommes tous d'accord que c'est une solution pour le premier, la discussion est terminée. Cependant, pour une discussion sur la conception d'un jalon « Futur », j'ai le sentiment qu'elle échouera, principalement parce que ce que nous ferons ici aura un impact sur la discussion future. Faire des erreurs ici, aura un impact.

@redknightlois , je proposerais la

@redknightlois

Faire des erreurs ici, aura un impact.

Je pense que si, à l'avenir, nous voulons prendre en charge des scénarios plus avancés, nous pouvons simplement le faire dans un type distinct de HashCode . Les décisions ici ne devraient pas vraiment avoir d'impact sur ces cas.

J'ai créé un problème différent pour commencer à m'y attaquer.

@redknightlois :+1:. Au fait, vous avez répondu avant que je puisse modifier mon commentaire, mais j'ai en fait essayé votre idée (ci-dessus) de faire fonctionner le hachage avec n'importe quel type (int, long, décimal, etc.) et d'encapsuler la logique de hachage de base dans une structure : https://github.com/jamesqo/HashApi (l'exemple d'utilisation était ici ). Mais, avoir deux paramètres de type générique a fini par être beaucoup trop complexe, et l'inférence de type du compilateur a fini par ne pas fonctionner lorsque j'ai essayé d'utiliser l'API. Alors oui, c'est une bonne idée de faire un hachage plus avancé dans un problème distinct pour le moment.

@terrajobst L'API semble presque prête, mais il y a 1 ou 2 autres choses que j'aimerais changer.

  • Au départ, je ne voulais pas de la méthode d'usine statique, car HashCode.Create(x) a le même effet que new HashCode().Combine(x) . Mais j'ai changé d'avis à ce sujet car cela signifie 1 hachage supplémentaire. Au lieu de cela, pourquoi ne pas renommer Create en Combine ? Il semble assez ennuyeux d'avoir à taper une chose pour le premier champ et une autre pour le deuxième champ.
  • Je pense que nous devrions avoir HashCode implémenter IEquatable<HashCode> et implémenter certains des opérateurs d'égalité. N'hésitez pas à me faire savoir si vous avez une objection.

(Espérons-le) proposition finale :

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 a dit :

Point juste. Je l'ai ajouté car il était référencé dans le code de @Eilon . Par expérience, je dirais que les cordes sont super courantes. D'un autre côté, je ne suis pas sûr que spécifier une comparaison le soit. Laissons cela de côté pour le moment.

C'est en fait super important : créer des hachages pour les chaînes implique souvent de prendre en compte le but de cette chaîne, qui implique à la fois sa culture et sa sensibilité à la casse. Le StringComparer ne concerne pas les comparaisons en soi, mais plutôt la fourniture d'implémentations GetHashCode spécifiques tenant compte de la culture et de la casse.

Sans cette API, vous auriez besoin de faire quelque chose de bizarre comme :

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

Et cela regorge d'allocations, suit des modèles de sensibilité culturelle médiocres, etc.

@Eilon dans ce cas, je m'attendrais à ce que le code appelle explicitement string.GetHashCode(StringComparison comparison) qui est sensible à la culture/à la casse et passe le résultat sous la forme int dans Combine .

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

@Eilon , vous pouvez simplement utiliser StringComparer.InvariantCultureIgnoreCase.GetHashCode.

Ceux-ci sont certainement meilleurs en termes d'allocations, mais ces appels ne sont pas jolis à regarder... Nous avons des utilisations partout dans ASP.NET où les hachages doivent inclure des chaînes tenant compte de la culture et de la casse.

Assez juste, en combinant tout ce qui a été dit ci-dessus, que diriez-vous de cette forme alors :

``` C#
espace de noms System.Collections.Generic
{
structure publique HashCode : IEquatable
{
public static HashCode Combine(int hash);
Combinaison de code de hachage statique public(T obj);
public static HashCode Combine (texte de chaîne, comparaison 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();
}

}

// Utilisation :

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

expédier! :-)

@terrajobst _Attendez--_ Combine(string, StringComparison) ne peut-il pas être simplement implémenté en tant que méthode d'extension ?

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:
                ...
        }
    }
}

Je préférerais de loin que ce soit une méthode d'extension plutôt qu'une partie de la signature de type. Cependant, si vous ou @Elion pensez absolument que cela devrait être une méthode intégrée, je ne bloquerai pas cette proposition.

( modifier : System.Numerics est probablement un meilleur espace de noms, à moins que nous ayons des types liés au hachage dans Collections.Generic aujourd'hui dont je ne suis pas au courant.)

LGTM. J'irais en prolongation.

Oui, cela pourrait être une méthode d'extension, mais quel problème résout-elle ?

@terrajobst

Oui, cela pourrait être une méthode d'extension, mais quel problème résout-elle ?

Je suggérais dans le code ASP.NET. Si c'est courant pour leur cas d'utilisation, alors c'est bien, mais cela peut ne pas être vrai pour d'autres bibliothèques/applications. S'il s'avère que cela est assez courant plus tard, nous pouvons toujours réévaluer et décider de l'ajouter dans une proposition distincte.

Mhhh c'est le noyau de toute façon. Une fois défini, il fera de toute façon partie de la signature. Supprimez le commentaire. C'est bien comme ça.

L'utilisation de méthodes d'extension est utile dans les cas où :

  1. c'est un type existant que nous aimerions augmenter sans avoir à envoyer une mise à jour du type lui-même
  2. résoudre les problèmes de superposition
  3. séparer les API super courantes des API beaucoup moins utilisées.

Je ne pense pas que (1) ou (2) s'appliquent ici. (3) n'aiderait que si nous déplaçons le code vers un autre assembly que HashCode ou si nous le déplaçons vers un autre espace de noms. Je dirais que les chaînes sont suffisamment courantes pour que cela n'en vaut pas la peine. En fait, je dirais même qu'ils sont si courants qu'il est plus logique de les traiter comme de première classe que d'essayer de les séparer artificiellement sur un type d'extension.

@terrajobst , pour être clair, je suggérais d'abandonner complètement l'API string et de laisser ASP.NET rédiger sa propre méthode d'extension pour les chaînes.

Je dirais que les chaînes sont suffisamment courantes pour que cela n'en vaut pas la peine. En fait, je dirais même qu'ils sont si courants qu'il est plus logique de les traiter comme de première classe que d'essayer de les séparer artificiellement sur un type d'extension.

Oui, mais dans quelle mesure est-il courant pour quelqu'un de vouloir obtenir le code de hachage non ordinal d'une chaîne, qui est le seul scénario dont la surcharge Combine<T> existante ne prend pas en compte ? (Par exemple, quelqu'un qui appelle StringComparer.CurrentCulture.GetHashCode dans ses dérogations ?) Je me trompe peut-être, mais je n'en ai pas vu beaucoup.

Désolé pour le recul à ce sujet ; c'est juste qu'une fois qu'une API est ajoutée, il n'y a plus de retour en arrière.

oui, mais est-il courant pour quelqu'un de vouloir obtenir le code de hachage non ordinal d'une chaîne

Je suis peut-être biaisé, mais l'invariance de cas est assez populaire. Bien sûr, peu (le cas échéant) se soucient des codes de hachage spécifiques à la culture, mais des codes de hachage qui ignorent la casse, je peux totalement le voir - et cela semble être ce que @Eilon recherche (ce qui est StringComparison.OrdinalIgnoreCase ).

Désolé pour le recul à ce sujet ; c'est juste qu'une fois qu'une API est ajoutée, il n'y a plus de retour en arrière.

Sans blague 😈 D'accord, mais même si l'API n'est pas autant utilisée, elle est utile et ne cause aucun dommage.

@terrajobst Ok, ajoutons-le :+1 : Dernier problème : j'ai mentionné cela ci-dessus, mais pouvons-nous créer l'espace de noms Numerics plutôt que Collections.Generic ? Si nous devions ajouter plus de types liés au hachage à l'avenir, comme le suggère @redknightlois , je pense qu'ils seraient quelque peu inappropriés dans Collections.

J'aime ça. ??

Je ne pense pas que Hashing tombe conceptuellement dans les collections. Qu'en est-il de System.Runtime ?

J'allais suggérer la même chose, ou même System. Ce n'est pas non plus du numérique.

@karelz , System.Runtime pourrait fonctionner. @redknightlois System serait pratique, car il est probable que vous ayez déjà importé cet espace de noms. Je ne sais pas si cela serait approprié, cependant (encore une fois, si plus de types de hachage sont ajoutés).

Nous ne devrions pas le mettre dans System.Runtime car c'est pour des cas ésotériques et assez spécialisés. J'ai parlé à @KrzysztofCwalina et nous pensons tous les deux que c'est l'un des deux :

  • System
  • System.Collections.*

Nous penchons tous les deux vers System .

Si ce dont nous avons besoin, c'est d'une justification pour opter pour System je peux essayer une justification. Nous avons construit HashCode pour aider dans les implémentations de object.GetHashCode() , il semble approprié que les deux partagent l'espace de noms.

@terrajobst Je pense que System devrait être l'espace de noms, alors. Allons :expédier:

Mise à jour de la spécification API dans la description.

[@redknightlois] Si nous avons besoin d'une justification pour choisir System je peux essayer une justification. Nous avons construit HashCode pour aider dans les implémentations de object.GetHashCode() , il semble approprié que les deux partagent l'espace de noms.

C'est la justification que @KrzysztofCwalina et moi avons également utilisée. Vendu!

@jamesqo

Je suppose que vous souhaitez également fournir au PR la mise en œuvre ?

@terrajobst Oui, définitivement. Merci d'avoir pris le temps de revoir ça !

Oui définitivement.

Doux. Dans ce cas, je vous le laisse. C'est bien avec toi @karelz ?

Merci d'avoir pris le temps de revoir ça !

Merci d'avoir pris le temps de travailler avec nous sur la forme de l'API. Les allers-retours peuvent être un processus douloureux. Nous apprécions grandement votre patience!

Et j'ai hâte de supprimer l'implémentation ASP.NET Core et de l'utiliser à la place 😄

public static HashCode Combine (texte de chaîne, comparaison StringComparison);
public HashCode Combine (texte de chaîne, comparaison StringComparison);

Nit : Les méthodes sur String qui prennent StringComparison (par exemple Equals , Compare , StartsWith , EndsWith , etc. .) utilisez comparisonType comme nom du paramètre, et non comparison . Le paramètre devrait-il également être nommé comparisonType ici pour être cohérent ?

@justinvp , cela ressemble plus à un défaut de nommage dans les méthodes de String; Type est redondant. Je ne pense pas que nous devrions rendre les noms de paramètres dans les nouvelles API plus détaillés juste pour "suivre les précédents" avec les anciennes.

Comme autre point de données, xUnit a également choisi d'utiliser comparisonType .

@justinvp Vous m'avez convaincu. Maintenant que j'y pense intuitivement, « insensible à la casse » ou « dépendant de la culture » ​​est un « type » de comparaison. Je vais changer le nom.

Je suis d'accord avec la forme de ceci, mais en ce qui concerne StringComparison, une alternative possible :

Ne pas inclure :

``` C#
public static HashCode Combine (texte de chaîne, comparaison StringComparison);
public HashCode Combine (texte de chaîne, comparaison StringComparison);

Instead, add a method:

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

Alors au lieu d'écrire :

``` C#
public override int GetHashCode()
{
return 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));
}

Oui, c'est un peu plus long, mais cela résout le même problème sans avoir besoin de deux méthodes spécialisées sur HashCode (que nous venons de promouvoir en System), et vous obtenez une méthode d'assistance statique qui peut être utilisée dans d'autres situations sans rapport. Il reste également similaire à la façon dont vous l'utiliseriez si vous avez déjà un StringComparer (puisque nous ne parlons pas de surcharges de comparateur):

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

@stephentoub , FromComparison semble être une bonne idée. J'ai en fait proposé vers le haut dans le fil d'ajouter une api string.GetHashCode(StringComparison) , ce qui rend votre exemple encore plus simple (en supposant une chaîne non nulle) :

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

@Elion a cependant déclaré avoir ajouté trop d'appels.

(edit: fait une proposition pour votre api.)

Je n'aime pas non plus ajouter 2 méthodes spécialisées sur HashCode pour la chaîne.
@Eilon, vous avez mentionné que le modèle est utilisé dans ASP.NET Core lui-même. Dans quelle mesure pensez-vous que les développeurs externes l'utiliseront ?

@jamesqo merci d'avoir @terrajobst , nous apprécions votre aide et votre patience. L'itération des petites API fondamentales peut parfois prendre un certain temps :).

Voyons où nous atterrissons avec ce dernier retour d'API, puis nous pourrons poursuivre la mise en œuvre.

Devrait-il y avoir un :

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

?

(Mes excuses si cela a déjà été rejeté et qu'il me manque ici).

@stephentoub a dit :

écrivez:

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

Oui, c'est un peu plus long, mais cela résout le même problème sans avoir besoin de deux méthodes spécialisées sur HashCode (que nous venons de promouvoir en System), et vous obtenez une méthode d'assistance statique qui peut être utilisée dans d'autres situations sans rapport. Il reste également similaire à la façon dont vous l'utiliseriez si vous avez déjà un StringComparer (puisque nous ne parlons pas de surcharges de comparateur):


Eh bien, ce n'est pas seulement un peu plus long, c'est comme waaay super plus long, et n'a aucune possibilité de découverte.

Quelle est la résistance à l'ajout de cette méthode? S'il est utile, peut être clairement mis en œuvre correctement, n'a aucune ambiguïté dans ce qu'il fait, pourquoi ne pas l'ajouter ?

Avoir la méthode d'aide/de conversion statique supplémentaire est bien - bien que je ne sois pas sûr de l'utiliser - mais pourquoi au détriment des méthodes pratiques ?

pourquoi au détriment des méthodes de commodité ?

Parce que ce n'est pas clair pour moi, des méthodes de commodité sont vraiment nécessaires ici. Je comprends qu'ASP.NET le fait à divers endroits. Combien d'endroits ? Et dans combien de ces endroits s'agit-il en fait d'une variable StringComparison plutôt que d'une valeur connue ? Dans ce cas, vous n'avez même pas besoin de l'aide que j'ai mentionnée et vous pourriez simplement faire :

``` C#
.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);

et est en fait plus rapide, car nous n'avons pas besoin de créer une branche dans Combine pour faire exactement la même chose que le développeur aurait pu écrire. Le code supplémentaire est-il si gênant qu'il vaut la peine d'ajouter des surcharges spécialisées pour ce cas particulier ? Pourquoi pas des surcharges pour StringComparer ? Pourquoi pas des surcharges pour EqualityComparer ? Pourquoi pas des surcharges qui prennent un Func<T, int> ? À un moment donné, vous tracez la ligne et dites "la valeur fournie par cette surcharge n'en vaut tout simplement pas la peine", car tout ce que nous ajoutons a un coût, que ce soit le coût de la maintenance, le coût de la taille du code, le coût de quoi que ce soit , et si le développeur a vraiment besoin de ce cas, c'est très peu de code supplémentaire à gérer avec moins de cas spécialisés. Je suggérais donc que le bon endroit pour tracer la ligne est avant ces surcharges plutôt qu'après (mais comme je l'ai dit au début de ma réponse précédente, "Je suis d'accord avec la forme de ceci", et je suggérais une alternative) .

Voici la recherche que j'ai effectuée : https://github.com/search?p=2&q=user%3Aaspnet+hashcodecombiner&type=Code&utf8=%E2%9C%93

Sur environ 100 correspondances, même à partir des premières pages, presque tous les cas d'utilisation ont des chaînes et, dans plusieurs cas, utilisent différents types de comparaisons de chaînes :

  1. Ordinal : https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperAttributeDescriptorComparer.cs#L46
  2. Ordinal + IgnoreCase : https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperRequiredAttributeDescriptorLCom
  3. Ordinal : https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Chunks/Generators/AttributeBlockChunkGenerator.cs#L58
  4. Ordinal : https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperDesignTimeDescriptorComparer.cs#L41
  5. Ordinal : https://github.com/aspnet/Razor/blob/dbcb6901209859e471c9aa978912cf7d6c178668/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AttributeBlockChunkGenerator.cs#L56
  6. Ordinal : https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/CaseSensitiveTagHelperDescriptor#L62Comparer.
  7. Ordinal + IgnoreCase : https://github.com/aspnet/dnx/blob/bebc991012fe633ecac69675b2e892f568b927a5/src/Microsoft.Dnx.Tooling/NuGet/Core/PackageSource/PackageSource.cs#L107
  8. Ordinal : https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Tokenizer/Symbols/SymbolBase.cs#L52
  9. Ordinal : https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/CaseSensitiveTagHelperAttributeComparer.cs
  10. Ordinal : https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperAttributeDesignTimeDescriptorL42Comparer.cs

(Et des dizaines d'autres.)

Il semble donc que dans la base de code ASP.NET Core, il s'agisse d'un modèle extrêmement courant. Bien sûr, je ne peux parler à aucun autre système.

Sur ~100 correspondances

Chacun des 10 que vous avez énumérés (je n'ai pas regardé le reste de la recherche) spécifie explicitement la comparaison de chaînes, plutôt que de la tirer d'une variable, alors ne parlons-nous pas simplement de la différence entre, par exemple :

``` C#
.Combine(Nom, Comparaison des Chaînes.OrdinalIgnoreCase)

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

? Ce n'est pas "waaay super long" et c'est plus efficace, à moins que quelque chose ne me manque.

Quoi qu'il en soit, comme je l'ai dit, je suggère simplement que nous examinions vraiment si ces surcharges sont nécessaires. Si la plupart des gens pensent qu'ils le sont, et que nous ne considérons pas seulement notre propre base de code ASP.NET, très bien.

Connexe, quel est le comportement que nous prévoyons pour les entrées nulles ? Qu'en est-il int==0 ? Je peux commencer à voir plus d'avantages à la surcharge de chaîne si nous permettons à null d'être transmis, car je pense que StringComparer.GetHashCode lance généralement une entrée null, donc si cela est vraiment courant, cela commence à devenir plus lourd si l'appelant a aux cas spéciaux nuls. Mais cela soulève alors également la question de savoir quel sera le comportement lorsque null est fourni. Un 0 est-il mélangé au code de hachage comme avec toute autre valeur ? Est-il traité comme un nop et le hashcode est-il laissé seul ?

Je pense que la meilleure approche générale de null est de mélanger dans un zéro. Pour un seul élément nul ajouté, il serait préférable de l'avoir comme nop, mais si quelqu'un se nourrit dans une séquence, il devient alors plus avantageux d'avoir 10 hachage nuls différemment de 20.

En effet, mon vote vient du point de vue de la base de code d'ASP.NET Core, où une surcharge tenant compte des chaînes serait très utile. Les choses sur la longueur des lignes n'étaient pas vraiment ma principale préoccupation, mais plutôt sur la découvrabilité.

Si une surcharge sensible aux chaînes n'était pas disponible dans le système, nous ajouterions simplement une méthode d'extension interne dans ASP.NET Core et l'utiliserions.

Si une surcharge sensible aux chaînes n'était pas disponible dans le système, nous ajouterions simplement une méthode d'extension interne dans ASP.NET Core et l'utiliserions.

Je pense que ce serait une excellente solution pour le moment, jusqu'à ce que nous voyions plus de preuves qu'une telle API est nécessaire en général, également en dehors de la base de code ASP.NET Core.

Je dois dire que je ne vois pas l'intérêt de supprimer la surcharge string . Cela ne réduit aucune complexité, cela ne rend pas le code plus efficace et cela ne nous empêche pas d'améliorer d'autres domaines, comme fournir une méthode qui renvoie un StringComparer partir d'un StringComparison . Le sucre syntaxique _est_ important, car .NET a toujours eu pour objectif de simplifier les cas courants. Nous voulons également guider le développeur pour qu'il fasse ce qu'il faut et tombe dans le gouffre du succès.

Nous devons comprendre que les cordes sont spéciales et incroyablement courantes. En ajoutant une surcharge qui les spécialise on obtient deux choses :

  1. Nous rendons des scénarios comme @Eilon beaucoup plus faciles.
  2. Nous faisons découvrir qu'il est important de considérer la comparaison des cordes, en particulier l'enveloppe.

Nous devons également considérer que les aides standard comme la méthode d'extension

Cependant, si la principale préoccupation concerne le boîtier spécial string , que diriez-vous de ceci :

``` C#
structure publique HashCode : IEquatable
{
Combinaison de code de hachage public(T obj, IEqualityComparercomparateur);
}

// Utilisation
return HashCode.Combine(_numberField)
.Combine(_stringField, StringComparer.OrdinalIgnoreCase);
```

@terrajobst , votre compromis est intelligent. J'aime le fait que vous n'ayez plus à appeler GetHashCode explicitement ou à imbriquer un ensemble supplémentaire de parenthèses avec un comparateur personnalisé.

(edit: je suppose que je devrais vraiment le créditer à @JonHanna puisqu'il l'a mentionné plus tôt dans le fil? 😄 )

@JonHanna Oui, nous allons également hacher les entrées nulles en tant que 0.

Désolé d'avoir interrompu la conversation ici. Mais, où dois-je mettre le nouveau type ? @mellinoe @ericstj @weshaggard , me suggérez-vous de créer un nouvel assembly/package pour ce type comme System.HashCode , ou devrais-je l'ajouter à un assembly existant comme System.Runtime.Extensions ? Merci.

Nous avons récemment remanié un peu la disposition de l'assembly dans .NET Core ; Je suggère de le mettre là où vivent les comparateurs concrets, qui semblent indiquer System.Runtime.Extensions .

@weshaggard ?

@terrajobst En ce qui concerne la proposition elle-même, je viens de découvrir que nous ne pouvons malheureusement pas nommer à la fois les surcharges statiques et d'instances Combine . ??

Les résultats suivants entraînent une erreur du compilateur, car les méthodes d'instance et statiques ne peuvent pas avoir les mêmes noms :

using System;
using System.Collections.Generic;

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

    public static void Combine(int i)
    {
    }
}

Maintenant, nous avons 2 options :

  • Renommez les surcharges statiques en quelque chose de différent comme Create , Seed , etc.
  • Déplacez les surcharges statiques vers une autre classe statique :
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);

Je suis préférentiel envers le second. C'est malheureux que nous devions contourner ce problème, mais... des pensées ?

Séparer la logique en 2 types me semble étrange - pour utiliser HashCode vous devez établir la connexion et commencer par la classe Hash la place.

Je préfère ajouter la méthode Create (ou Seed ou Init ).
J'ajouterais également la surcharge no-args HashCode.Create().Combine(_field1).Combine(_field2) .

@karelz , je ne pense pas que nous devrions ajouter une méthode d'usine si ce n'est pas le même nom. Nous devrions simplement proposer le constructeur sans paramètre, new , car il est plus naturel. De plus, on ne peut pas empêcher les gens d'écrire new HashCode().Combine car c'est une structure.

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

Cela fait une combinaison supplémentaire avec 0 et le code de hachage de _field1 , au lieu d'initialiser directement à partir du code de hachage. Cependant, un effet secondaire du hachage actuel que nous utilisons est que 0 est transmis comme premier paramètre, il sera tourné à zéro et ajouté à zéro. Et lorsque 0 est xoré avec le premier code de hachage, il ne produira que le premier code de hachage. Donc, si le JIT est bon pour un repliement constant (et je pense qu'il optimise ce xor), cela devrait en fait être équivalent à une initialisation directe.

API proposée (spécification mise à jour) :

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 , avez-vous un avis sur une méthode d'usine vs utiliser le constructeur par défaut ? J'ai découvert que le compilateur n'autorisait pas une surcharge statique de Combine car cela entre en conflit avec les méthodes d'instance, nous avons donc la possibilité de

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

// or, using default constructor

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

L'avantage du premier est qu'il est un peu plus rapide. L'avantage du second est qu'il aura un nommage cohérent afin que vous n'ayez pas à écrire quelque chose de différent pour le premier champ.

Une autre possibilité est deux types différents, l'un avec l'usine Combine , l'autre avec l'instance Combine (ou le second en tant qu'extension sur le premier type).

Je ne sais pas ce que je préférerais TBH.

@JonHanna , votre deuxième idée avec les surcharges d'instances étant des méthodes d'extension sonne bien. Cela dit, hc.Combine(obj) dans ce cas essaie de détecter la surcharge statique :

J'ai proposé d'avoir une classe statique comme point d'entrée quelques commentaires ci-dessus, ce qui me rappelle... @karelz , vous avez dit

Séparer la logique en 2 types me semble étrange - pour utiliser HashCode, vous devez établir la connexion et commencer par la classe Hash à la place.

Quel lien les gens devraient-ils faire ? Ne leur présenterions-nous pas d'abord Hash , puis de là, ils pourront se diriger vers HashCode ? Je ne pense pas que l'ajout d'une nouvelle classe statique serait un problème.

Séparer la logique en 2 types me semble étrange - pour utiliser HashCode, vous devez établir la connexion et commencer par la classe Hash à la place.

Nous pourrions conserver le type de niveau supérieur HashCode et simplement imbriquer la structure. Cela permettrait l'utilisation souhaitée tout en gardant le "point d'entrée" de l'API à un type de niveau supérieur, par exemple :

``` c#
système d'espace de noms
{
HashCode de classe statique publique
{
public static HashCodeValue Combine(int hash);
Combiner HashCodeValue statique publique(T obj);
Combiner HashCodeValue statique publique(T obj, IEqualityComparercomparateur);

    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();
    }
}

}
```

Edit : bien que nous ayons probablement besoin d'un meilleur nom que HashCodeValue pour le type imbriqué si nous suivons ce chemin car HashCodeValue.Value est un peu redondant, pas que Value serait très utilisé souvent. Peut-être que nous n'avons même pas besoin d'une propriété Value -- vous pouvez obtenir le Value via GetHashCode() si vous ne voulez pas caster sur int .

@justinvp Quel est le problème d'avoir deux types distincts en premier lieu, cependant? Ce système semble fonctionner correctement pour LinkedList<T> et LinkedListNode<T> , par exemple.

Quel est le problème d'avoir deux types distincts en premier lieu, cependant?

Il y a deux problèmes avec deux types de niveau supérieur :

  1. Quel type est le "point d'entrée" pour l'API ? Si les noms sont Hash et HashCode , par lequel commencez-vous ? Ce n'est pas clair à partir de ces noms. Avec LinkedList<T> et LinkedListNode<T> il est assez clair lequel est le point d'entrée principal, LinkedList<T> , et lequel est une aide.
  2. Pollution de l'espace System noms System noms

L'imbrication aide à atténuer ces problèmes.

@justinvp

Quel type est le "point d'entrée" pour l'API ? Si les noms sont Hash et HashCode, par lequel commencez-vous ? Ce n'est pas clair à partir de ces noms. Avec liste liéeet LinkedListNodeil est assez clair lequel est le point d'entrée principal, LinkedList, et qui est une aide.

OK, assez juste. Et si nous nommions les types Hash et HashValue , pas les types d'imbrication ? Cela dénoterait-il suffisamment une relation de subjugation entre les deux types ?

Si nous le faisons, la méthode d'usine devient encore plus laconique : Hash.Combine(field1).Combine(field2) . De plus, l'utilisation du type struct en lui-même est toujours pratique. Par exemple, quelqu'un peut vouloir collecter une liste de hachages et pour le communiquer au lecteur, un List<HashValue> est utilisé au lieu d'un List<int> . Cela pourrait ne pas fonctionner aussi bien si nous rendions le type imbriqué : List<HashCode.HashCodeValue> (même List<Hash.Value> est un peu déroutant à première vue).

Pollution de l'espace de noms système. Ce n'est pas aussi préoccupant que (1), mais quelque chose à garder à l'esprit lorsque nous envisageons d'exposer de nouvelles fonctionnalités dans l'espace de noms System.

Je suis d'accord, mais je pense aussi qu'il est important de respecter les conventions et de ne pas sacrifier la facilité d'utilisation. Par exemple, les seules API BCL auxquelles je peux penser où nous avons des types imbriqués (les collections immuables ne comptent pas, elles ne font pas strictement partie du framework) est List<T>.Enumerator , où nous voulons activement masquer le imbriqué type car il est destiné à être utilisé par le compilateur. Nous ne voulons pas faire cela dans ce cas.

Peut-être que nous n'avons même pas besoin d'une propriété Value -- vous pouvez obtenir la valeur via GetHashCode() si vous ne voulez pas transtyper en int.

J'y ai pensé plus tôt. Mais alors comment l'utilisateur saura-t-il que le type remplace GetHashCode , ou qu'il a un opérateur implicite ?

API proposée

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();
}

Et si nous nommions les types Hash et HashValue, pas les types d'imbrication ?

Hash me semble un nom bien trop général. Je pense que nous devons avoir HashCode dans le nom de l'API du point d'entrée car son objectif est d'aider à implémenter GetHashCode() , pas GetHash() .

quelqu'un pourrait vouloir collecter une liste de hachages et la communiquer au lecteur une listeest utilisé à la place d'une liste. Cela pourrait ne pas fonctionner aussi bien si nous rendions le type imbriqué : Liste(même listeest un peu déroutant à première vue).

Cela semble être un cas d'utilisation improbable - pas sûr que nous devions optimiser la conception pour cela.

les seules API BCL auxquelles je peux penser où nous avons des types imbriqués

TimeZoneInfo.AdjustmentRule et TimeZoneInfo.TransitionTime sont des exemples dans la BCL qui ont été intentionnellement ajoutés en tant que types imbriqués.

@justinvp

Je pense que nous devons avoir HashCode dans le nom de l'API du point d'entrée car son objectif est d'aider à implémenter GetHashCode(), pas GetHash().

Je vois.

J'ai réfléchi un peu plus aux choses. Il semble raisonnable d'avoir une structure imbriquée ; comme vous l'avez mentionné, la plupart des gens ne verront jamais le type réel. Juste une chose : je pense que le type devrait s'appeler Seed , plutôt que HashCodeValue . Le contexte de son nom est déjà impliqué par la classe conteneur.

API proposée

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 Une objection ou un problème de mise en œuvre avec public readonly int Value place ? Le problème avec Seed est qu'il ne s'agit pas techniquement d'une graine après la première moissonneuse-batteuse.

D'accord également avec @justinvp , Hash doit être réservé au traitement des hachages. Cela a été introduit pour simplifier le traitement de HashCode place.

@redknightlois Pour être clair, nous parlions du nom de la structure, pas du nom de la propriété.

        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();
        }

Usage:
c# int hashCode = HashCode.Combine(field1).Combine(name, StringComparison.OrdinalIgnoreCase).Value; int hashCode = (int)HashCode.Combine(field1).Combine(field2);

Le problème avec Seed est qu'il ne s'agit pas techniquement d'une graine après la première moissonneuse-batteuse.

C'est une graine pour la moissonneuse-batteuse suivante, qui produit une nouvelle graine.

Une objection ou un problème de mise en œuvre avec une valeur int en lecture seule publique à la place ?

Pourquoi? int Value { get; } est plus idiomatique et peut facilement être intégré.

C'est une graine pour la moissonneuse-batteuse suivante, qui produit une nouvelle graine.

Ne serait-ce pas un semis ? ;)

@jamesqo D'après mon expérience, lorsqu'elles sont entourées de propriétés de code complexes, elles ont tendance à générer un code pire que les champs (parmi ceux qui ne sont pas en ligne). De plus, un champ en lecture seule d'un seul int sur une structure se traduit directement dans un registre et éventuellement lorsque le JIT utilise en lecture seule pour l'optimisation (qui n'en a pas encore trouvé l'utilité en ce qui concerne la génération de code); il y a des optimisations qui pourraient être autorisées car cela peut raisonner qu'il s'agit d'une lecture seule. Du point de vue de l'utilisation, il n'y a vraiment pas de différence avec un seul getter.

EDIT : De plus, cela pousse également l'idée que ces structures sont vraiment immuables.

D'après mon expérience, lorsqu'elles sont entourées de propriétés de code complexes, elles ont tendance à générer un code pire que les champs (parmi ceux qui ne sont pas en ligne).

Si vous trouvez une seule version non-débogage où une propriété implémentée automatiquement n'est pas toujours intégrée, il s'agit d'un problème JIT et doit absolument être résolu.

De plus, un champ en lecture seule d'un seul int sur une structure se traduit directement dans un registre

il y a des optimisations qui pourraient être autorisées car cela peut raisonner qu'il s'agit d'une lecture seule.

Le champ de sauvegarde de cette structure sera en lecture seule ; l'API sera un accesseur.

Je ne pense pas que l'utilisation d'une propriété nuira aux performances de quelque manière que ce soit ici.

@jamesqo, je garderai cela à l'esprit quand je les trouverai. Pour le code sensible aux performances, je n'utilise plus les propriétés à cause de cela (mémoire musculaire à ce stade).

Vous pouvez envisager d'appeler la structure imbriquée « State » plutôt que « Seed » ?

@ellismg Bien sûr, merci pour la suggestion. J'avais du mal à trouver un bon nom pour la structure interne.

@karelz Je pense que cette API est enfin

@jamesqo @JonHanna pourquoi avons-nous besoin du Combine<T>(T obj) au lieu de Combine(object o) ?

pourquoi avons-nous besoin de la moissonneuse-batteuse(T obj) au lieu de Combine(object o) ?

Ce dernier allouerait si l'instance était une structure.

duh, merci pour les éclaircissements.

Nous n'aimons pas le type imbriqué car il semble compliquer la conception. Le problème principal était que nous ne pouvons pas nommer les statiques et les non-statiques de la même manière. Nous avons deux options : se débarrasser de la statique ou renommer. Nous pensons que renommer en Create est le plus logique car il crée un code assez lisible, par rapport à l'utilisation du constructeur par défaut.

À moins qu'il n'y ait une forte opposition, c'est la conception sur laquelle nous avons opté :

```C#
système d'espace de noms
{
structure publique HashCode : IEquatable
{
public static HashCode Create(int hashCode);
HashCode statique public Créer(T obj);
HashCode statique public Créer(T obj, IEqualityComparercomparateur);

    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();
}

}
```

Attendons quelques jours pour des commentaires supplémentaires pour savoir s'il y a de bons commentaires sur la proposition approuvée. Ensuite, nous pouvons le faire « à gagner ».

Pourquoi cela complique-t-il la conception? Je pourrais comprendre à quel point ce serait mauvais si nous devions réellement utiliser le HashCode.State dans le code (par exemple pour définir le type d'une variable) mais pensons-nous que c'est souvent le cas? La plupart du temps, je finirai soit par renvoyer la valeur directement, soit par la convertir en int et la stocker.

Je pense que la combinaison de Créer et Combiner est pire.

Veuillez consulter https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653

@terrajobst

Nous pensons que renommer en Create est le plus logique car il crée un code assez lisible, par rapport à l'utilisation du constructeur par défaut.

À moins qu'il n'y ait une forte opposition, c'est la conception sur laquelle nous avons opté :

Je vous entends, mais j'ai eu une réflexion de dernière minute pendant que je travaillais sur l'implémentation... pourrions-nous simplement ajouter une propriété statique Zero / Empty à HashCode , et ensuite demander aux gens d'appeler Combine partir de là ? Cela nous éviterait d'avoir des méthodes Combine / Create distinctes.

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);
}

Quelqu'un d'autre pense que c'est une bonne idée ? (Je soumettrai un PR en attendant, et si les gens le pensent, je le changerai dans le PR.)

@jamesqo , j'aime l'idée Vide/Zéro.

Je serais d' Empty avec ça (pas de préférence marquée entre Empty vs Create factory) ... @weshaggard @bartonjs @stephentoub @terrajobst qu'en pensez-vous ?

Personnellement, je pense que Create() est meilleur ; mais j'aime mieux HashCode.Empty que new HashCode() .

Puisqu'il permet une version qui n'a pas d'opérateur new, et cela n'empêche pas de décider plus tard que nous voulons vraiment Create en tant qu'amorceur... ::shrug::.

C'est toute l'étendue de mon refoulement (aka pas beaucoup).

FWIW Je voterais pour Create plutôt que Empty / Zero . Je préfère commencer avec une valeur réelle que de tout suspendre Empty / Zero . C'est juste bizarre.

Cela décourage également les gens de semer avec zéro, ce qui a tendance à être une mauvaise graine.

Je préfère Créer plutôt que Vider. Cela correspond à ce que j'en pense : je veux créer du code de hachage et ajouter des valeurs supplémentaires. Je serais d'accord avec l'approche imbriquée aussi.

Alors que j'allais dire que l'appeler Empty n'était pas une bonne idée (et cela a déjà été dit), après une troisième réflexion, je pense toujours que ce n'est pas une mauvaise solution. Que diriez-vous de quelque chose comme Builder. Bien qu'il soit encore possible d'utiliser zéro, le mot vous décourage un peu de l'utiliser tout de suite.

@JonHanna juste pour clarifier: vous Create , n'est-ce pas ?

Et sur une quatrième pensée, que diriez-vous de With au lieu de Create.

HashCode.With(a).Combine(b). Combiner(c)

Exemple d'utilisation basé sur la dernière discussion (avec Create éventuellement remplacé par un autre nom) :

```c#
public override 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 Conduirait à un code incohérent + plus de jitting, je pense, à cause de combinaisons plus génériques. Nous pourrons toujours revenir là-dessus dans un autre numéro si cela s'avère souhaitable.

Pour ce que ça vaut, je préfère la version proposée à l'origine, du moins à l'usage (pas sûr des commentaires concernant la taille du code, le jitting, etc.). Il semble exagéré d'avoir une structure supplémentaire et plus de 10 membres différents pour quelque chose qui pourrait être exprimé comme une méthode avec quelques surcharges d'arité différente. Je ne suis pas non plus fan des API de style fluide en général, alors peut-être que cela colore mon opinion.

Je n'allais pas le mentionner car c'est un peu inhabituel et je ne sais toujours pas ce que je ressens, mais voici une autre idée, juste pour m'assurer que toutes les alternatives ont été envisagées...

Et si nous faisions quelque chose du genre du "constructeur" mutable HashCodeCombiner d'ASP.NET Core, avec des méthodes similaires Add , mais incluait également la prise en charge de la syntaxe d'initialisation de la collection ?

Usage:

```c#
public override int GetHashCode() =>
nouveau 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();
    }
}

Il faudrait au moins implémenter IEnumerable avec au moins une méthode Add pour activer la syntaxe d'initialisation de la collection. IEnumerable pourrait être implémenté explicitement pour le cacher d'intellisense et GetEnumerator pourrait soit lancer NotSupportedException soit renvoyer la valeur du code de hachage en tant qu'élément combiné unique dans l'énumérable, si quelqu'un arrivait à l'utiliser (ce qui serait rare).

@justinvp , vous avez une idée intéressante. Cependant, je ne suis pas d'accord avec respect; Je pense que HashCode devrait rester immuable pour éviter les pièges avec les structs mutables. Devoir également implémenter IEnumerable pour cela semble un peu artificiel/floconneux ; si quelqu'un a une directive using System.Linq dans le fichier, alors Cast<> et OfType<> apparaîtront comme méthodes d'extension s'ils placent un point à côté d'un HashCode . Je pense que nous devrions nous en tenir plus à la proposition actuelle.

@jamesqo , je suis d'accord - d'où mon hésitation à le mentionner. La seule chose que j'aime à ce sujet, c'est que l'utilisation peut être plus propre que le chaînage, mais c'est en soi un autre inconvénient car il n'est pas clair que les initialiseurs de collection puissent même être utilisés sans voir l'utilisation des échantillons.

@MadsTorgersen , @jaredpar , pourquoi l'initialiseur de collection nécessite l'implémentation de IEnumerable\Le troisième commentaire de @justinvp ci-dessus.

@jamesqo , je suis d'accord qu'il vaut mieux garder cet immuable (et non IEnumerable\

@mellinoe Je pense que cela rendrait le cas simple légèrement plus simple, mais cela rendrait également tout au-delà plus compliqué (et moins clair sur la bonne chose à faire).

Qui comprend:

  1. plus d'articles que vous n'avez de surcharges pour
  2. conditions
  3. boucles
  4. utiliser un comparateur

Considérez le code d'ASP.NET publié auparavant sur ce sujet (mis à jour par rapport à la proposition actuelle) :

```c#
var hashCode = HashCode
.Créer(EstPageMain)
.Combine(ViewName, StringComparer.Ordinal)
.Combine(ControllerName, StringComparer.Ordinal)
.Combine(AreaName, StringComparer.Ordinal);

if (ViewLocationExpanderValues ​​!= null)
{
foreach (élément var dans ViewLocationExpanderValues)
{
hashCode = hashCode
.Combine(item.Key, StringComparer.Ordinal)
.Combine(item.Value, StringComparer.Ordinal);
}
}

retourner 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;

Même si vous ignorez l'appel de GetHashCode() pour les comparateurs personnalisés, je trouve que devoir passer la valeur précédente de hashCode comme premier paramètre n'est pas simple.

@KrzysztofCwalina Selon la @ericlippert dans The C# Programming Language 1 , c'est parce que les initialiseurs de collection sont (sans surprise) censés être du sucre de syntaxe pour la création de collection, pas pour l'arithmétique (qui était l'autre utilisation courante de la méthode nommée Add ).

1 En raison du fonctionnement de Google Livres, ce lien peut ne pas fonctionner pour tout le monde.

@KrzysztofCwalina , et notez que cela nécessite IEnumerable non générique , pas IEnumerable<T> .

@svick , mineur nit dans votre premier exemple ci-dessus : le premier appel à .Combine serait .Create avec la proposition actuelle. Sauf si nous utilisons l'approche imbriquée.

@svick

cela rendrait également tout au-delà de cela plus compliqué (et moins clair sur la bonne chose à faire)

Je ne sais pas, le deuxième exemple est à peine différent du premier dans l'ensemble, et ce n'est pas plus complexe OMI. Avec la deuxième/approche originale, vous transmettez simplement un tas de codes de hachage (je pense que le premier paramètre devrait en fait être IsMainPage.GetHashCode() ), donc cela me semble simple. Mais il semble que je sois en minorité ici, donc je ne pousserai pas pour l'approche originale. Je n'ai pas d'opinion tranchée ; ces deux exemples me semblent assez raisonnables.

@justinvp Merci, mis à jour. (Je suis allé avec la première proposition dans le premier message et je n'ai pas réalisé qu'elle était obsolète, quelqu'un devrait probablement la mettre à jour.)

@mellinoe le problème est en fait que le second peut générer des bugs subtils. Il s'agit du code réel d'un de nos projets.

        [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);            
        }

Nous vivons avec, mais nous traitons tous les jours avec des trucs de très bas niveau ; donc pas le développeur moyen c'est sûr. Cependant ce n'est pas la même ici de combiner v avec w que w avec v ... la même chose entre v et w combine. Les combinaisons de hachage ne sont pas commutatives, donc enchaîner les unes après les autres peut en fait éliminer tout un ensemble d'erreurs au niveau de l'API.

Je suis allé avec la première proposition dans le premier message et je n'ai pas réalisé qu'elle était obsolète, quelqu'un devrait probablement la mettre à jour.

Fait.
BTW : Cette proposition est très difficile à suivre, en particulier les votes ... tant de variantes (ce qui, je suppose, est bien ;-))

@karelz Si nous ajoutons des API Create je pense que nous pouvons toujours ajouter Empty . Ce n'est pas forcément l' un ou l'autre, comme l' @bartonjs . Proposé

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

Cela décourage également les gens de semer avec zéro, ce qui a tendance à être une mauvaise graine.

L'algorithme de hachage que nous choisissons sera le même que celui utilisé dans HashHelpers aujourd'hui, ce qui a pour effet que hash(0, x) == x . HashCode.Empty.Combine(x) produira exactement les mêmes résultats que HashCode.Create(x) , il n'y a donc objectivement aucune différence.

@jamesqo vous avez oublié d'inclure les Zero supplémentaires dans votre dernière proposition. S'il s'agissait d'une omission, pouvez-vous la mettre à jour ? Nous pouvons ensuite demander aux gens de voter pour votre dernière proposition. On dirait que les autres alternatives (voir le post du haut que j'ai mis à jour) n'obtiennent pas beaucoup de suivi ...

@karelz Merci pour le repérage, corrigé.

@KrzysztofCwalina pour vérifier que vous voulez dire « Ajouter » dans le sens d'ajouter à une collection, pas dans un autre sens. Je ne sais pas si j'aime cette restriction, mais c'est ce que nous avons décidé à l'époque.

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

Le paramètre doit-il être nommé hashCode au lieu de hash puisque la valeur transmise sera un code de hachage probablement obtenu en appelant GetHashCode() ?

Empty / Zero

Si nous finissons par conserver cela, un autre nom à considérer est Default .

@justinvp

Le paramètre doit-il être nommé hashCode au lieu de hash puisque la valeur transmise sera un code de hachage probablement obtenu en appelant GetHashCode() ?

Je voulais nommer les paramètres int hash et les paramètres hashCode HashCode hashCode . À la réflexion cependant, je pense que hashCode serait mieux parce que, comme vous l'avez mentionné, hash est un peu vague. Je vais mettre à jour l'API.

Si nous finissons par conserver cela, un autre nom à considérer est Default.

Quand j'entends Default je pense "la manière habituelle de faire quelque chose quand vous ne savez pas quelle option choisir", pas "la valeur par défaut d'une structure". par exemple, quelque chose comme Encoding.Default a une connotation complètement différente.

L'algorithme de hachage que nous choisissons sera le même que celui utilisé dans HashHelpers aujourd'hui, ce qui a pour effet que hash(0, x) == x. HashCode.Empty.Combine(x) produira exactement les mêmes résultats que HashCode.Create(x), il n'y a donc objectivement aucune différence.

En tant que personne qui ne connaît pas grand-chose à l'intérieur de cela, j'aime beaucoup la simplicité de HashCode.Create(x).Combine(...) . Create est très évident, car il est utilisé dans de nombreux autres endroits.

Si Empty / Zero / Default ne fournit aucune utilisation algorithmique, cela ne devrait pas être là IMO.

PS : sujet très intéressant !! Bon travail! ??

@cwe1ss

Si Empty / Zero / Default ne fournit aucune utilisation algorithmique, cela ne devrait pas être là IMO.

Avoir un champ Empty fournit une utilisation algorithmique. Il représente une "valeur de départ" à partir de laquelle vous pouvez combiner des hachages. Par exemple, si vous voulez combiner un tableau de hachages en utilisant strictement Create , c'est assez pénible :

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;
}

Si vous avez Empty , cela devient beaucoup plus naturel :

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 Ce type est assez analogue à ImmutableArray<T> pour moi. Un tableau vide n'est pas très utile en soi, mais est très utile comme "point de départ" pour d'autres opérations, et c'est pourquoi nous avons une propriété Empty pour lui. Je pense qu'il serait logique d'en avoir un pour HashCode , aussi; nous gardons Create .

@jamesqo J'ai remarqué que vous en silence / par accident changé de nom arg obj à value dans votre proposition https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653. Je l'ai remis à obj qui IMO capture mieux ce que vous obtenez. Le nom value est davantage associé à la valeur de hachage "int" elle-même dans ce contexte.
Je suis ouvert à une discussion plus approfondie sur le nom de l'argument si nécessaire, mais modifions-le volontairement et gardons une trace de la différence par rapport à la dernière proposition approuvée.

J'ai mis à jour la proposition en haut. J'ai également appelé diff contre la dernière version approuvée de la proposition.

L'algorithme de hachage que nous choisissons sera le même que celui utilisé dans HashHelpers aujourd'hui

Pourquoi est-ce un bon algorithme à choisir comme celui qui devrait être utilisé partout ? Quelle hypothèse va-t-il faire sur la combinaison des hashcodes ? S'il est utilisé partout, va-t-il ouvrir de nouvelles voies aux attaques DDoS ? (Notez que nous avons été brûlés par cela pour le hachage de chaîne dans le passé.)

Et si nous faisions quelque chose dans le style du "constructeur" mutable HashCodeCombiner d'ASP.NET Core

Je pense que c'est le bon modèle à utiliser. Un bon combineur de hashcode universel peut généralement utiliser plus d'états que ce qui rentre dans le hashcode lui-même, mais le modèle fluide s'effondre ensuite, car le passage de structures plus grandes est un problème de performances.

Pourquoi est-ce un bon algorithme à choisir comme celui qui devrait être utilisé partout ?

Il ne doit pas être utilisé partout. Voir mon commentaire sur https://github.com/dotnet/corefx/issues/8034#issuecomment -260790829; il s'adresse principalement aux personnes qui ne connaissent pas grand-chose au hachage. Les personnes qui savent ce qu'elles font peuvent l'évaluer pour voir si cela correspond à leurs besoins.

Quelle hypothèse va-t-il faire sur la combinaison des hashcodes ? S'il est utilisé partout, va-t-il ouvrir de nouvelles voies aux attaques DDoS ?

Un problème avec le hachage actuel que nous avons est que hash(0, x) == x . Donc, si une série de zéros ou de zéros est fournie au hachage, il restera 0. Voir le code . Cela ne veut pas dire que les valeurs nulles ne comptent pas, mais aucun des valeurs nulles initiales ne compte. J'envisage d'utiliser quelque chose de plus robuste (mais légèrement plus cher) comme ici , qui ajoute une constante magique pour éviter de mapper zéro à zéro.

Je pense que c'est le bon modèle à utiliser. Un bon combineur de hashcode universel peut généralement utiliser plus d'états que ce qui rentre dans le hashcode lui-même, mais le modèle fluide s'effondre ensuite, car le passage de structures plus grandes est un problème de performances.

Je ne pense pas qu'il devrait y avoir un combineur universel avec une grande taille de structure qui tente de s'adapter à tous les cas d'utilisation. Au lieu de cela, j'imaginais des types de code de hachage distincts qui sont tous de taille int ( FnvHashCode , etc.) et qui ont tous leurs propres méthodes Combine . En outre, ces types de "constructeurs" seront de toute façon conservés dans la même méthode, et non transmis.

Je ne pense pas qu'il devrait y avoir un combineur universel avec une grande taille de structure qui tente de s'adapter à tous les cas d'utilisation.

ASP.NET Core pourra-t-il remplacer son propre combineur de hashcode - qui a actuellement 64 bits d'état - par celui-ci ?

J'envisageais des types de code de hachage distincts qui sont tous de taille int (FnvHashCode, etc.)

Cela ne conduit-il pas à une explosion combinatoire ? Cela devrait faire partie de la proposition d'API pour indiquer clairement à quoi cette conception d'API mène.

@jkotas J'ai formulé des objections similaires au début de la discussion. Traiter des fonctions de hachage nécessite des connaissances en la matière. Mais je comprends et je soutiens la résolution du problème causé en 2001 avec l'introduction de codes de hachage à la racine même du framework et je ne prescris pas de recette pour combiner les hachages. Cette conception vise à résoudre ce problème dans 99% des cas (où aucune connaissance du sujet n'est disponible ou même nécessaire, car les propriétés statistiques du hachage sont assez bonnes). ASP.Net Core devrait pouvoir utiliser d'inclure de tels combineurs dans un cadre à usage général sur un assemblage non système comme celui proposé pour discussion ici : https://github.com/dotnet/corefx/issues/13757

Je suis d'accord que c'est une bonne idée d'avoir un combineur de hashcode qui est une évidence à utiliser dans 99% des cas. Cependant, il doit autoriser plus d'états internes que 32 bits.

BTW : ASP.NET utilisait le modèle fluide pour la combinaison de hashcode à l'origine, mais a cessé de le faire à cause de cela conduisait à des bogues faciles à manquer : https://github.com/aspnet/Razor/pull/537

@jkotas concernant la sécurité contre l'inondation de hachage.
AVIS DE NON-RESPONSABILITÉ : Pas un expert (vous devriez en consulter un et MS en a plus que quelques-uns sur la question) .

J'ai regardé autour de moi et bien qu'il ne s'agisse pas d'un consensus général sur la question, il existe un argument qui gagne du terrain de nos jours. Les codes de hachage sont de taille 32 bits, j'ai posté avant un graphique montrant la probabilité de collisions compte tenu de la taille de l'ensemble. Cela signifie que peu importe la qualité de votre algorithme (en regardant SipHash par exemple), il est assez viable de générer beaucoup de hachages et de trouver des collisions dans un délai raisonnable (environ moins d'une heure). Ces problèmes doivent être résolus au niveau de la structure de données contenant les hachages, ils ne peuvent pas être résolus au niveau de la fonction de hachage. Payer des performances supplémentaires sur des données non cryptographiques pour se protéger contre l'inondation de hachage sans réparer la structure de données sous-jacente ne résoudra pas le problème.

EDIT : Vous avez posté pendant que j'écrivais. À la lumière de cela, qu'est-ce que l'état 64 bits gagne pour vous ?

@jkotas J'ai examiné le problème

Réaction à aspnet/Common#40

Description de https://github.com/aspnet/Common/issues/40 :

Repérez le bogue :

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();
    }
}

Allez. Cet argument revient à dire que string devrait être modifiable puisque les gens ne réalisent pas que Substring renvoie une nouvelle chaîne. Les structures mutables sont bien pires en termes de pièges; Je pense que nous devrions garder la structure immuable.

concernant la sécurité contre les inondations de hachage.

Il y a deux aspects à cela : une conception correcte par construction (structures de données robustes, etc.) ; et l'atténuation des problèmes dans la conception existante. Les deux sont importants.

@karelz Concernant le nommage des paramètres

J'ai remarqué que vous avez changé silencieusement/par accident le nom d'argument obj en valeur dans votre proposition dotnet/corefx#8034 (commentaire). Je l'ai remis à obj qui IMO capture mieux ce que vous obtenez. La valeur du nom est davantage associée à la valeur de hachage "int" elle-même dans ce contexte.
Je suis ouvert à une discussion plus approfondie sur le nom de l'argument si nécessaire, mais modifions-le volontairement et gardons une trace de la différence par rapport à la dernière proposition approuvée.

J'envisage, dans une future proposition, d'ajouter des API pour combiner des valeurs en bloc. Par exemple : CombineRange(ReadOnlySpan<T>) . Si nous l'appelions obj , nous devrions nommer le paramètre ici objs , ce qui semble très étrange. Nous devrions donc le nommer item place ; à l'avenir, nous pourrons nommer le paramètre span items . Mise à jour de la proposition.

@jkotas est d' accord, mais le fait est que nous

La seule chose que nous puissions faire est d'avoir une graine aléatoire, qui pour tous les états et objectifs, je me souviens avoir vu le code à string et il est fixé par build. (c'est peut-être faux, car c'était il y a longtemps). Avoir une mise en œuvre correcte des graines aléatoires est la seule atténuation qui pourrait s'appliquer ici.

C'est un défi, donnez-moi votre meilleure fonction de hachage de chaîne et/ou de mémoire avec une graine aléatoire fixe et j'irai construire un ensemble sur des codes de hachage 32 bits qui ne généreront que des collisions. Je n'ai pas peur de lancer un tel défi car c'est assez facile à faire, la théorie des probabilités est de mon côté. J'irais même faire un pari, mais je sais que je gagnerai, donc ce n'est plus un pari.

De plus... une analyse plus approfondie montre que même si l'atténuation est la possibilité d'intégrer ces "graines aléatoires" par exécution, un combineur plus compliqué n'est pas nécessaire. Parce que vous avez essentiellement atténué le problème à la source.

Disons que vous avez M1 et M2 avec différentes graines aléatoires rs1 et rs2 ....
M1 émettra h1 = hash('a', rs1) et h2=hash('b', rs1)
M2 émettra h1' = hash('a', rs2) et h2'=hash('b', rs2)
Le point clé ici est que h1 et h1' différeront avec une probabilité 1/ (int.MaxInt-1) (si hash est assez bon) qui à toutes fins est aussi bon comme ça va devenir.
Par conséquent, quel que soit le c(x,y) vous décidez d'utiliser (si suffisamment bon) prend déjà en compte l'atténuation intégrée à la source.

EDIT : j'ai trouvé le code, vous utilisez Marvin32 qui change maintenant sur chaque domaine. Ainsi, l'atténuation des chaînes utilise des graines aléatoires par exécution. Ce qui, comme je l'ai dit, est une atténuation suffisante.

@jkotas

ASP.NET Core pourra-t-il remplacer son propre combineur de hashcode - qui a actuellement 64 bits d'état - par celui-ci ?

Absolument; il utilise le même algorithme de hachage. Je viens de créer cette application de test pour mesurer le nombre de collisions et je l'ai exécutée 10 fois. Aucune différence significative par rapport à l'utilisation de 64 bits.

J'envisageais des types de code de hachage distincts qui sont tous de taille int (FnvHashCode, etc.)

Cela ne conduit-il pas à une explosion combinatoire ? Cela devrait faire partie de la proposition d'API pour indiquer clairement à quoi cette conception d'API mène.

@jkotas , ce ne sera pas le cas. La conception de cette classe ne définira pas la conception des futures API de hachage dans le marbre. Ceux-ci devraient être considérés comme des scénarios plus avancés, devraient être inclus dans une proposition différente telle que dotnet/corefx#13757, et auront une discussion de conception différente. Je pense qu'il est bien plus important d'avoir une API simple pour un algorithme de hachage général, pour les débutants qui ont du mal à remplacer GetHashCode .

Je suis d'accord que c'est une bonne idée d'avoir un combineur de hashcode qui est une évidence à utiliser dans 99% des cas. Cependant, il doit autoriser plus d'états internes que 32 bits.

Quand aurions-nous besoin de plus d'état interne que 32 bits ? edit: Si c'est pour permettre aux gens de brancher une logique de hachage personnalisée, je pense (encore une fois) que cela devrait être considéré comme un scénario avancé et être discuté dans dotnet/corefx#13757.

vous utilisez Marvin32 qui change maintenant sur chaque domaine

À droite, l'atténuation de la randomisation du code de hachage de chaîne est activée par défaut dans .NET Core. Il n'est pas activé par défaut pour les applications autonomes dans le .NET Framework complet en raison de la compatibilité ; il n'est activé que via des bizarreries (par exemple dans des environnements à haut risque).

Nous avons toujours le code pour le hachage non aléatoire dans .NET Core, mais il devrait être possible de le supprimer. Je ne pense pas que nous en aurons encore besoin. Cela rendrait également le calcul du code de hachage de la chaîne un peu plus rapide car il n'y aura plus de vérification pour savoir s'il faut utiliser le chemin non aléatoire.

L'algorithme Marvin32 utilisé pour calculer les codes de hachage de chaîne aléatoires a un état interne de 64 bits. Il a été choisi par les experts du sujet MS. Je suis presque sûr qu'ils avaient une bonne raison d'utiliser l'état interne 64 bits, et ils ne l'ont pas utilisé uniquement pour ralentir les choses.

Un combineur de hachage à usage général devrait continuer à faire évoluer cette atténuation : il devrait utiliser une graine aléatoire et un algorithme de combinaison de code de hachage suffisamment puissant. Idéalement, il utiliserait le même Marvin32 comme hachage de chaîne aléatoire.

L'algorithme Marvin32 utilisé pour calculer les codes de hachage de chaîne aléatoires a un état interne de 64 bits. Il a été choisi par les experts du sujet MS. Je suis presque sûr qu'ils avaient une bonne raison d'utiliser l'état interne 64 bits, et ils ne l'ont pas utilisé uniquement pour ralentir les choses.

@jkotas , le combineur de code de hachage string.GetHashCode non aléatoire.

Un combineur de hachage à usage général devrait continuer à faire évoluer cette atténuation : il devrait utiliser une graine aléatoire et un algorithme de combinaison de code de hachage suffisamment puissant. Idéalement, il utiliserait le même Marvin32 comme hachage de chaîne aléatoire.

Ce type n'est pas destiné à être utilisé dans des endroits sujets aux attaques DoS par hachage. Ceci est destiné aux personnes qui ne savent pas mieux ajouter/xor, et aidera à empêcher des choses comme https://github.com/dotnet/coreclr/pull/4654.

Un combineur de hachage à usage général devrait continuer à faire évoluer cette atténuation : il devrait utiliser une graine aléatoire et un algorithme de combinaison de code de hachage suffisamment puissant. Idéalement, il utiliserait le même Marvin32 comme hachage de chaîne aléatoire.

Ensuite, nous devrions discuter avec l'équipe C# pour qu'elle implémente un algorithme de hachage ValueTuple atténué. Parce que ce code sera également utilisé dans des environnements à haut risque. Et bien sûr Tuple https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Tuple.cs#L60 ou System.Numerics.HashHelpers (utilisé partout dans le lieu).

Maintenant, avant de décider comment l'implémenter, je me tournerais vers les mêmes experts en la matière si payer le coût d'un algorithme de combinaison de hashcode entièrement aléatoire en vaut la peine (s'il existe bien sûr) même si cela ne changerait pas la façon dont l'API est conçu soit (sous l'API proposée, vous pouvez utiliser un état de 512 bits et avoir toujours la même API publique, si vous êtes prêt à en payer le coût, bien sûr).

Ceci est destiné aux personnes qui ne savent pas mieux ajouter/xor

C'est exactement pourquoi il est important qu'il soit robuste. La valeur clé de .NET est qu'il résout les problèmes des personnes qui ne connaissent pas mieux.

Et pendant que nous y sommes, n'oublions pas IntPtr https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/IntPtr.cs#L119
Celui-ci est particulièrement méchant, xor est probablement le pire car bad entrera en collision avec dab .

implémenter un algorithme de hachage ValueTuple atténué

Bon point. Je ne sais pas si ValueTuple a été expédié ou s'il est encore temps de le faire. Ouvert https://github.com/dotnet/corefx/issues/14046.

n'oublions pas IntPtr

Ce sont des erreurs du passé... la barre pour les réparer est beaucoup plus élevée.

@jkotas

Ce sont des erreurs du passé... la barre pour les réparer est beaucoup plus élevée.

Je pensais que l'un des points de .Net Core est que la barre pour les "petits" changements comme celui-ci devrait être beaucoup plus basse. Si quelqu'un dépend de l'implémentation de IntPtr.GetHashCode (ce qu'il ne devrait vraiment pas faire), il peut choisir de ne pas mettre à niveau sa version de .Net Core.

la barre pour les "petits" changements comme ça devrait être beaucoup plus basse

Oui, c'est - par rapport au .NET Framework complet. Mais vous devez toujours faire le travail pour que le changement soit appliqué dans le système et vous constaterez peut-être que cela n'en vaut pas la peine. Un exemple récent est le changement de l'algorithme de hachage Tuple<T> qui a été annulé à cause de la panne de F# : https://github.com/dotnet/coreclr/pull/6767#issuecomment -256896016

@jkotas

Si nous devions créer HashCode 64 bits, pensez-vous qu'une conception immuable tuerait les performances dans les environnements 32 bits ? Je suis d'accord avec les autres lecteurs, un modèle de constructeur semble être bien pire.

Tuez la perf - non. Pénalité de performance payée pour le sucre de syntaxe - oui.

Pénalité de performance payée pour le sucre de syntaxe - oui.

Est-ce quelque chose qui pourrait être optimisé par le JIT à l'avenir ?

Tue la perf - non.
Pénalité de performance payée pour le sucre de syntaxe - oui.

C'est plus que du sucre syntaxique. Si nous étions prêts à faire de HashCode une classe, alors ce serait du sucre syntaxique. Mais un type de valeur mutable est une ferme de bogues.

Vous citant plus tôt :

C'est exactement pourquoi il est important qu'il soit robuste. La valeur clé de .NET est qu'il résout les problèmes des personnes qui ne connaissent pas mieux.

Je dirais qu'un type de valeur mutable n'est pas une API robuste pour la majorité des personnes qui ne connaissent pas mieux.

Je dirais qu'un type de valeur mutable n'est pas une API robuste pour la majorité des personnes qui ne connaissent pas mieux.

Accepter. Pensé, je pense qu'il est regrettable que ce soit le cas pour les types de constructeurs de structure mutables. J'utilise les tout le temps à cause de ce qu'ils soient bien serrés. [MustNotCopy] annotations quelqu'un ?

MustNotCopy est le rêve d'un amateur de struct devenu réalité. @jaredpar ?

MustNotCopy est comme une pile seulement mais encore plus difficile à utiliser 😄

Je suggère de ne créer aucune classe mais de créer des méthodes d'extension pour combiner le hachage

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);
}

C'est tout! C'est rapide et facile à utiliser.

@AlexRadch Je n'aime pas que cela pollue la liste des méthodes pour tous les entiers , pas seulement ceux qui sont censés être des hachages.

De plus, vous avez des méthodes qui continuent une chaîne de calcul du code de hachage, mais comment la démarrez-vous ? Devez-vous faire quelque chose de non évident comme commencer par zéro ? C'est-à-dire 0.CombineHash(this.FirstName).CombineHash(this.LastName) .

Mise à jour : par le commentaire dans dotnet/corefx#14046, il a été décidé que la formule de hachage existante serait conservée pour ValueTuple :

@jamesqo Merci pour l'aide.
Depuis la dernière discussion avec @jkotas et @VSadov , nous sommes d'accord pour aller de l'avant avec la randomisation/l'ensemencement, mais nous
Faire la randomisation permet de changer la fonction de hachage à l'avenir si le besoin s'en fait sentir.

@jkotas , pouvons-nous simplement conserver le hachage actuel basé sur ROL 5 pour HashCode et le réduire à 4 octets ? Cela éliminerait tous les problèmes de copie de structure. Nous pouvons avoir HashCode.Empty représentant une valeur de hachage aléatoire.

@svick
Oui, cela pollue les méthodes pour tous les entiers, mais il peut être placé dans un espace de nom séparé et si vous ne travaillez pas avec des hachages, vous ne l'inclurez pas et ne le verrez pas.

0.CombineHash(this.FirstName).CombineHash(this.LastName) doit être écrit sous la forme this.FirstName.GetHash().CombineHash(this.LastName)

Pour implémenter à partir de la graine, il peut avoir la méthode statique suivante

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

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

Ainsi, chaque classe aura une graine différente pour randomiser les hachages.

@jkotas , pouvons-nous simplement conserver le hachage actuel basé sur ROL 5 pour HashCode et le réduire à 4 octets ?

Je pense qu'un assistant de construction de hashcode de plate-forme publique doit utiliser un état 64 bits pour être robuste. S'il est uniquement en 32 bits, il aura tendance à produire de mauvais résultats lorsqu'il est utilisé pour hacher davantage d'éléments, de tableaux ou de collections en particulier. Comment rédigez-vous de la documentation quand c'est une bonne idée de l'utiliser plutôt que de ne pas l'utiliser ? Oui, ce sont des instructions supplémentaires passées à mélanger les bits, mais je ne pense pas que cela ait une importance. Ce genre d'instructions s'exécute très rapidement. Mon expérience est qu'il vaut mieux faire plus de peu de mélange que moins parce que les effets de faire trop peu de mélange sont beaucoup plus graves que d'en faire trop.

De plus, j'ai toujours des inquiétudes concernant la forme proposée de l'API. Je pense que le problème doit être considéré comme une construction de code de hachage, et non comme une combinaison de code de hachage. Il est peut-être prématuré d'ajouter cela en tant qu'API de plate-forme, et nous devrions plutôt attendre et voir si un meilleur modèle émerge pour cela. Cela n'empêche pas quelqu'un de publier un package nuget (source) avec cette API, ou corefx de l'utiliser comme assistant interne.

@jkotas ayant un état

Voyez ce qui fait une bonne fonction de hachage (dont certains sont clairement comme xor : http://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and -speed et https://research.neustar.biz/2012/02/02/choosing-a-good-hash-function-part-3/

@jamesqo BTW, je viens de réaliser que le combineur ne fonctionnera pas dans le cas de : "Je combine en fait des hachages (pas des hachages d'exécution) car la graine changera à chaque fois." ... constructeur public en semence ?

@jkotas

Je pense qu'un assistant de construction de hashcode de plate-forme publique doit utiliser un état 64 bits pour être robuste. S'il est uniquement en 32 bits, il aura tendance à produire de mauvais résultats lorsqu'il est utilisé pour hacher davantage d'éléments, de tableaux ou de collections en particulier.

Est-ce important quand il finira par être condensé en un seul int à la fin ?

@jamesqo Pas vraiment, la taille de l'état ne dépend que de la fonction, pas de la robustesse. En fait, vous pouvez aggraver votre fonction de hachage si la moissonneuse-batteuse n'est pas conçue pour fonctionner de cette façon, et au mieux, vous gaspillez des ressources car vous ne pouvez pas acquérir le caractère aléatoire de la coercition.

Corollaire : si vous cochez, assurez-vous que la fonction est statistiquement bonne ou vous êtes presque assuré de l'aggraver.

Cela dépend de l'existence d'une corrélation entre les éléments. S'il n'y a pas de corrélation, l'état 32 bits et le simple rotl (ou même xor) fonctionnent très bien. S'il y a corrélation, ça dépend.

Considérez si quelqu'un l'a utilisé pour créer un code de hachage de chaîne à partir de caractères individuels. Non pas qu'il soit probable que quelqu'un fasse cela pour une chaîne, mais cela démontre le problème :

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

Cela donnerait de mauvais résultats pour les chaînes avec un état de 32 bits et un simple rotl car les caractères dans les chaînes du monde réel ont tendance à être corrélés. À quelle fréquence les éléments pour lesquels cela est utilisé vont-ils être corrélés et à quel point cela donnerait-il de mauvais résultats ? Difficile à dire, bien que les choses dans la vie réelle aient tendance à être corrélées de manière inattendue.

Ce sera formidable d'ajouter la méthode suivante à la prise en charge de la randomisation par hachage de l'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 Je ne l'ai pas testé, alors j'espère que vous l'avez fait. Mais cela dit certainement quelque chose sur la fonction que nous avons l'intention d'utiliser. Ce n'est tout simplement pas suffisant , du moins si vous voulez échanger la vitesse contre la fiabilité (personne ne peut faire de bêtises avec). Pour une fois, je privilégie la conception selon laquelle il ne s'agit pas d'une fonction de hachage non cryptographique, mais d'un moyen rapide de combiner des codes de hachage non corrélés (qui sont aussi aléatoires que possible).

Si nous voulons que personne ne fasse de bêtises avec, utiliser un état 64 bits ne résout rien, nous cachons simplement le problème. Il serait toujours possible de créer une entrée qui exploitera cette corrélation. Ce qui nous renvoie encore une fois au même argument que j'ai avancé il y a 18 jours. Voir : https://github.com/dotnet/corefx/issues/8034#issuecomment -261301533

Pour une fois, je privilégie la conception selon laquelle il ne s'agit pas d'une fonction de hachage non crypto, mais d'un moyen rapide de combiner des codes de hachage non corrélés

Le moyen le plus rapide de combiner des codes de hachage non corrélés est xor...

C'est vrai, mais nous savons que la dernière fois n'a pas si bien fonctionné (IntPtr me vient à l'esprit). La rotation et le XOR (courant) sont tout aussi rapides, sans perte si quelqu'un y met des éléments corrélés.

Ajoutez la randomisation du code de hachage avec public static HashCode CreateRandomized(Type type); ou avec les méthodes public static HashCode CreateRandomized<T>(); ou avec les deux.

@jkotas Je pense que j'ai peut-être trouvé un meilleur modèle pour cela. Et si nous utilisions les retours de référence C# 7 ? Au lieu de renvoyer un HashCode chaque fois, nous renverrions un ref HashCode qui rentre dans un registre.

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;
    }
}

L'utilisation reste la même qu'avant :

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

Le seul inconvénient est que nous revenons à nouveau à une structure mutable. Mais je ne pense pas qu'il y ait un moyen d'avoir à la fois l'absence de copie et l'immutabilité.

( ref this ne fonctionne pas encore, mais je vois un PR dans Roslyn pour l'activer ici. )


@AlexRadch Je ne pense pas qu'il soit sage de combiner davantage le hachage avec le type, car obtenir le code de hachage du type coûte cher.

@jamesqo public static HashCode CreateRandomized<T>(); n'obtient pas le code de hachage de type. Il crée un HashCode aléatoire pour ce type.

@jamesqo " ref this ne fonctionne pas encore". Même une fois le problème Roslyn résolu, ref this ne sera pas disponible pour le référentiel corefx pendant un certain temps (je ne sais pas combien de temps, @stephentoub peut probablement définir des attentes).

La discussion sur la conception ne converge pas ici. De plus les 200 commentaires sont très difficiles à suivre.
Nous prévoyons de saisir @jkotas la semaine prochaine et de débusquer la proposition lors de l'examen de l'API mardi prochain. Nous publierons ensuite la proposition ici pour d'autres commentaires.

D'un côté : je suggère de clore ce dossier et d'en créer un nouveau avec la « proposition bénie » lorsque nous l'aurons la semaine prochaine pour alléger le fardeau de la suite de la longue discussion. Faites-moi savoir si vous pensez que c'est une mauvaise idée.

@jcouv Je suis d'accord avec le fait que cela ne fonctionne pas encore, donc tant que nous pouvons suivre cette conception lors de sa sortie. (Je pense aussi qu'il peut être possible de contourner cela temporairement en utilisant Unsafe .)

@karelz OK :smile: Je fermerai cette proposition plus tard quand j'aurai le temps et en ouvrirai une nouvelle. Je suis d'accord; mon navigateur ne peut pas gérer plus de 200 commentaires aussi bien.

@karelz, j'ai rencontré un problème ; il s'avère que le PR en question essayait d'activer les retours ref this pour les types référence par opposition aux types valeur. ref this ne peut pas être renvoyé en toute sécurité à partir des structs ; voir ici pourquoi. Ainsi, le compromis de retour de référence ne fonctionnera pas.

Quoi qu'il en soit, je vais clore ce sujet. J'ai ouvert un autre numéro ici : https://github.com/dotnet/corefx/issues/14354

Devrait pouvoir retourner la référence "this" à partir d'une méthode d'extension de type valeur post https://github.com/dotnet/roslyn/pull/15650 bien que je suppose que C#vNext...

@benaadams

Devrait être capable de renvoyer la référence "this" à partir d'une méthode d'extension de type valeur après dotnet/roslyn#15650 bien que je suppose que C#vNext...

Correct. Il est possible de retourner this partir d'une méthode d'extension ref this . Cependant, il n'est pas possible de retourner this partir d'une méthode d'instance de structure normale. Il y a beaucoup de détails sanglants sur la raison pour laquelle c'est le cas :(

@redknightlois

si nous voulons être stricts, le seul hachage doit être uint , cela peut être considéré comme un oubli que le framework renvoie int sous cette lumière.

Conformité CLS ? Les entiers non signés ne sont pas compatibles CLS.

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

btecu picture btecu  ·  3Commentaires

aggieben picture aggieben  ·  3Commentaires

Timovzl picture Timovzl  ·  3Commentaires

matty-hall picture matty-hall  ·  3Commentaires

jkotas picture jkotas  ·  3Commentaires