Runtime: Hinzufügen eines HashCode-Typs, um das Kombinieren von Hash-Codes zu erleichtern

Erstellt am 25. Apr. 2016  ·  206Kommentare  ·  Quelle: dotnet/runtime

Ersetze die lange Diskussion mit über 200 Kommentaren durch die neue Ausgabe dotnet/corefx#14354

Diese Ausgabe ist GESCHLOSSEN!!!


Motivation

Java hat Objects.hash zum schnellen Kombinieren der Hash-Codes der einzelnen Felder, um sie in Object.hashCode() . Leider hat .NET keine solche äquivalenten und Entwickler gezwungen sind , ihre eigenen Hashes wie zu rollen dies :

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

Manchmal greifen die Leute sogar darauf zurück, Tuple.Create(field1, field2, ...).GetHashCode() zu verwenden, was (offensichtlich) schlecht ist, da es allokiert.

Vorschlag

  • Liste der Änderungen im aktuellen Vorschlag (gegenüber der zuletzt genehmigten Version https://github.com/dotnet/corefx/issues/8034#issuecomment-262331783):

    • Empty Eigenschaft hinzugefügt (als natürlicher Ausgangspunkt analog zu ImmutableArray )

    • Argumentnamen aktualisiert: 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();
    }
}

Verwendung:

```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 hashes) { hashCode3 = hashCode3.Combine(hash); }
(int)hashCode3;
```

Anmerkungen

Die Implementierung sollte den Algorithmus in HashHelpers .

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

Hilfreichster Kommentar

[@redknightlois] Wenn wir eine Begründung dafür brauchen, warum wir uns für System kann ich es mit einer Begründung versuchen. Wir haben HashCode , um bei Implementierungen von object.GetHashCode() HashCode zu helfen. Es klingt passend, dass beide einen Namensraum teilen würden.

Das war auch die Begründung, die @KrzysztofCwalina und ich verwendet haben. Verkauft!

Alle 206 Kommentare

Wenn Sie etwas Schnelles wollen, können Sie ValueTuple.Create(field1, field2).GetHashCode() . Es ist derselbe Algorithmus, der in Tuple (der übrigens dem in Objects ähnlich ist) und hat keinen Zuweisungs-Overhead.

Andernfalls stellt sich die Frage, wie gut ein Hash benötigt wird, welche wahrscheinlichen Feldwerte vorhanden sein werden (was beeinflusst, welche Algorithmen gute oder schlechte Ergebnisse liefern), ob HashDoS-Angriffe wahrscheinlich sind, Kollisionen modulo a binary- gerade Zahlen schaden (wie sie es bei binär-gerade Hash-Tabellen tun) und so weiter, wodurch One-fits-all-Anwendbarkeit entfällt.

@JonHanna Ich denke, diese Frage gilt beispielsweise auch für string.GetHashCode() . Ich verstehe nicht, warum es schwieriger sein sollte, Hash bereitzustellen.

Eigentlich sollte es einfacher sein, da Benutzer mit besonderen Anforderungen leicht aufhören können, Hash , aber die Verwendung von string.GetHashCode() ist schwieriger.

Wenn Sie etwas Schnelles wollen, können Sie ValueTuple.Create(field1, field2).GetHashCode() verwenden.

Ah, gute Idee, an ValueTuple hatte ich bei diesem Beitrag nicht gedacht. Leider glaube ich nicht, dass das vor C# 7/der nächsten Framework-Version verfügbar sein wird oder ob es so performant sein wird (diese Eigenschafts-/Methodenaufrufe von EqualityComparer können sich summieren). Aber ich habe keine Benchmarks genommen, um das zu messen, also würde ich es nicht wirklich wissen. Ich denke nur, dass es eine dedizierte/einfache Klasse für das Hashing geben sollte, die Leute verwenden können, ohne Tupel als hackische Problemumgehung zu verwenden.

Andernfalls stellt sich die Frage, wie gut ein Hash benötigt wird, welche wahrscheinlichen Feldwerte vorhanden sein werden (was beeinflusst, welche Algorithmen gute oder schlechte Ergebnisse liefern), ob HashDoS-Angriffe wahrscheinlich sind, Kollisionen modulo a binary- gerade Zahlen schaden (wie sie es bei binär-gerade Hash-Tabellen tun) und so weiter, wodurch One-fits-all-Anwendbarkeit entfällt.

Absolut zugestimmt, aber ich denke, die meisten Implementierungen berücksichtigen das nicht, zB ist die aktuelle Implementierung von ArraySegment ziemlich naiv. Der Hauptzweck dieser Klasse (zusammen mit der Vermeidung Allokationen) würde eine Go-to - Implementierung für die Menschen zu schaffen sein , die nicht wissen viel über Hashing, sie daran zu hindern , etwas Dummes wie tut dies . Personen, die mit den von Ihnen beschriebenen Situationen umgehen müssen, können ihren eigenen Hashing-Algorithmus implementieren.

Leider glaube ich nicht, dass das vor C# 7/der nächsten Framework-Version verfügbar sein wird

Ich denke, Sie können es mit C# 2 verwenden, nur nicht mit integrierter Unterstützung.

oder sogar wissen, ob es so performant ist (diese Eigenschafts-/Methodenaufrufe an EqualityComparer können sich summieren)

Was würde diese Klasse anders machen? Wenn der explizite Aufruf von obj == null ? 0 : obj.GetHashCode() schneller ist, sollte dies in ValueTuple verschoben werden.

Ich wäre geneigt gewesen, diesem Vorschlag vor ein paar Wochen +1 zu geben, aber ich bin weniger geneigt, da ValueTuple den Zuweisungsaufwand für den Trick der Verwendung von Tuple dafür reduziert hat, das scheint mir zwischen zwei Stühlen zu fallen: Wenn Sie nichts besonders Spezialisiertes brauchen, können Sie ValueTuple , aber wenn Sie etwas darüber hinaus brauchen, wird eine solche Klasse nicht weit kommen genügend.

Und wenn wir C#7 haben, wird es den syntaktischen Zucker haben, um es noch einfacher zu machen.

@JonHanna

Was würde diese Klasse anders machen? Wenn explizit obj == null aufgerufen wird? 0 : obj.GetHashCode() ist schneller, als das sollte in ValueTuple verschoben werden.

Warum nicht ValueTuple einfach die Klasse Hash , um Hash-Codes zu erhalten? Das würde auch den LOC in der Datei erheblich reduzieren (der derzeit etwa ~2000 Zeilen beträgt).

bearbeiten:

Wenn Sie nichts besonders Spezialisiertes benötigen, können Sie ValueTuple verwenden

Stimmt, aber das Problem ist, dass viele Leute dies möglicherweise nicht erkennen und ihre eigene minderwertige naive Hashing-Funktion implementieren (wie die, die ich oben verlinkt habe).

Dass ich tatsächlich hinterherkommen konnte.

Vermutlich außerhalb des Rahmens dieser Ausgabe. Aber einen Hashing-Namespace zu haben, in dem wir von Experten geschriebene kryptografische und nicht-kryptografische Hochleistungs-Hashes finden, wäre hier ein Gewinn.

Wir mussten zum Beispiel xxHash32, xxHash64, Metro128 und auch Downsampling von 128 auf 64 und von 64 auf 32 Bit selbst codieren. Eine Reihe optimierter Funktionen kann Entwicklern helfen, zu vermeiden, ihre eigenen nicht optimierten und/oder fehlerhaften zu schreiben (ich weiß, wir haben auch einige Fehler in unseren eigenen gefunden); aber immer noch in der Lage sein, je nach Bedarf auszuwählen.

Gerne spenden wir unsere Implementierungen bei Interesse, damit diese von Experten überprüft und weiter optimiert werden können.

@redknightlois Ich würde gerne meine SpookyHash-Implementierung zu einem solchen Versuch hinzufügen.

@svick Vorsicht bei string.GetHashCode(), das ist aus gutem Grund sehr spezifisch für Hash-DoS-Angriffe.

@terrajobst , wie weit ist das in der API-Triage/Review-Warteschlange? Ich denke, es ist eine einfache API, die wir schon immer der Plattform hinzufügen wollten und möglicherweise haben wir jetzt genug kritische Masse, um dies tatsächlich zu tun?

cc: @ellismg

Ich denke, es ist bereit, in seinem aktuellen Zustand zu überprüfen.

@mellinoe Das ist großartig! Ich habe den Vorschlag ein wenig aufgeräumt, um ihn knapper zu machen, und am Ende auch einige Fragen hinzugefügt, die meiner Meinung nach angegangen werden sollten.

@jamesqo Es sollte auch long basieren.

@redknightlois , klingt vernünftig. Ich habe den Vorschlag aktualisiert, um long Überladungen von Combine .

Ist der Vorschlag von @JonHanna nicht gut genug?

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

Wenn es nicht genügend Gründe gibt, warum das nicht gut genug ist, glauben wir nicht, dass es den Schnitt macht.

Abgesehen davon, dass der generierte Code einige Größenordnungen schlechter ist, kann ich mir keinen anderen guten Grund vorstellen. Es sei denn natürlich, es gibt Optimierungen in der neuen Laufzeit, die diesen speziellen Fall berücksichtigen, in diesem Fall ist diese Analyse gegenstandslos. Allerdings habe ich dies auf 1.0.1 versucht.

Lassen Sie es mich an einem Beispiel veranschaulichen.

Nehmen wir an, wir nehmen den tatsächlichen Code, der für ValueTuple und verwenden Konstanten, um ihn aufzurufen.

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

Unter einem optimierenden Compiler sollte es wahrscheinlich keinen Unterschied geben, aber in Wirklichkeit gibt es ihn.

Dies ist der eigentliche Code für ValueTuple

image
Was ist nun hier zu sehen? Zuerst erstellen wir eine Struktur im Stack, dann rufen wir den eigentlichen Hash-Code auf.

Vergleichen Sie es nun mit der Verwendung von HashHelper.Combine was für alle Zwecke die tatsächliche Implementierung von Hash.Combine

image

Ich weiss!!!
Aber lasst uns hier nicht aufhören ... verwenden wir aktuelle Parameter:

        [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

Das Gute, das ist extrem stabil. Aber vergleichen wir es mit der Alternative:

image

Gehen wir jetzt über Bord...

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

Und das Ergebnis ist ziemlich anschaulich

image

Ich kann den tatsächlichen Code, den das JIT für den Aufruf generiert, nicht wirklich überprüfen, aber nur der Prolog und der Epilog reichen aus, um die Aufnahme des Vorschlags zu rechtfertigen.

image

Das Fazit der Analyse ist einfach: Dass der Haltetyp ein struct bedeutet nicht, dass es kostenlos ist :)

Die Aufführung wurde während des Treffens angesprochen. Die Frage ist, ob diese API wahrscheinlich auf dem heißen Weg ist. Um es klar zu sagen, ich sage nicht, dass wir die API nicht haben sollten. Ich sage nur, es sei denn, es gibt ein konkretes Szenario, ist es schwieriger, die API zu entwerfen, weil wir nicht sagen können, "wir brauchen sie für X, daher ist der Maßstab für den Erfolg, ob X sie verwenden kann". Das ist wichtig für APIs, die es Ihnen nicht ermöglichen, etwas Neues zu tun, sondern dasselbe auf optimierte Weise zu tun.

Ich denke, je wichtiger es ist, einen schnellen, qualitativ hochwertigen Hash zu haben, desto wichtiger ist es, den verwendeten Algorithmus auf die Objekte und den wahrscheinlich zu sehenden Wertebereich abzustimmen ein Helfer, desto mehr brauchen Sie einen solchen Helfer nicht zu verwenden.

@terrajobst , Leistung war eine Hauptmotivation für diesen Vorschlag, aber nicht die einzige. Ein dedizierter Typ hilft bei der Auffindbarkeit; selbst mit integrierter Tupelunterstützung in C# 7 wissen Entwickler möglicherweise nicht unbedingt, dass sie mit Wert gleichgesetzt werden. Selbst wenn dies der Fall ist, vergessen sie möglicherweise, dass Tupel GetHashCode überschreiben, und müssen am Ende wahrscheinlich Google fragen, wie GetHashCode in .NET implementiert wird.

Außerdem gibt es ein subtiles Korrektheitsproblem bei der Verwendung von ValueTuple.Create.GetHashCode . Nach 8 Elementen werden nur die letzten 8 Elemente gehasht; der Rest wird ignoriert.

@terrajobst Bei RavenDB hatte die Leistung von GetHashCode einen derartigen Erfolg, dass wir schließlich eine ganze Reihe hochoptimierter Routinen implementiert haben. Sogar Roslyn hat ihr eigenes internes Hashing https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/InternalUtilities/Hash.cs Überprüfen Sie auch die Diskussion zu Roslyn speziell hier: https://github .com/dotnet/coreclr/issues/1619 ... Wenn also Leistung der

Auch das @jamesqo- Problem ist vollständig gültig. Ich musste nicht so viele Hashes kombinieren, aber für 1 Million Fälle gibt es jemanden, der mit diesem über die Klippe gehen wird.

@JonHanna

Ich denke, je wichtiger es ist, einen schnellen, qualitativ hochwertigen Hash zu haben, desto wichtiger ist es, den verwendeten Algorithmus auf die Objekte und den wahrscheinlich zu sehenden Wertebereich abzustimmen ein Helfer, desto mehr brauchen Sie einen solchen Helfer nicht zu verwenden.

Sie sagen also, dass das Hinzufügen einer Helper-Klasse schlecht wäre, da es die Leute ermutigen würde, einfach die Helper-Funktion einzufügen, ohne darüber nachzudenken, wie man einen richtigen Hash macht?

Es scheint tatsächlich das Gegenteil der Fall zu sein; Hash.Combine sollte im Allgemeinen die Implementierungen von GetHashCode verbessern. Leute, die wissen, wie man Hashing durchführt, kann Hash.Combine auswerten, um zu sehen, ob es zu ihrem Anwendungsfall passt. Anfänger, die sich mit Hashing nicht wirklich auskennen, werden Hash.Combine anstatt nur die einzelnen Felder zu xorieren (oder noch schlimmer hinzuzufügen), weil sie nicht wissen, wie man einen richtigen Hash macht.

Wir haben darüber noch ein bisschen diskutiert und du hast uns überzeugt :-)

Noch ein paar Fragen:

  1. Wir müssen entscheiden, wo wir diesen Typ platzieren. Die Einführung eines neuen Namespace erscheint seltsam; System.Numerics könnte aber funktionieren. System.Collections.Generic könnte auch funktionieren, da es die Vergleicher hat und Hashing am häufigsten im Kontext von Sammlungen verwendet wird.
  2. Sollten wir ein zuweisungsfreies Builder-Muster bereitstellen, um eine unbekannte Anzahl von Hash-Codes zu kombinieren?

Am (2) hatte @Eilon folgendes zu sagen:

Als Referenz verwenden ASP.NET Core (und seine Vorgänger und verwandte Projekte) einen HashCodeCombiner: https://github.com/aspnet/Common/blob/dev/src/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs

( @David Fowler hat es vor einigen Monaten im GitHub-Thread erwähnt.)

Und dies ist ein Anwendungsbeispiel: 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 (var-Element in ViewLocationExpanderValues)
{
hashCodeCombiner.Add(item.Key, StringComparer.Ordinal);
hashCodeCombiner.Add(item.Value, StringComparer.Ordinal);
}
}

HashCodeCombiner zurückgeben;
```

Wir haben darüber noch ein bisschen diskutiert und du hast uns überzeugt :-)

🎉

Die Einführung eines neuen Namespace erscheint seltsam; System.Numerics könnte jedoch funktionieren.

Wenn wir uns entscheiden, keinen neuen Namespace hinzuzufügen, ist zu beachten, dass jeder Code mit einer Klasse namens Hash und einer using System.Numerics Direktive mit einem mehrdeutigen Typfehler nicht kompiliert werden kann.

Sollten wir ein zuweisungsfreies Builder-Muster bereitstellen, um eine unbekannte Anzahl von Hash-Codes zu kombinieren?

Das klingt nach einer tollen Idee. Als ein paar erste Vorschläge sollten wir es vielleicht HashBuilder (a la StringBuilder ) nennen und es return this nach jeder Add Methode setzen, um es einfacher zu machen um Hashes hinzuzufügen, wie folgt:

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

@jamesqo bitte aktualisiere den Vorschlag oben, wenn ein Konsens über den Thread besteht. Wir können dann eine abschließende Überprüfung vornehmen. Ihnen vorerst zuweisen, während Sie das Design vorantreiben ;-)

Wenn wir uns entscheiden, keinen neuen Namespace hinzuzufügen, ist zu beachten, dass jeder Code mit einer Klasse namens Hash und einer using System.Numerics Direktive mit einem mehrdeutigen Typfehler nicht kompiliert werden kann.

Hängt vom tatsächlichen Szenario ab. In vielen Fällen wird der Compiler Ihren Typ bevorzugen, da die definierte Namespace-Hierarchie der Kompilierungseinheit durchlaufen wird, bevor die Verwendung von Direktiven in Betracht gezogen wird.

Aber trotzdem: Das Hinzufügen von APIs kann eine bahnbrechende Änderung sein. Dies zu vermeiden ist jedoch unpraktisch, vorausgesetzt, wir wollen vorankommen. 😄 Generell bemühen wir uns, Konflikte zu vermeiden, indem wir beispielsweise nicht zu allgemeine Namen verwenden. Ich denke beispielsweise nicht, dass wir den Typ Hash . Ich denke, HashCode wäre wahrscheinlich besser.

Als ein paar erste Vorschläge sollten wir es vielleicht HashBuilder nennen

In erster Näherung dachte ich daran, die Statik und den Builder in einem einzigen Typ zu kombinieren, etwa so:

``` C#
Namespace System.Collections.Generic
{
öffentliche Struktur HashCode
{
public static int Combine(int hash1, int hash2);
public static int Combine(int hash1, int hash2, int hash3);
öffentliches statisches int Combine(int hash1, int hash2, int hash3, int hash4);
öffentliches statisches int Combine(int hash1, int hash2, int hash3, int hash4, int hash5);
öffentliches statisches int Combine(int hash1, int hash2, int hash3, int hash4, int hash5, int hash6);

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

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

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

    public int Value { get; }
}

}

This allows for code like this:

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

ebenso gut wie:

``` C#
var hashCode = neuer 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 (var-Element in ViewLocationExpanderValues)
{
hashCode.Combine(item.Key, StringComparer.Ordinal);
hashCode.Combine(item.Value, StringComparer.Ordinal);
}
}

HashCode.Value zurückgeben;
```

Die Gedanken?

Ich mag @jamesqos Idee von verketteten Aufrufen (geben Sie this von den Instanzmethoden Combine ).

Ich würde sogar so weit gehen, die statischen Methoden vollständig zu entfernen und nur die Instanzmethoden beizubehalten ...

Combine(long hashCode) wird einfach auf int . Wollen wir das wirklich?
Was ist überhaupt der Anwendungsfall für long Überladungen?

@karelz Bitte entferne sie nicht, Strukturen sind nicht kostenlos. Hashes können in sehr heißen Pfaden verwendet werden, Sie möchten sicherlich keine Anweisungen verschwenden, wenn die statische Methode im Wesentlichen kostenlos wäre. Schauen Sie sich die Analyse des Codes an, in der ich die tatsächlichen Auswirkungen der einschließenden Struktur gezeigt habe.

Wir haben die statische Klasse Hashing , um Namenskonflikte zu vermeiden, und der Code sieht gut aus.

@redknightlois Ich frage mich, ob wir auch bei einer nicht generischen Struktur mit einem int-Feld den gleichen "schlechten" Code erwarten sollten.
Wenn das immer noch 'schlechter' Assemblercode ist, frage ich mich, ob wir JIT verbessern könnten, um hier bessere Arbeit bei den Optimierungen zu leisten. Das Hinzufügen von APIs, nur um ein paar Anweisungen zu sparen, sollte unser letzter Ausweg sein.

@redknightlois Neugierig, generiert das JIT schlechteren Code, wenn die Struktur (in diesem Fall HashCode ) in ein Register passt? Es wird nur int groß sein.

Außerdem habe ich in letzter Zeit viele Pull-Requests in coreclr gesehen, um den um Strukturen generierten Code zu verbessern, und es sieht so aus, als würde dotnet/coreclr#8057 diese Optimierungen ermöglichen. Vielleicht wird der Code, den das JIT generiert, nach dieser Änderung besser?

edit: Ich sehe, @karelz hat meine Punkte hier bereits erwähnt.

@karelz , ich stimme Ihnen zu - vorausgesetzt, das JIT generiert anständigen Code für eine Struktur der Größe int (was meiner Meinung nach der Fall ist, hat beispielsweise ImmutableArray keinen Overhead), dann sind die statischen Überladungen überflüssig und kann entfernt werden.

@terrajobst Noch ein

  • Ich denke, wir können Ihre & meine Ideen ein wenig kombinieren. HashCode scheint ein guter Name zu sein; es muss keine veränderliche Struktur sein, die dem Builder-Muster folgt. Stattdessen kann es sich um einen unveränderlichen Wrapper um int , und jede Combine Operation kann einen neuen HashCode Wert zurückgeben. Beispielsweise
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);
  • Wir sollten nur einen impliziten Operator für die Konvertierung in int damit die Leute nicht den letzten .Value Aufruf haben müssen.
  • Zu Combine , ist das der beste Name? Es klingt beschreibender, aber Add ist kürzer und einfacher zu tippen. ( Mix ist eine weitere Alternative, aber das Tippen ist etwas schmerzhaft.)

    • public void Combine(string text, StringComparison comparison) : Ich glaube nicht, dass das wirklich in den gleichen Typ gehört, da dies nichts mit Strings zu tun hat. Außerdem ist es einfach, StringComparer.XXX.GetHashCode(str) für die seltenen Fälle zu schreiben, in denen Sie dies tun müssen.

    • Wir sollten die langen Überladungen von diesem Typ entfernen und einen separaten HashCode Typ für Longs haben. Etwas wie Int64HashCode oder LongHashCode .

Ich habe eine kleine Beispielimplementierung von Dingen auf TryRoslyn gemacht: http://tinyurl.com/zej9yux

Zum Glück ist es leicht zu überprüfen. Und die gute Nachricht ist, dass es so wie es ist richtig funktioniert

image

Wir sollten nur einen impliziten Operator für die Umwandlung in int haben, damit die Leute nicht den letzten .Value-Aufruf haben müssen.

Wahrscheinlich ist der Code nicht annähernd so einfach, eine implizite Konvertierung würde ihn ein wenig aufräumen. Ich mag immer noch die Idee, auch eine Schnittstelle mit mehreren Parametern haben zu können.

        [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, ist das der beste Name? Es klingt beschreibender, aber Hinzufügen ist kürzer und einfacher zu tippen. (Mix ist eine weitere Alternative, aber das ist etwas schmerzhaft beim Tippen.)

Combine ist der eigentliche Name, der in der Hashing-Community afaik verwendet wird. Und es gibt einem eine klare Vorstellung davon, was es tut.

@jamesqo Es gibt viele Hashing-Funktionen, wir mussten sehr schnelle Versionen von 32bits, 64bits bis 128bits für RavenDB implementieren (und wir verwenden jede einzelne für verschiedene Zwecke).

Wir können in diesem Design mit einem erweiterbaren Mechanismus wie diesem vorwärts denken:

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

Ich weiß nicht, warum das JIT dafür einen sehr nervigen Prolog-Code erstellt. Es sollte nicht, damit es wahrscheinlich optimiert werden kann, wir sollten die JIT-Entwickler danach fragen. Im Übrigen können Sie beliebig viele verschiedene Combiner implementieren, ohne eine einzige Anweisung zu verschwenden. Allerdings ist diese Methode wahrscheinlich für tatsächliche Hash-Funktionen nützlicher als für Kombinierer. cc @CarolEidt @AndyAyersMS

BEARBEITEN: Hier wird laut über einen allgemeinen Mechanismus nachgedacht, um Krypto- und Nicht-Krypto-Hash-Funktionen unter einem einzigen Hash-Konzept zu kombinieren.

@jamesqo

es muss keine veränderliche Struktur sein, die dem Builder-Muster folgt

Ah ja. In diesem Fall komme ich mit diesem Muster gut zurecht. Im Allgemeinen mag ich das Muster der Rückgabe von Instanzen nicht, wenn die Operation einen Nebeneffekt hatte. Es ist besonders schlimm, wenn die API dem unveränderlichen WithXxx Muster folgt. In diesem Fall ist das Muster jedoch im Wesentlichen eine unveränderliche Datenstruktur, sodass das Muster gut funktionieren würde.

Ich denke, wir können Ihre & meine Ideen ein wenig kombinieren.

👍, was ist mit:

``` C#
öffentliche Struktur HashCode
{
öffentlicher statischer HashCode erstellen(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);
}

so gut wie das:

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

if (ViewLocationExpanderValues ​​!= null)
{
foreach (var-Element in ViewLocationExpanderValues)
{
hashCode = hashCode.Combine(item.Key, StringComparer.Ordinal);
hashCode = hashCode.Combine(item.Value, StringComparer.Ordinal);
}
}

HashCode.Value zurückgeben;
```

@terrajobst Gedanken:

  1. Die Factory-Methode Create<T> sollte entfernt werden. Andernfalls gäbe es zwei Möglichkeiten, dasselbe zu schreiben, HashCode.Create(_val) oder new HashCode().Combine(_val) . Außerdem wären unterschiedliche Namen für Create / Combine nicht vergleichsfreundlich, da Sie beim Hinzufügen eines neuen ersten Felds 2 Zeilen ändern müssten.
  2. Ich glaube nicht, dass die Überladung, die einen String/StringComparison akzeptiert, hierher gehört; HashCode hat nichts mit Zeichenfolgen zu tun. Stattdessen sollten wir vielleicht eine GetHashCode(StringComparison) API zum String hinzufügen? (Außerdem sind all dies ordinale Vergleiche, was das Standardverhalten von string.GetHashCode .)
  3. Was ist der Sinn von Value , wenn es bereits einen impliziten Operator für die Konvertierung in int ? Auch dies würde dazu führen, dass unterschiedliche Leute unterschiedliche Dinge schreiben.
  4. Wir müssen die Überladung long in einen neuen Typ verschieben. HashCode wird nur 32 Bit breit sein; es kann nicht lange passen.
  5. Fügen wir einige Überladungen hinzu, die Typen ohne Vorzeichen verwenden, da sie beim Hashing häufiger vorkommen.

Hier ist meine vorgeschlagene 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);
}

Nur mit diesen Methoden kann das Beispiel aus ASP.NET noch geschrieben werden als

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

Was ist der Sinn von Value , wenn es bereits einen impliziten Operator für die Konvertierung in int ? Auch dies würde dazu führen, dass unterschiedliche Leute unterschiedliche Dinge schreiben.

In den Framework Design Guidelines für Operatorüberladungen heißt es:

Erwägen Sie, Methoden mit Anzeigenamen bereitzustellen, die jedem überladenen Operator entsprechen.

Viele Sprachen unterstützen das Überladen von Operatoren nicht. Aus diesem Grund wird empfohlen, dass Typen, die Operatoren überladen, eine sekundäre Methode mit einem geeigneten domänenspezifischen Namen einschließen, die eine äquivalente Funktionalität bietet.

Insbesondere ist F# eine der Sprachen, die es umständlich machen, implizite Konvertierungsoperatoren aufzurufen.


Außerdem glaube ich nicht, dass es so wichtig ist, nur eine Möglichkeit zu haben, Dinge zu tun. Meiner Meinung nach ist es wichtiger, die API komfortabel zu gestalten. Wenn ich nur Hashcodes mit wenigen Werten kombinieren möchte, denke ich, dass HashCode.CombineHashCodes(value1, value2, value3) einfacher, kürzer und verständlicher ist als new HashCode().Combine(value1).Combine(value2).Combine(value3) .

Die Instanzmethoden-API ist immer noch für kompliziertere Fälle nützlich, aber ich denke, der häufigere Fall sollte die einfachere statische Methoden-API haben.

@svick , Ihr Punkt, dass andere Sprachen Operatoren nicht so gut unterstützen, ist legitim. Ich gebe nach, lass uns dann Value hinzufügen.

Ich glaube nicht, dass es so wichtig ist, nur eine Möglichkeit zu haben, Dinge zu tun.

Es ist wichtig. Wenn jemand es auf die eine Weise macht und den Code einer Person liest, die es auf eine andere Weise tut, dann muss er/sie googeln, was der andere Weg tut.

Wenn ich nur Hashcodes mit wenigen Werten kombinieren möchte, denke ich, dass HashCode.CombineHashCodes(value1, value2, value3) einfacher, kürzer und verständlicher ist als der neue HashCode().Combine(value1).Combine(value2).Combine( Wert3).

  • Das Problem bei einer statischen Methode besteht darin, dass wir Überladungen für jede unterschiedliche Arität hinzufügen müssen, da es keine params int[] Überladung gibt, was viel weniger Geld fürs Geld ist. Es ist viel schöner, wenn eine Methode alle Anwendungsfälle abdeckt.
  • Das zweite Formular wird leicht zu verstehen sein, wenn Sie es ein- oder zweimal sehen. Tatsächlich könnte man argumentieren, dass es besser lesbar ist, da es einfacher ist, vertikal zu verketten (und somit die Unterschiede beim Hinzufügen/Entfernen eines Felds minimiert):
public override int GetHashCode()
{
    return new HashCode()
        .Combine(_field1)
        .Combine(_field2)
        .Combine(_field3)
        .Combine(_field4);
}

[@svick] Ich glaube nicht, dass es so wichtig ist, nur einen Weg zu haben, Dinge zu tun.

Ich denke, es ist wichtig, die Anzahl der Möglichkeiten zu minimieren, die Sie tun können, um Verwirrung zu vermeiden. Gleichzeitig ist es unser Ziel, nicht zu 100 % überlappungsfrei zu sein, wenn es hilft, andere Ziele wie Auffindbarkeit, Komfort, Leistung oder Lesbarkeit zu erreichen. Im Allgemeinen ist es unser Ziel, Konzepte und nicht APIs zu minimieren. Mehrere Überladungen sind beispielsweise weniger problematisch als mehrere unterschiedliche Methoden mit unzusammenhängender Terminologie.

Der Grund, warum ich die Factory-Methode hinzugefügt habe, ist, klar zu machen, wie man einen anfänglichen Hash-Code erhält. Das Erstellen der leeren Struktur gefolgt von Combine scheint nicht sehr intuitiv zu sein. Die logische Sache wäre, .ctor hinzuzufügen, aber um Boxen zu vermeiden, müsste es generisch sein, was Sie mit einem .ctor nicht tun können. Eine generische Fabrikmethode ist die nächstbeste Lösung.

Ein schöner Nebeneffekt ist, dass es sehr ähnlich aussieht, wie unveränderliche Datenstrukturen im Framework aussehen. Und beim API-Design bevorzugen wir Konsistenz gegenüber fast allem anderen.

[@svick] Wenn ich nur Hashcodes mit wenigen Werten kombinieren möchte, denke ich, dass HashCode.CombineHashCodes(value1, value2, value3) einfacher, kürzer und verständlicher ist als der neue HashCode().Combine(value1).Combine(value2 ).Kombinieren(Wert3).

Ich stimme @jamesqo zu : Was mir an dem Builder-Muster gefällt, ist, dass es auf eine beliebige Anzahl von Argumenten mit minimalen Leistungseinbußen skaliert wird (falls vorhanden, je nachdem, wie gut unser Inliner ist).

[@jamesqo] Ich glaube nicht, dass die Überladung, die einen String/StringComparison akzeptiert, hierher gehört; HashCode hat nichts mit Strings zu tun

Gutes Argument. Ich habe es hinzugefügt, weil es im Code von @Eilon referenziert wurde. Aus Erfahrung würde ich sagen, dass Saiten sehr verbreitet sind. Auf der anderen Seite bin ich mir nicht sicher, ob es einen Vergleich gibt. Lassen wir es vorerst weg.

[@jamesqo] Wir müssen die lange Überladung auf einen neuen Typ verschieben. HashCode wird nur 32 Bit breit sein; es kann nicht lange passen.

Das ist ein guter Punkt. Brauchen wir überhaupt eine long Version? Ich habe es nur drin gelassen, weil es oben erwähnt wurde und ich nicht wirklich darüber nachgedacht habe.

Nun, da ich es bin, sollten wir nur 32-Bit belassen, denn darum geht es bei .NET GetHashCode() . In diesem Sinne bin ich mir nicht einmal sicher, ob wir die Version uint hinzufügen sollten. Wenn Sie Hashing außerhalb dieses Bereichs verwenden, ist es meiner Meinung nach in Ordnung, die Leute auf die allgemeineren Hashing-Algorithmen hinzuweisen, die wir in System.Security.Cryptography .

```C#
öffentliche Struktur HashCode
{
öffentlicher statischer HashCode erstellen(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);

}
```

Nun, da ich es bin, sollten wir nur 32-Bit belassen, denn darum geht es bei .NET GetHashCode(). In diesem Sinne bin ich mir nicht einmal sicher, ob wir die uint-Version hinzufügen sollten. Wenn Sie Hashing außerhalb dieses Bereichs verwenden, ist es meiner Meinung nach in Ordnung, die Leute auf die allgemeineren Hashing-Algorithmen hinzuweisen, die wir in System.Security.Cryptography haben.

@terrajobst Es gibt ganz unterschiedliche Arten von Hashing-Algorithmen, ein echter Zoo. Tatsächlich sind wahrscheinlich 70 % von Natur aus nicht kryptografisch. Und wahrscheinlich sind weit mehr als die Hälfte davon für 64+ Bits ausgelegt (gemeinsames Ziel ist 128/256). Dass sich das Framework für die Verwendung von 32 Bit entschieden hat (ich war noch nicht dort), liegt daran, dass x86 zu der Zeit noch ein großer Verbraucher war und Hashes überall verwendet werden, sodass die Leistung auf geringerer Hardware von größter Bedeutung war.

Um genau zu sein, sind die meisten Hashfunktionen wirklich über die uint Domäne definiert und nicht über die int da die Regeln für das Verschieben unterschiedlich sind. In der Tat, wenn Sie den Code, den ich zuvor gepostet habe, überprüfen, wird das int aus diesem Grund sofort in ein uint (und verwenden Sie die ror/rol Optimierung). Falls wir streng sein wollen, sollte der einzige Hash uint . Es kann als ein Versehen angesehen werden, dass das Framework unter diesem Licht int zurückgibt.

Die Beschränkung auf int ist nicht besser als das, was wir heute haben. Wenn es mein Anruf wäre, würde ich mich an das Designteam drängen, um zu prüfen, wie wir die Unterstützung von 128- und 256-Varianten und verschiedenen Hash-Funktionen unterstützen können (auch wenn wir Ihnen eine Alternative unter die Finger werfen würden).

Die durch die Vereinfachung verursachten Probleme sind manchmal schlimmer als die Designprobleme, die entstehen, wenn man gezwungen ist, sich mit komplexen Dingen zu befassen. Eine so starke Vereinfachung der Funktionalität, weil Entwickler als not being able to deal with having multiple options wahrgenommen werden, kann leicht auf den Weg zum aktuellen Stand von SIMD führen. Die meisten leistungsbewussten Entwickler können es nicht verwenden, und alle anderen werden es auch nicht verwenden, da die meisten sowieso nicht mit leistungsempfindlichen Anwendungen zu tun haben, die so feine Durchsatzziele haben.

Beim Hashing ist es ähnlich, die Domains, in denen Sie 32 Bit verwenden würden, sind sehr eingeschränkt (die meisten werden bereits vom Framework selbst abgedeckt), für den Rest haben Sie Pech.

image

Sobald Sie mit mehr als 75000 Elementen zu tun haben, haben Sie außerdem eine 50%ige Chance auf eine Kollision, und das ist in den meisten Szenarien schlecht (und das vorausgesetzt, Sie haben eine gut gestaltete Hash-Funktion). Deshalb werden 64 Bit und 128 Bit auch außerhalb der Grenzen von Laufzeitstrukturen verwendet.

Mit einem Design, das auf int feststeckt, behandeln wir nur die Probleme, die dadurch verursacht wurden, dass es die Montagszeitung im Jahr 2000 nicht gab (jetzt schreibt jeder sein schlechtes Hashing selbst), aber wir werden den Zustand der Kunst auch nicht.

Das sind meine 2 Cent für die Diskussion.

@redknightlois , ich denke, wir verstehen die Einschränkungen der int-Hashes. Aber ich stimme @terrajobst zu : Bei dieser Funktion sollte es um APIs gehen, um Hashes zu berechnen, um sie von Object.GetHashCode-Überschreibungen zurückzugeben. Möglicherweise haben wir zusätzlich eine separate Bibliothek für moderneres Hashing, aber ich würde sagen, es sollte eine separate Diskussion sein, da es die Entscheidung beinhalten muss, was mit Object.GetHashCode und allen vorhandenen Hashing-Datenstrukturen zu tun ist.

Es sei denn, Sie denken, dass es immer noch von Vorteil ist, Hash-Kombinationen in 128 Bit durchzuführen und dann in int zu konvertieren, damit das Ergebnis von GetHahsCode zurückgegeben werden kann.

@KrzysztofCwalina Ich stimme zu, dass es sich um zwei verschiedene Ansätze handelt. Eine besteht darin, ein Problem zu beheben, das im Jahr 2000 verursacht wurde; eine andere ist, das allgemeine Hashing-Problem anzugehen. Wenn wir uns alle einig sind, dass dies eine Lösung für ersteres ist, ist die Diskussion beendet. Für eine Designdiskussion für einen Meilenstein der "Zukunft" habe ich jedoch das Gefühl, dass sie zu kurz kommen wird, hauptsächlich weil das, was wir hier tun werden, die zukünftige Diskussion beeinflussen wird. Hier Fehler zu machen, wird sich auswirken.

@redknightlois , ich würde folgendes vorschlagen: Lassen Sie uns eine API entwerfen, als ob wir uns keine Sorgen um die Zukunft machen müssten. Lassen Sie uns dann diskutieren, welche Designentscheidungen unserer Meinung nach Probleme für die zukünftigen APIs verursachen würden. Was wir auch tun könnten, ist die c2000-APIs zu corfx hinzuzufügen und parallel zu versuchen, mit den zukünftigen APIs in corfxlab zu experimentieren, was alle Probleme im Zusammenhang mit solchen Ergänzungen aufdecken sollte, falls wir sie jemals machen wollten.

@redknightlois

Hier Fehler zu machen, wird sich auswirken.

Ich denke, wenn wir in Zukunft fortgeschrittenere Szenarien unterstützen möchten, können wir dies einfach in einem separaten Typ von HashCode tun. Entscheidungen hier sollten diese Fälle nicht wirklich beeinflussen.

Ich habe ein anderes Problem erstellt, um damit zu beginnen.

@redknightlois :+1:. Übrigens, Sie haben geantwortet, bevor ich meinen Kommentar bearbeiten konnte, aber ich habe tatsächlich Ihre Idee (oben) ausprobiert, den Hash mit jedem Typ (int, long, dezimal usw.) arbeiten zu lassen und die Kern-Hashing-Logik in eine Struktur zu kapseln: https://github.com/jamesqo/HashApi (Beispielverwendung war hier ). Aber zwei generische Typparameter zu haben, war viel zu komplex, und die Compiler-Typ-Inferenz funktionierte nicht, als ich versuchte, die API zu verwenden. Also ja, es ist eine gute Idee, das fortgeschrittenere Hashing vorerst in ein separates Problem zu packen.

@terrajobst Die API scheint fast fertig zu sein, aber es gibt noch 1 oder 2 Dinge, die ich ändern möchte.

  • Ursprünglich wollte ich die statische Factory-Methode nicht, da HashCode.Create(x) den gleichen Effekt wie new HashCode().Combine(x) . Aber ich habe meine Meinung geändert, da dies 1 zusätzliches Hash bedeutet. Warum benennen wir stattdessen nicht Create in Combine ? Es scheint irgendwie nervig zu sein, eine Sache für das erste Feld und eine andere für das zweite Feld eingeben zu müssen.
  • Ich denke, wir sollten HashCode IEquatable<HashCode> implementieren und einige der Gleichheitsoperatoren implementieren. Melden Sie sich gerne, wenn Sie Einwände haben.

(hoffentlich) endgültiger Vorschlag:

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 sagte:

Gutes Argument. Ich habe es hinzugefügt, weil es im Code von @Eilon referenziert wurde. Aus Erfahrung würde ich sagen, dass Saiten sehr verbreitet sind. Auf der anderen Seite bin ich mir nicht sicher, ob es einen Vergleich gibt. Lassen wir es vorerst weg.

Es ist eigentlich sehr wichtig: Beim Erstellen von Hashes für Strings muss oft der Zweck dieses Strings berücksichtigt werden, was sowohl seine Kultur als auch seine Groß-/Kleinschreibung umfasst. Beim StringComparer geht es nicht um Vergleiche an sich, sondern darum, spezifische GetHashCode-Implementierungen bereitzustellen, die kultur- und fallbezogen sind.

Ohne diese API müssten Sie etwas Seltsames tun wie:

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

Und das ist randvoll mit Zuweisungen, folgt schlechten Kultur-Sensibilitäts-Mustern usw.

@Eilon in einem solchen Fall würde ich erwarten, dass der Code explizit string.GetHashCode(StringComparison comparison) aufruft, was kultur- und int an Combine .

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

@Eilon , Sie könnten einfach StringComparer.InvariantCultureIgnoreCase.GetHashCode verwenden.

Diese sind sicherlich besser in Bezug auf die Zuweisungen, aber diese Aufrufe sind nicht schön anzusehen ... Wir haben überall in ASP.NET Verwendungen, bei denen Hashes kultur- und case-sensitive Zeichenfolgen enthalten müssen.

Fair genug, wenn man alles oben Gesagte kombiniert, wie wäre es dann mit dieser Form:

``` C#
Namespace System.Collections.Generic
{
öffentliche Struktur HashCode : IEquatable
{
öffentlicher statischer HashCode Combine(int hash);
öffentliche statische HashCode-Kombination(T obj);
öffentliches statisches HashCode Combine (Stringtext, StringComparison-Vergleich);

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

}

// Verwendung:

öffentliche Überschreibung int GetHashCode()
{
HashCode.Combine(_field1) zurückgeben
.Kombinieren(_field2)
.Kombinieren(_field3)
.Kombinieren(_field4);
}
```

es versenden! :-)

@terrajobst _Halten--_ kann Combine(string, StringComparison) einfach als Erweiterungsmethode implementiert werden?

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

Ich würde es viel, viel vorziehen, dass es sich um eine Erweiterungsmethode und nicht um einen Teil der Typsignatur handelt. Wenn Sie oder @Elion jedoch absolut der Meinung sind, dass dies eine integrierte Methode sein sollte, werde ich diesen Vorschlag nicht blockieren.

( Bearbeiten: Auch System.Numerics ist wahrscheinlich ein besserer Namespace, es sei denn, wir haben heute Hash-bezogene Typen in Collections.Generic, die mir nicht bekannt sind.)

LGTM. Ich würde verlängern gehen.

Ja, es könnte eine Erweiterungsmethode sein, aber welches Problem löst sie?

@terrajobst

Ja, es könnte eine Erweiterungsmethode sein, aber welches Problem löst sie?

Ich habe in ASP.NET-Code vorgeschlagen. Wenn es für ihren Anwendungsfall üblich ist, ist das in Ordnung, aber das gilt möglicherweise nicht für andere Bibliotheken/Apps. Wenn sich später herausstellt, dass dies häufig genug ist, können wir es jederzeit neu bewerten und beschließen, es in einen separaten Vorschlag aufzunehmen.

Mhhh das ist sowieso Kern. Einmal definiert, ist es sowieso Teil der Signatur. Streichen Sie den Kommentar. Es ist in Ordnung, wie es ist.

Die Verwendung von Erweiterungsmethoden ist in Fällen nützlich, in denen:

  1. Es ist ein vorhandener Typ, den wir erweitern möchten, ohne ein Update für den Typ selbst versenden zu müssen
  2. Layering-Probleme lösen
  3. Trennen Sie sehr häufig verwendete APIs von viel weniger verwendeten APIs.

Ich glaube nicht, dass (1) oder (2) hier zutreffen. (3) würde nur helfen, wenn wir den Code in eine andere Assembly als HashCode oder in einen anderen Namespace verschieben würden. Ich würde argumentieren, dass Strings häufig genug sind, dass es sich nicht lohnt. Tatsächlich würde ich sogar argumentieren, dass sie so häufig vorkommen, dass es sinnvoller ist, sie als erste Klasse zu behandeln, als zu versuchen, sie künstlich nach einem Erweiterungstyp zu trennen.

@terrajobst , um das vorgeschlagen , die string API ganz aufzugeben und es ASP.NET zu überlassen, ihre eigene Erweiterungsmethode für Zeichenfolgen zu schreiben.

Ich würde argumentieren, dass Strings häufig genug sind, dass es sich nicht lohnt. Tatsächlich würde ich sogar argumentieren, dass sie so häufig vorkommen, dass es sinnvoller ist, sie als erste Klasse zu behandeln, als zu versuchen, sie künstlich nach einem Erweiterungstyp zu trennen.

Ja, aber wie häufig möchte jemand den nicht-ordinalen Hash-Code einer Zeichenfolge abrufen, was das einzige Szenario ist, das die vorhandene Combine<T> Überladung nicht berücksichtigt? (zB Jemand, der StringComparer.CurrentCulture.GetHashCode in seinen Overrides anruft?) Ich kann mich irren, aber ich habe nicht viele gesehen.

Entschuldigung für die Zurückweisung diesbezüglich; Es ist nur so, dass es nach dem Hinzufügen einer API kein Zurück mehr gibt.

ja, aber wie häufig ist es, dass jemand den nicht-ordinalen Hash-Code einer Zeichenfolge erhalten möchte?

Ich mag voreingenommen sein, aber Fallinvarianz ist ziemlich beliebt. Sicher, nicht viele (wenn überhaupt) interessieren sich für kulturspezifische Hash-Codes, aber Hash-Codes, die die Groß-/Kleinschreibung ignorieren, kann ich völlig sehen - und das scheint StringComparison.OrdinalIgnoreCase ).

Entschuldigung für die Zurückweisung diesbezüglich; Es ist nur so, dass es nach dem Hinzufügen einer API kein Zurück mehr gibt.

Kein Scherz 😈 Einverstanden, aber auch wenn die API nicht so oft genutzt wird, ist sie nützlich und schadet nicht.

@terrajobst Ok dann fügen wir es hinzu :+1: Letztes Problem: Ich habe das oben erwähnt, aber können wir den Namespace Numerics statt Collections.Generic machen? Wenn wir in Zukunft weitere Hashing-bezogene Typen hinzufügen würden, wie @redknightlois vorschlägt, wären sie meiner Meinung nach in Sammlungen eine falsche Bezeichnung.

Ich liebe es. 🍔

Ich glaube nicht, dass Hashing konzeptionell in Sammlungen fällt. Was ist mit System.Runtime?

Ich wollte dasselbe oder sogar System vorschlagen. Es ist auch keine Numerik.

@karelz , System.Runtime könnte funktionieren. @redknightlois System wäre praktisch, da Sie diesen Namespace

Wir sollten es nicht in System.Runtime da dies für esoterische und sehr spezielle Fälle gilt. Ich habe mit @KrzysztofCwalina gesprochen und wir denken beide, dass es eine von beiden ist:

  • System
  • System.Collections.*

Wir neigen beide zu System .

Wenn wir eine Begründung dafür brauchen, warum wir uns für System kann ich es mit einer Begründung versuchen. Wir haben HashCode , um bei Implementierungen von object.GetHashCode() HashCode zu helfen. Es klingt passend, dass beide einen Namensraum teilen würden.

@terrajobst Ich denke, System sollte dann der Namespace sein. Lass uns :shipit:

Die API-Spezifikation in der Beschreibung wurde aktualisiert.

[@redknightlois] Wenn wir eine Begründung dafür brauchen, warum wir uns für System kann ich es mit einer Begründung versuchen. Wir haben HashCode , um bei Implementierungen von object.GetHashCode() HashCode zu helfen. Es klingt passend, dass beide einen Namensraum teilen würden.

Das war auch die Begründung, die @KrzysztofCwalina und ich verwendet haben. Verkauft!

@jamesqo

Ich nehme an, Sie wollen die PR auch mit der Umsetzung versorgen?

@terrajobst Ja, definitiv. Vielen Dank, dass Sie sich die Zeit genommen haben, dies zu überprüfen!

Ja definitiv.

Süss. In diesem Fall überlasse ich es Ihnen. Das ist gut mit dir @karelz?

Vielen Dank, dass Sie sich die Zeit genommen haben, dies zu überprüfen!

Vielen Dank, dass Sie sich die Zeit genommen haben, mit uns an der API-Form zu arbeiten. Es kann ein schmerzhafter Prozess sein, hin und her zu gehen. Wir wissen Ihre Geduld sehr zu schätzen!

Und ich freue mich darauf, die ASP.NET Core-Implementierung zu löschen und stattdessen diese zu verwenden 😄

öffentliches statisches HashCode Combine (Stringtext, StringComparison-Vergleich);
public HashCode Combine (Stringtext, StringComparison-Vergleich);

Nit: Die Methoden auf String , die StringComparison (zB Equals , Compare , StartsWith , EndsWith usw.) .) Verwenden Sie comparisonType als Namen des Parameters, nicht comparison . Soll der Parameter auch hier comparisonType heißen, damit er konsistent ist?

@justinvp , das scheint eher ein Type ist überflüssig. Ich denke nicht, dass wir Parameternamen in neuen APIs ausführlicher machen sollten, nur um mit alten "dem Präzedenzfall zu folgen".

Als weiteren Datenpunkt wählte xUnit ebenfalls comparisonType .

@justinvp Du hast mich überzeugt. Jetzt, wo ich intuitiv darüber nachdenke, ist "Groß-/Kleinschreibung" oder "kulturabhängig" eine "Art" des Vergleichs. Ich werde den Namen ändern.

Ich bin mit der Form einverstanden, aber in Bezug auf den StringComparison eine mögliche Alternative:

Nicht enthalten:

``` C#
öffentliches statisches HashCode Combine (Stringtext, StringComparison-Vergleich);
public HashCode Combine (Stringtext, StringComparison-Vergleich);

Instead, add a method:

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

Dann anstatt zu schreiben:

``` C#
öffentliche Überschreibung int GetHashCode()
{
HashCode.Combine(_field1) zurückgeben
.Kombinieren(_field2)
.Kombinieren(_field3)
.Kombinieren(_field4, _comparison);
}

you write:

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

Ja, es ist etwas länger, aber es löst das gleiche Problem, ohne dass zwei spezialisierte Methoden für HashCode benötigt werden (die wir gerade zu System hochgestuft haben), und Sie erhalten eine statische Hilfsmethode, die in anderen, nicht zusammenhängenden Situationen verwendet werden kann. Es hält es auch ähnlich wie Sie es verwenden würden, wenn Sie bereits einen StringComparer haben (da wir nicht über Vergleicherüberladungen sprechen):

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

@stephentoub , FromComparison klingt nach einer guten Idee. Ich habe im Thread tatsächlich nach oben vorgeschlagen, eine string.GetHashCode(StringComparison) API hinzuzufügen, was Ihr Beispiel noch einfacher macht (unter der Annahme, dass eine Zeichenfolge nicht null ist):

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

@Elion sagte, es seien jedoch zu viele Anrufe hinzugefügt worden.

(Bearbeiten: einen Vorschlag für Ihre API gemacht.)

Ich mag es auch nicht, 2 spezialisierte Methoden für HashCode für String hinzuzufügen.
@Eilon Sie haben erwähnt, dass das Muster in ASP.NET Core selbst verwendet wird. Was glauben Sie, wie oft externe Entwickler es verwenden werden?

@jamesqo danke, dass vorangetrieben hast ! Wie @terrajobst sagte, schätzen wir Ihre Hilfe und Geduld. Die Iteration grundlegender kleiner APIs kann manchmal eine Weile dauern :).

Mal sehen, wo wir mit diesem letzten API-Feedback landen, dann können wir mit der Implementierung fortfahren.

Sollte da sein:

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

?

(Entschuldigung, wenn das schon abgetan wurde und ich es hier übersehe).

@stephentoub sagte:

schreiben:

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

Ja, es ist etwas länger, aber es löst das gleiche Problem, ohne dass zwei spezialisierte Methoden für HashCode benötigt werden (die wir gerade zu System hochgestuft haben), und Sie erhalten eine statische Hilfsmethode, die in anderen, nicht zusammenhängenden Situationen verwendet werden kann. Es hält es auch ähnlich wie Sie es verwenden würden, wenn Sie bereits einen StringComparer haben (da wir nicht über Vergleicherüberladungen sprechen):


Nun, es ist nicht nur ein bisschen länger, es ist wie waaay super länger und hat keine Auffindbarkeit.

Was ist der Widerstand gegen das Hinzufügen dieser Methode? Wenn es nützlich ist, klar korrekt implementiert werden kann, keine Mehrdeutigkeit in seiner Funktion hat, warum nicht hinzufügen?

Die zusätzliche statische Hilfs-/Konvertierungsmethode ist in Ordnung - obwohl ich nicht sicher bin, ob ich sie verwenden würde - aber warum auf Kosten von Komfortmethoden?

warum auf Kosten von Convenience-Methoden?

Weil mir nicht klar ist, dass hier Convenience-Methoden wirklich gebraucht werden. Ich verstehe, dass ASP.NET dies an verschiedenen Stellen tut. Wie viele Plätze? Und an wie vielen dieser Stellen ist es tatsächlich eine Variable StringComparison, die Sie haben, anstatt einen bekannten Wert? In diesem Fall benötigen Sie nicht einmal den von mir erwähnten Helfer und könnten einfach Folgendes tun:

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

und ist tatsächlich schneller, da wir nicht in Combine verzweigen müssen, um genau dasselbe zu tun, was der Entwickler hätte schreiben können. Ist der zusätzliche Code so unpraktisch, dass es sich lohnt, für diesen einen Fall spezielle Überladungen hinzuzufügen? Warum nicht für StringComparer überladen? Warum nicht für EqualityComparer überladen? Warum nicht Überladungen, die Func<T, int> benötigen? Irgendwann ziehen Sie die Grenze und sagen "der Wert, den diese Überladung bietet, ist es einfach nicht wert", denn alles, was wir hinzufügen, hat seinen Preis, sei es die Kosten für die Wartung, die Kosten für die Codegröße oder was auch immer , und wenn der Entwickler diesen Fall wirklich benötigt, ist es sehr wenig zusätzlicher Code, den der Entwickler mit weniger spezialisierten Fällen verarbeiten muss. Ich habe also vorgeschlagen, dass der richtige Ort zum Ziehen der Linie vor diesen Überladungen ist und nicht danach (aber wie ich zu Beginn meiner vorherigen Antwort sagte: "Ich bin mit der Form einverstanden" und schlug eine Alternative vor) .

Hier ist die Suche, die ich durchgeführt habe: https://github.com/search?p=2&q=user%3Aaspnet+hashcodecombiner&type=Code&utf8=%E2%9C%93

Von ~100 Übereinstimmungen hat fast jeder Anwendungsfall bereits auf den ersten Seiten Zeichenfolgen und verwendet in einigen Fällen verschiedene Arten von Zeichenfolgenvergleichen:

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

(Und Dutzende andere.)

Es scheint also, dass dies innerhalb der ASP.NET Core-Codebasis sicherlich ein äußerst häufiges Muster ist. Natürlich kann ich mit keinem anderen System sprechen.

Von ~100 Übereinstimmungen

Jeder der 10, die Sie aufgelistet haben (ich habe mir den Rest der Suche nicht angesehen) spezifiziert den String-Vergleich explizit, anstatt ihn aus einer Variablen zu ziehen. Sprechen wir also nicht nur über den Unterschied zwischen zum Beispiel:

``` C#
.Combine(Name, StringComparison.OrdinalIgnoreCase)

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

? Das ist nicht "waaay super länger" und effizienter, es sei denn, ich vermisse etwas.

Wie auch immer, wie gesagt, ich schlage nur vor, dass wir wirklich überlegen, ob diese Überladungen notwendig sind. Wenn die meisten Leute glauben, dass sie es sind, und wir denken nicht nur an unsere eigene ASP.NET-Codebasis, gut.

Was ist das Verhalten, das wir für Nulleingaben planen? Was ist mit int==0? Ich kann mehr Vorteile aus der String-Überladung sehen, wenn wir die Übergabe von null zulassen, da StringComparer.GetHashCode meiner Meinung nach normalerweise eine Null-Eingabe auslöst zu Sonderfällen Nullen. Aber das wirft dann auch die Frage auf, wie sich das Verhalten verhält, wenn null bereitgestellt wird. Wird eine 0 in den Hashcode eingemischt, wie bei jedem anderen Wert? Wird es als Nop behandelt und der Hashcode in Ruhe gelassen?

Ich denke, der beste allgemeine Ansatz für Null besteht darin, eine Null einzumischen. Für ein einzelnes Nullelement, das hinzugefügt wird, wäre es besser, es als Nop zu haben, aber wenn jemand eine Sequenz einfügt, ist es vorteilhafter, 10 Nullen Hash anders als 20 zu haben.

Tatsächlich kommt meine Stimme aus der Perspektive der Codebasis von ASP.NET Core, wo eine Überladung, die Zeichenfolgen erkennt, sehr hilfreich wäre. Die Dinge über die Zeilenlänge waren nicht wirklich mein Hauptanliegen, sondern eher die Auffindbarkeit.

Wenn im System keine zeichenfolgensensitive Überladung verfügbar wäre, fügen wir einfach eine interne Erweiterungsmethode in ASP.NET Core hinzu und verwenden diese.

Wenn im System keine zeichenfolgensensitive Überladung verfügbar wäre, fügen wir einfach eine interne Erweiterungsmethode in ASP.NET Core hinzu und verwenden diese.

Ich denke, das wäre vorerst eine großartige Lösung, bis wir mehr Beweise dafür sehen, dass eine solche API im Allgemeinen auch außerhalb der ASP.NET Core-Codebasis benötigt wird.

Ich muss sagen, dass ich den Wert beim Entfernen der string Überladung nicht sehe. Es reduziert nicht die Komplexität, macht den Code nicht effizienter und hindert uns nicht daran, andere Bereiche zu verbessern, wie beispielsweise die Bereitstellung einer Methode, die ein StringComparer von einem StringComparison zurückgibt

Wir müssen anerkennen, dass Saiten etwas Besonderes und unglaublich verbreitet sind. Durch Hinzufügen einer Überladung, die sie spezialisiert, erreichen wir zwei Dinge:

  1. Wir machen Szenarien wie das von @Eilon viel einfacher.
  2. Wir machen deutlich, dass es wichtig ist, den Vergleich für Saiten, insbesondere Gehäuse, zu betrachten.

Wir müssen auch bedenken, dass gängige Boilerplate-Helfer wie die oben erwähnte Erweiterungsmethode

Wenn das Hauptanliegen jedoch die spezielle Schreibweise string , wie wäre es dann damit:

``` C#
öffentliche Struktur HashCode : IEquatable
{
öffentliche HashCode-Kombination(T obj, IEqualityComparerVergleich);
}

// Verwendung
HashCode.Combine(_numberField) zurückgeben
.Combine(_stringField, StringComparer.OrdinalIgnoreCase);
```

@terrajobst , dein Kompromiss ist ein kluger. Mir gefällt, dass Sie GetHashCode nicht mehr explizit aufrufen oder einen zusätzlichen Satz Klammern mit einem benutzerdefinierten Vergleich verschachteln müssen.

(Bearbeiten: Ich denke, ich sollte es wirklich @JonHanna anschreiben, da er es früher im Thread erwähnt hat? 😄 )

@JonHanna Ja, wir werden auch Null-Eingaben als 0

Entschuldigung, dass ich das Gespräch hier unterbreche. Aber wo soll ich den neuen Typ ablegen? @mellinoe @ericstj @weshaggard , schlagen Sie vor, dass ich eine neue Assembly / ein neues Paket für diesen Typ wie System.HashCode erstelle, oder sollte ich es zu einer vorhandenen Assembly wie System.Runtime.Extensions hinzufügen? Danke.

Wir haben das Assembly-Layout in .NET Core kürzlich ziemlich überarbeitet. Ich schlage vor, es dort zu platzieren, wo die konkreten Vergleiche leben, die System.Runtime.Extensions anzuzeigen scheinen.

@weshaggard?

@terrajobst In Bezug auf den Vorschlag selbst habe ich gerade herausgefunden, dass wir leider nicht sowohl die statischen als auch die Combine benennen können. 😢

Folgendes führt zu einem Compilerfehler, da Instanz- und statische Methoden nicht denselben Namen haben können:

using System;
using System.Collections.Generic;

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

    public static void Combine(int i)
    {
    }
}

Jetzt haben wir 2 Möglichkeiten:

  • Benennen Sie die statischen Überladungen in einen anderen Namen wie Create , Seed usw. um.
  • Verschieben Sie die statischen Überladungen in eine andere statische Klasse:
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);

Ich bevorzuge das zweite. Es ist bedauerlich, dass wir dieses Problem umgehen müssen, aber... Gedanken?

Die Aufteilung der Logik in 2 Typen klingt für mich seltsam - um HashCode , müssen Sie die Verbindung herstellen und stattdessen Hash Klasse

Ich würde eher die Methode Create hinzufügen (oder Seed oder Init ).
Ich würde auch No-Args Overload HashCode.Create().Combine(_field1).Combine(_field2) hinzufügen.

@karelz , ich denke nicht, dass wir eine Factory-Methode hinzufügen sollten, wenn sie nicht denselben Namen hat. Wir sollten nur den parameterlosen Konstruktor new anbieten, da er natürlicher ist. Außerdem kann ich die Leute nicht daran hindern, new HashCode().Combine schreiben, da es sich um eine Struktur handelt.

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

Dies führt eine zusätzliche Kombination mit dem Hash-Code von 0 und _field1 , anstatt direkt aus dem Hash-Code zu initialisieren. Ein Nebeneffekt des aktuellen Hashs, den wir verwenden , besteht jedoch darin, dass 0 als erster Parameter übergeben wird, er wird auf Null gedreht und zu Null addiert. Und wenn 0 mit dem ersten Hash-Code xored wird, wird nur der erste Hash-Code erzeugt. Wenn der JIT also beim konstanten Falten gut ist (und ich glaube, dass er dieses xor weg optimiert), sollte dies in der Tat einer direkten Initialisierung entsprechen.

Vorgeschlagene API (aktualisierte Spezifikation):

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 , haben Sie eine Meinung zu einer Factory-Methode im Vergleich zur Verwendung des Standardkonstruktors? Ich habe herausgefunden, dass der Compiler keine statische Combine Überladung zulässt, da dies mit den Instanzmethoden in Konflikt steht, also haben wir die Wahl zwischen beiden

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

// or, using default constructor

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

Der Vorteil des ersten ist, dass es etwas knapper ist. Der Vorteil des zweiten Felds besteht darin, dass es eine einheitliche Benennung hat, sodass Sie für das erste Feld nichts anderes schreiben müssen.

Eine andere Möglichkeit sind zwei verschiedene Typen, einer mit der Combine Factory, einer mit der Combine Instanz (oder der zweite als Erweiterung des ersten Typs).

Ich bin mir nicht sicher, was ich TBH bevorzugen würde.

@ JonHanna , Ihre zweite Idee, dass die hc.Combine(obj) versucht in diesem Fall, die statische Überladung aufzufangen :

Ich habe vorgeschlagen, eine statische Klasse als Einstiegspunkt ein paar Kommentare oben zu haben, was mich daran erinnert ... @karelz ,

Die Aufteilung der Logik in 2 Typen klingt für mich seltsam - um HashCode zu verwenden, müssen Sie die Verbindung herstellen und stattdessen mit der Hash-Klasse beginnen.

Welche Verbindung müssten die Leute herstellen? Würden wir ihnen nicht zuerst Hash vorstellen, und von dort aus können sie sich dann auf den Weg zu HashCode ? Ich glaube nicht, dass das Hinzufügen einer neuen statischen Klasse ein Problem wäre.

Die Aufteilung der Logik in 2 Typen klingt für mich seltsam - um HashCode zu verwenden, müssen Sie die Verbindung herstellen und stattdessen mit der Hash-Klasse beginnen.

Wir könnten den Top-Level-Typ HashCode beibehalten und die Struktur einfach verschachteln. Dies würde die gewünschte Verwendung ermöglichen, während der "Einstiegspunkt" der API auf einen Typ der obersten Ebene gehalten wird, z. B.:

``` c#
Namensraum-System
{
öffentliche statische Klasse HashCode
{
öffentlicher statischer HashCodeValue Combine(int hash);
öffentliches statisches HashCodeValue Combine(T obj);
öffentliches statisches HashCodeValue Combine(T obj, IEqualityComparerVergleich);

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

}
```

Bearbeiten: Obwohl, wahrscheinlich brauchen wir einen besseren Namen als HashCodeValue für den verschachtelten Typ, wenn wir diesen Weg gehen, da HashCodeValue.Value ein wenig überflüssig ist, nicht dass Value sehr verwendet würde oft. Vielleicht brauchen wir nicht einmal eine Value -Eigenschaft – Sie können die Value über GetHashCode() wenn Sie nicht auf int umwandeln möchten.

@justinvp Was ist jedoch das Problem, wenn man überhaupt zwei separate Typen hat? Dieses System scheint zum Beispiel für LinkedList<T> und LinkedListNode<T> zu funktionieren.

Was ist aber überhaupt das Problem, wenn man zwei verschiedene Typen hat?

Bei zwei Typen der obersten Ebene gibt es zwei Bedenken:

  1. Welcher Typ ist der "Einstiegspunkt" für die API? Wenn die Namen Hash und HashCode , mit welchem ​​beginnen Sie? Das geht aus diesen Namen nicht hervor. Mit LinkedList<T> und LinkedListNode<T> ist ziemlich klar, welcher der Haupteinstiegspunkt ist, LinkedList<T> , und welcher ein Helfer ist.
  2. Verschmutzung des System Namespace. Es ist nicht so besorgniserregend wie (1), aber etwas, das Sie im Hinterkopf behalten sollten, wenn wir erwägen, neue Funktionen im System Namespace bereitzustellen.

Die Verschachtelung hilft, diese Bedenken zu mildern.

@justinvp

Welcher Typ ist der "Einstiegspunkt" für die API? Wenn die Namen Hash und HashCode sind, mit welchem ​​beginnen Sie? Das geht aus diesen Namen nicht hervor. Mit LinkedListund LinkedListNodeEs ist ziemlich klar, welcher der Haupteinstiegspunkt ist, LinkedList, und das ist ein Helfer.

OK, fairer Punkt. Was wäre, wenn wir die Typen Hash und HashValue benennen, keine Verschachtelungstypen? Würde das genug von einer unterjochenden Beziehung zwischen den beiden Typen bedeuten?

Wenn wir dies tun, wird die Factory-Methode noch knapper: Hash.Combine(field1).Combine(field2) . Außerdem ist die Verwendung des Strukturtyps an sich immer noch praktisch. Zum Beispiel möchte jemand eine Liste von Hashes sammeln, und um dies dem Leser mitzuteilen, wird ein List<HashValue> anstelle eines List<int> . Dies funktioniert möglicherweise nicht so gut, wenn wir den Typ verschachtelt machen: List<HashCode.HashCodeValue> (sogar List<Hash.Value> ist auf den ersten Blick etwas verwirrend).

Verschmutzen des System-Namespace. Es ist nicht so besorgniserregend wie (1), aber etwas, das Sie im Hinterkopf behalten sollten, wenn wir erwägen, neue Funktionen im System-Namespace bereitzustellen.

Ich stimme zu, aber ich denke auch, dass es wichtig ist, dass wir uns an Konventionen halten und nicht auf Benutzerfreundlichkeit verzichten. Die einzigen BCL-APIs, die mir einfallen, bei denen wir verschachtelte Typen haben (unveränderliche Sammlungen zählen nicht, sie sind nicht unbedingt Teil des Frameworks) sind List<T>.Enumerator , wo wir die verschachtelten Typen aktiv ausblenden möchten type, weil es für Compiler-Verwendung gedacht ist. Das wollen wir in diesem Fall nicht.

Vielleicht brauchen wir nicht einmal eine Value-Eigenschaft – Sie können den Wert über GetHashCode() abrufen, wenn Sie nicht in int umwandeln möchten.

Darüber habe ich vorhin nachgedacht. Aber woher soll der Benutzer dann wissen, dass der Typ GetHashCode überschreibt oder einen impliziten Operator hat?

Vorgeschlagene API

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

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

    public int Value { get; }

    public static implicit operator int(HashValue hashValue);

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

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

Was wäre, wenn wir die Typen Hash und HashValue benennen, keine Verschachtelungstypen?

Hash scheint mir einfach ein viel zu allgemeiner Name zu sein. Ich denke, wir brauchen HashCode im Namen der Einstiegspunkt-API, da ihr beabsichtigter Zweck darin besteht, GetHashCode() zu implementieren, nicht GetHash() .

jemand möchte vielleicht eine Liste von Hashes sammeln und diese dem Leser als Liste mitteilenwird anstelle einer Liste verwendet. Dies funktioniert möglicherweise nicht so gut, wenn wir den Typ verschachtelt gemacht haben: Liste(sogar Listeist auf den ersten Blick etwas verwirrend).

Dies scheint ein unwahrscheinlicher Anwendungsfall zu sein – wir sind uns nicht sicher, ob wir das Design dafür optimieren sollten.

die einzigen BCL-APIs, die mir einfallen, bei denen wir verschachtelte Typen haben

TimeZoneInfo.AdjustmentRule und TimeZoneInfo.TransitionTime sind Beispiele in der BCL, die absichtlich als verschachtelte Typen hinzugefügt wurden.

@justinvp

Ich denke, wir müssen HashCode im Namen der Einstiegspunkt-API haben, da sein beabsichtigter Zweck darin besteht, GetHashCode() und nicht GetHash() zu implementieren.

Ich verstehe.

Ich habe mir etwas mehr Gedanken gemacht. Es scheint vernünftig, eine verschachtelte Struktur zu haben; Wie Sie bereits erwähnt haben, werden die meisten Leute den tatsächlichen Typ nie sehen. Nur eine Sache: Ich denke, der Typ sollte Seed und nicht HashCodeValue heißen. Der Kontext seines Namens wird bereits durch die enthaltende Klasse impliziert.

Vorgeschlagene API

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

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

            public int Value { get; }

            public static implicit operator int(Seed seed);

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

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

@jamesqo Irgendwelche Einwände oder Implementierungsprobleme mit public readonly int Value stattdessen? Das Problem mit Seed ist, dass es technisch gesehen kein Seed nach dem ersten Mähdrescher ist.

Stimmen Sie auch @justinvp zu , Hash sollte dem Umgang mit Hashes vorbehalten sein. Dies wurde eingeführt, um stattdessen den Umgang mit HashCode vereinfachen.

@redknightlois Um es klar zu

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

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

Das Problem mit Saatgut ist, dass es technisch gesehen kein Saatgut nach dem ersten Mähdrescher ist.

Es ist eine Saat für den nächsten Mähdrescher, der eine neue Saat hervorbringt.

Gibt es Einwände oder Implementierungsprobleme, wenn stattdessen public readonly int Value verwendet wird?

Wieso den? int Value { get; } ist idiomatischer und kann leicht eingefügt werden.

Es ist eine Saat für den nächsten Mähdrescher, der eine neue Saat hervorbringt.

Wäre das nicht ein Sämling? ;)

@jamesqo Nach meiner Erfahrung generieren komplexe

BEARBEITEN: Außerdem wird die Idee gefördert, dass diese Strukturen wirklich unveränderlich sind.

Meiner Erfahrung nach generieren komplexe Codeeigenschaften in der Regel schlechteren Code als Felder (darunter Nicht-Inlines).

Wenn Sie einen einzelnen Nicht-Debug-Build finden, bei dem eine automatisch implementierte Eigenschaft nicht immer eingebunden ist, ist dies ein JIT-Problem und sollte auf jeden Fall behoben werden.

Auch ein schreibgeschütztes Feld eines einzelnen int auf einer Struktur wird direkt in ein Register übersetzt

Es gibt Optimierungen, die zulässig sein könnten, da dies darauf schließen kann, dass es sich um eine schreibgeschützte Datei handelt.

Das Hintergrundfeld dieser Struktur ist schreibgeschützt; die API wird ein Accessor sein.

Ich glaube nicht, dass die Verwendung einer Eigenschaft die Leistung hier in irgendeiner Weise beeinträchtigen wird.

@jamesqo Ich werde das im Hinterkopf behalten, wenn ich die finde. Für leistungsempfindlichen Code verwende ich deswegen einfach keine Eigenschaften mehr (Muskelspeicher an dieser Stelle).

Sie können erwägen, die verschachtelte Struktur "State" statt "Seed" aufzurufen?

@ellismg Klar, danke für den Vorschlag. Ich hatte Mühe, einen guten Namen für die innere Struktur zu finden.

@karelz Ich denke, diese API ist endlich

@jamesqo @JonHanna warum brauchen wir Combine<T>(T obj) statt Combine(object o) ?

warum brauchen wir das Combine(T obj) statt Combine(object o)?

Letzteres würde zuordnen, wenn die Instanz eine Struktur wäre.

ähm, danke für die Klarstellung.

Wir mögen den verschachtelten Typ nicht, weil er das Design zu komplizieren scheint. Das Hauptproblem bestand darin, dass wir die Statik und die Nicht-Statik nicht gleich benennen können. Wir haben zwei Möglichkeiten: die Statik entfernen oder umbenennen. Wir denken, dass das Umbenennen in Create am sinnvollsten ist, da es im Vergleich zur Verwendung des Standardkonstruktors ziemlich lesbaren Code erzeugt.

Sofern es keinen starken Widerstand gibt, haben wir uns für dieses Design entschieden:

```C#
Namensraum-System
{
öffentliche Struktur HashCode : IEquatable
{
öffentlicher statischer HashCode Create(int hashCode);
öffentlicher statischer HashCode erstellen(T obj);
öffentlicher statischer HashCode erstellen(T obj, IEqualityComparerVergleich);

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

}
```

Warten wir einige Tage auf zusätzliches Feedback, um herauszufinden, ob es starkes Feedback zu dem genehmigten Vorschlag gibt. Dann können wir es 'zu gewinnen' machen.

Warum erschwert es das Design? Ich könnte verstehen, dass es schlecht wäre, wenn wir den HashCode.State tatsächlich im Code verwenden müssten (zB um den Typ einer Variablen zu definieren), aber erwarten wir, dass dies häufig der Fall ist? Meistens werde ich entweder den Wert direkt zurückgeben oder in einen int konvertieren und diesen speichern.

Ich denke, die Kombination von Create und Combine ist schlimmer.

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

@terrajobst

Wir denken, dass das Umbenennen in Create am sinnvollsten ist, da es im Vergleich zur Verwendung des Standardkonstruktors ziemlich lesbaren Code erzeugt.

Sofern es keinen starken Widerstand gibt, haben wir uns für dieses Design entschieden:

Ich habe Sie gehört, aber ich hatte einen Gedanken in letzter Minute, während ich an der Implementierung arbeitete ... könnten wir einfach eine statische Eigenschaft Zero / Empty zu HashCode hinzufügen? und lassen Sie dann die Leute Combine von dort anrufen? Das würde uns davon befreien, separate Combine / Create Methoden haben zu müssen.

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

Hält noch jemand das für eine gute Idee? (Ich werde in der Zwischenzeit eine PR einreichen, und wenn die Leute so denken, werde ich es in der PR ändern.)

@jamesqo , ich mag die Idee von Empty/Zero.

Das wäre für mich in Ordnung (keine starke Präferenz zwischen Empty vs. Create Fabrik) ... @weshaggard @bartonjs @stephentoub @terrajobst was meint ihr?

Ich persönlich finde Create() besser; aber HashCode.Empty gefällt mir besser als new HashCode() .

Da es eine Version ohne Operator-New zulässt, und es nicht ausschließt, später zu entscheiden, dass wir Create wirklich als Bootstrapper wollen... ::shrug::.

Das ist das volle Ausmaß meines Pushbacks (auch bekannt als nicht sehr viel).

FWIW Ich würde eher für Create stimmen als für Empty / Zero . Ich beginne lieber mit einem tatsächlichen Wert, als alles an Empty / Zero aufzuhängen. Es fühlt/sieht einfach komisch aus.

Es entmutigt auch Leute, mit Null zu säen, was tendenziell ein schlechter Samen ist.

Ich bevorzuge Erstellen statt Leeren. Es passt zu meiner Denkweise: Ich möchte Hashcode erstellen und zusätzliche Werte einmischen. Der verschachtelte Ansatz würde mir auch gut tun.

Obwohl ich sagen wollte, dass es keine gute Idee war, es leer zu nennen (und das wurde bereits gesagt), denke ich nach einem dritten Gedanken immer noch, dass es keine schlechte Lösung ist. Wie wäre es mit so etwas wie Builder. Obwohl es immer noch möglich ist, Null zu verwenden, rät das Wort irgendwie davon ab, es sofort zu verwenden.

@JonHanna nur um es klarzustellen: Du Create , oder?

Und bei einem vierten Gedanken, wie wäre es mit With statt Create.

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

Verwendungsbeispiel basierend auf der letzten Diskussion (wobei Create möglicherweise durch einen alternativen Namen ersetzt wurde):

```c#
öffentliche Überschreibung 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 Würde zu inkonsistentem Code + mehr Jitting führen, denke ich, b / c von allgemeineren Kombinationen. Wir können dies jederzeit in einer anderen Ausgabe wiederholen, wenn es sich als wünschenswert herausstellt.

Für das, was es wert ist, bevorzuge ich die ursprünglich vorgeschlagene Version, zumindest in Bezug auf die Verwendung (nicht sicher über die Kommentare zu Codegröße, Jitting usw.). Es scheint übertrieben zu sein, eine zusätzliche Struktur und mehr als 10 verschiedene Member für etwas zu haben, das als eine Methode mit einigen Überladungen unterschiedlicher Aktualität ausgedrückt werden könnte. Ich bin auch kein Fan von APIs im fließenden Stil im Allgemeinen, also färbt das vielleicht meine Meinung.

Ich wollte das nicht erwähnen, weil es ein wenig ungewöhnlich ist und ich immer noch nicht sicher bin, wie ich darüber denke, aber hier ist eine andere Idee, nur um sicherzustellen, dass alle Alternativen in Betracht gezogen wurden ...

Was wäre, wenn wir etwas in der Art des veränderlichen HashCodeCombiner "Builders" von ASP.NET Core mit ähnlichen Add Methoden tun würden, aber auch Unterstützung für die Syntax des Sammlungsinitialisierers enthalten?

Verwendung:

```c#
öffentliche Überschreibung int GetHashCode() =>
neuer 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();
    }
}

Es müsste mindestens IEnumerable zusammen mit mindestens einer Add Methode implementieren, um die Syntax des Sammlungsinitialisierers zu aktivieren. IEnumerable könnte explizit implementiert werden, um es vor Intellisense zu verbergen, und GetEnumerator könnte entweder NotSupportedException werfen oder den Hash-Code-Wert als einzelnes kombiniertes Element in der Aufzählung zurückgeben, falls jemand zufällig dazugekommen ist benutze es (was selten wäre).

@justinvp , du hast eine interessante Idee. Ich stimme jedoch respektvoll nicht zu; Ich denke, HashCode sollte unveränderlich bleiben, um Fallstricke mit veränderlichen Strukturen zu vermeiden. Auch IEnumerable dafür implementieren zu müssen, erscheint irgendwie künstlich/flockig; Wenn jemand eine using System.Linq Direktive in der Datei hat, dann werden Cast<> und OfType<> als Erweiterungsmethoden angezeigt, wenn sie einen Punkt neben ein HashCode . Ich denke, wir sollten näher am aktuellen Vorschlag bleiben.

@jamesqo , ich stimme zu - daher

@MadsTorgersen , @jaredpar , warum der Sammlungsinitialisierer die Implementierung von IEnumerable erfordert\Der dritte Kommentar von @justinvp oben.

@jamesqo , ich stimme zu, dass es besser ist, dies unveränderlich zu halten (und nicht IEnumerable\

@mellinoe Ich denke, das würde den einfachen Fall etwas einfacher machen, aber auch alles darüber hinaus komplizierter (und weniger klar, was das Richtige ist).

Das beinhaltet:

  1. mehr Gegenstände als du Überladungen hast
  2. Bedingungen
  3. Schleifen
  4. Vergleich verwenden

Betrachten Sie den Code von ASP.NET, der zuvor zu diesem Thema gepostet wurde (aktualisiert auf den aktuellen Vorschlag):

```c#
var hashCode = HashCode
.Create(IsMainPage)
.Combine(ViewName, StringComparer.Ordinal)
.Combine(ControllerName, StringComparer.Ordinal)
.Combine(AreaName, StringComparer.Ordinal);

if (ViewLocationExpanderValues ​​!= null)
{
foreach (var-Element in ViewLocationExpanderValues)
{
hashCode = hashCode
.Kombinieren(item.Key, StringComparer.Ordinal)
.Combine(item.Value, StringComparer.Ordinal);
}
}

HashCode zurückgeben;

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;

Selbst wenn Sie den Aufruf von GetHashCode() für benutzerdefinierte Vergleiche ignorieren, finde ich es nicht einfach, den vorherigen Wert von hashCode als ersten Parameter übergeben zu müssen.

@KrzysztofCwalina Laut @ericlipperts Anmerkung in The C# Programming Language 1 liegt dies daran, dass Sammlungsinitialisierer (nicht überraschend) als Syntaxzucker für die Sammlungserstellung gedacht sind, nicht für die Arithmetik (was die andere übliche Verwendung der Methode namens Add ).

1 Aufgrund der Funktionsweise von Google Books funktioniert dieser Link möglicherweise nicht bei jedem.

@KrzysztofCwalina , und beachten Sie, dass es nicht generisch IEnumerable erfordert, nicht IEnumerable<T> .

@svick , kleiner .Combine wäre .Create mit dem aktuellen Vorschlag. Es sei denn, wir verwenden den verschachtelten Ansatz.

@svick

es würde auch alles darüber hinaus komplizierter machen (und weniger klar machen, was das Richtige ist)

Ich weiß nicht, das zweite Beispiel unterscheidet sich insgesamt kaum vom ersten und ist IMO nicht komplexer. Beim zweiten/ursprünglichen Ansatz übergeben Sie einfach eine Reihe von Hash-Codes (ich denke, der erste Parameter sollte eigentlich IsMainPage.GetHashCode() ), daher scheint es mir einfach. Aber es scheint, als ob ich hier in der Minderheit bin, also werde ich nicht auf den ursprünglichen Ansatz drängen. Ich habe keine starke Meinung; beide Beispiele erscheinen mir vernünftig genug.

@justinvp Danke, aktualisiert. (Ich ging mit dem ersten Vorschlag im ersten Beitrag und wusste nicht, dass er veraltet ist, jemand sollte ihn wahrscheinlich aktualisieren.)

@mellinoe das Problem ist eigentlich, dass die zweite subtile Fehler generieren kann. Dies ist tatsächlicher Code aus einem unserer Projekte.

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

Wir leben damit, aber wir haben es jeden Tag mit sehr niedrigen Dingen zu tun; Also nicht der durchschnittliche Entwickler, das ist sicher. Allerdings ist es hier nicht dasselbe, v mit w zu kombinieren als w mit v ... dasselbe zwischen v und w kombiniert. Hash-Kombinationen sind nicht kommutativ, sodass eine Verkettung nach der anderen tatsächlich eine ganze Reihe von Fehlern auf API-Ebene beseitigen kann.

Ich ging mit dem ersten Vorschlag im ersten Beitrag und wusste nicht, dass er veraltet ist, jemand sollte ihn wahrscheinlich aktualisieren.

Fertig.
Übrigens: Dieser Vorschlag ist sehr schwer zu verfolgen, vor allem die Stimmen ... so viele Variationen (was ich gut finde ;-))

@karelz Wenn wir Create APIs hinzufügen, dann können wir meiner Meinung nach immer noch Empty hinzufügen. Es muss nicht das eine oder das andere sein, wie @bartonjs sagte. Vorgeschlagen

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

Es entmutigt auch Leute, mit Null zu säen, was tendenziell ein schlechter Samen ist.

Der von uns gewählte Hashing-Algorithmus wird derselbe sein, der heute in HashHelpers wird, was den Effekt hat, dass hash(0, x) == x . HashCode.Empty.Combine(x) liefert genau die gleichen Ergebnisse wie HashCode.Create(x) , also gibt es objektiv keinen Unterschied.

@jamesqo du hast vergessen, die zusätzlichen Zero in deinem letzten Vorschlag anzugeben . Wenn das eine Unterlassung war, können Sie es aktualisieren? Wir können dann die Leute bitten, für Ihren neuesten Vorschlag zu stimmen. Sieht so aus, als ob die anderen Alternativen (siehe den oberen Beitrag, den ich aktualisiert habe) nicht so viele Folgen haben ...

@karelz Danke fürs

@KrzysztofCwalina , um zu überprüfen, ob Sie "Hinzufügen" im Sinne des Hinzufügens zu einer Sammlung meinen, nicht in einem anderen Sinne. Ich weiß nicht, ob mir diese Einschränkung gefällt, aber so haben wir uns damals entschieden.

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

Sollte der Parameter hashCode anstelle von hash heißen, da der übergebene Wert ein Hash-Code ist, der wahrscheinlich durch den Aufruf von GetHashCode() ?

Empty / Zero

Wenn wir dies am Ende behalten, ist Default anderer zu berücksichtigender Name.

@justinvp

Sollte der Parameter hashCode anstelle von hash heißen, da der übergebene Wert ein Hashcode ist, der wahrscheinlich durch den Aufruf von GetHashCode() erhalten wird?

Ich wollte die int-Parameter hash und die HashCode Parameter hashCode benennen. Beim zweiten Nachdenken glaube ich jedoch, dass hashCode besser wäre, weil, wie Sie erwähnt haben, hash ziemlich vage ist. Ich werde die API aktualisieren.

Wenn wir dies am Ende behalten, ist ein anderer zu berücksichtigender Name Default.

Wenn ich Default höre, denke ich an "die normale Methode, etwas zu tun, wenn Sie nicht wissen, welche Option Sie wählen sollen", nicht an "den Standardwert einer Struktur". zB Encoding.Default hat eine ganz andere Bedeutung.

Der von uns gewählte Hashing-Algorithmus wird derselbe sein, der heute in HashHelpers verwendet wird, was den Effekt hat, dass hash(0, x) == x. HashCode.Empty.Combine(x) erzeugt genau die gleichen Ergebnisse wie HashCode.Create(x), daher gibt es objektiv keinen Unterschied.

Als jemand, der nicht viel über die Interna weiß, mag ich die Einfachheit von HashCode.Create(x).Combine(...) . Create ist sehr offensichtlich, da es an vielen anderen Stellen verwendet wird.

Wenn Empty / Zero / Default keine algorithmische Verwendung bereitstellt, sollte es IMO nicht vorhanden sein.

PS: sehr interessanter Thread!! Gut gemacht! 👍

@cwe1ss

Wenn Empty / Zero / Default keine algorithmische Verwendung bietet, sollte es IMO nicht vorhanden sein.

Ein Empty Feld bietet eine algorithmische Verwendung. Es stellt einen "Startwert" dar, ab dem Sie Hashes kombinieren können. Wenn Sie beispielsweise ein Array von Hashes ausschließlich mit Create kombinieren möchten, ist dies ziemlich mühsam:

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

Wenn Sie Empty , wird es viel natürlicher:

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 Dieser Typ ist für mich ziemlich analog zu ImmutableArray<T> . Ein leeres Array allein ist nicht sehr nützlich, aber als "Startpunkt" für andere Operationen sehr nützlich, und deshalb haben wir eine Empty Eigenschaft dafür. Ich denke, es wäre auch sinnvoll, einen für HashCode zu haben; wir behalten Create .

@jamesqo Mir ist aufgefallen, dass Sie in Ihrem Vorschlag https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653 stillschweigend/aus Versehen den Argnamen obj in value geändert haben. Ich habe es wieder auf obj umgestellt, was IMO besser erfasst, was Sie bekommen. Der Name value ist in diesem Zusammenhang eher mit dem "int"-Hashwert selbst verbunden.
Ich bin offen für weitere Diskussionen über den Arg-Namen, falls erforderlich, aber ändern wir ihn absichtlich und verfolgen Sie den Unterschied zum zuletzt genehmigten Vorschlag.

Ich habe den Vorschlag oben aktualisiert. Ich habe auch den Unterschied zur letzten genehmigten Version des Vorschlags ausgerufen.

Der von uns gewählte Hashing-Algorithmus wird der gleiche sein, der heute in HashHelpers verwendet wird

Warum ist es ein guter Algorithmus, den zu wählen, der überall verwendet werden sollte? Welche Annahme wird hinsichtlich der Kombination der Hashcodes gemacht? Wenn es überall eingesetzt wird, eröffnet es neue Wege für DDoS-Angriffe? (Beachten Sie, dass wir in der Vergangenheit davon für String-Hashing verbrannt wurden.)

Was wäre, wenn wir etwas in der Art des veränderlichen HashCodeCombiner-"Builders" von ASP.NET Core tun würden?

Ich denke, das ist das richtige Muster. Ein guter universeller Hashcode-Combiner kann im Allgemeinen mehr Zustände verwenden, als in den Hashcode selbst passt, aber dann bricht das fließende Muster zusammen, da das Herumgeben größerer Strukturen ein Leistungsproblem darstellt.

Warum ist es ein guter Algorithmus, den zu wählen, der überall verwendet werden sollte?

Es sollte nicht überall verwendet werden. Siehe meinen Kommentar unter https://github.com/dotnet/corefx/issues/8034#issuecomment -260790829; es richtet sich hauptsächlich an Leute, die nicht viel über Hashing wissen. Leute, die wissen, was sie tun, können es bewerten, um zu sehen, ob es ihren Bedürfnissen entspricht.

Welche Annahme wird hinsichtlich der Kombination der Hashcodes gemacht? Wenn es überall eingesetzt wird, eröffnet es neue Wege für DDoS-Angriffe?

Ein Problem mit dem aktuellen Hash, den wir haben, ist, dass hash(0, x) == x . Wenn dem Hash also eine Reihe von Nullen oder Nullen zugeführt wird, bleibt er 0. Siehe Code . Das soll nicht heißen, dass Nullen nicht zählen, aber keine der anfänglichen Nullen. Ich erwäge, etwas Robusteres (aber etwas teureres) wie hier zu verwenden , das eine magische Konstante hinzufügt, um eine Zuordnung von Null zu Null zu vermeiden.

Ich denke, das ist das richtige Muster. Ein guter universeller Hashcode-Combiner kann im Allgemeinen mehr Zustände verwenden, als in den Hashcode selbst passt, aber dann bricht das fließende Muster zusammen, da das Herumgeben größerer Strukturen ein Leistungsproblem darstellt.

Ich denke nicht, dass es einen universellen Combiner mit einer großen Struktur geben sollte, der versucht, jeden Anwendungsfall zu erfüllen. Stattdessen stellte ich mir separate Hash-Code-Typen vor, die alle eine Int-Größe haben ( FnvHashCode usw.) und alle ihre eigenen Combine Methoden haben. Außerdem werden diese "Builder"-Typen sowieso in der gleichen Methode gehalten und nicht weitergegeben.

Ich denke nicht, dass es einen universellen Combiner mit einer großen Struktur geben sollte, der versucht, jeden Anwendungsfall zu erfüllen.

Wird ASP.NET Core in der Lage sein, seinen eigenen Hashcode-Combiner - der derzeit 64-Bit-Zustand hat - durch diesen zu ersetzen?

Ich habe mir separate Hash-Code-Typen vorgestellt, die alle die Größe Int haben (FnvHashCode usw.)

Führt dies nicht zu einer kombinatorischen Explosion? Es sollte Teil des API-Vorschlags sein, um deutlich zu machen, wozu dieses API-Design führt.

@jkotas Ähnliche Einwände geäußert . Der Umgang mit Hashfunktionen erfordert Fachkenntnisse. Aber ich verstehe und unterstütze die Behebung des Problems, das 2001 mit der Einführung von Hash-Codes an der Wurzel des Frameworks verursacht wurde, und verschreibe kein Rezept zum Kombinieren von Hashes. Dieses Design zielt darauf ab, dies in 99% der Fälle zu lösen (in denen kein Fachwissen vorhanden oder sogar erforderlich ist, da die statistischen Eigenschaften des Hashs gut genug sind). ASP.Net Core sollte in der Lage sein, solche Combiner in ein Allzweck-Framework auf einer Nicht-System-Assembly einzuschließen, wie sie hier zur Diskussion vorgeschlagen wird: https://github.com/dotnet/corefx/issues/13757

Ich stimme zu, dass es eine gute Idee ist, einen Hashcode-Combiner zu haben, der in 99% der Fälle einfach zu verwenden ist. Es muss jedoch mehr internen Status als nur 32-Bit zulassen.

Übrigens: ASP.NET hat ursprünglich das fließende Muster für die Hashcode-Kombination verwendet, es jedoch nicht mehr verwendet, da es zu leicht zu übersehenden Fehlern führte: https://github.com/aspnet/Razor/pull/537

@jkotas bezüglich der Hash-Flooding-Sicherheit.
HAFTUNGSAUSSCHLUSS: Kein Experte (Sie sollten einen konsultieren und MS haben mehr als ein paar zu diesem Thema) .

Ich habe mich umgesehen und obwohl es nicht den allgemeinen Konsens zu diesem Thema gibt, gibt es ein Argument, das heutzutage an Bedeutung gewinnt. Hash-Codes haben eine Größe von 32 Bit. Ich habe vor einem Diagramm gepostet, das die Wahrscheinlichkeit von Kollisionen angesichts der Größe des Satzes zeigt. Das bedeutet, dass Ihr Algorithmus, egal wie gut Ihr Algorithmus ist (zum Beispiel SipHash), ziemlich praktikabel ist, viele Hashes zu generieren und Kollisionen in einer vernünftigen Zeit (in weniger als einer Stunde) zu finden. Diese Probleme müssen an der Datenstruktur, die die Hashes enthält, angegangen werden, sie können nicht auf der Hash-Funktionsebene gelöst werden. Die Zahlung zusätzlicher Leistung für nicht-kryptografische Daten zum Schutz gegen Hash-Flooding, ohne die zugrunde liegende Datenstruktur zu reparieren, wird das Problem nicht lösen.

EDIT: Du hast gepostet, während ich geschrieben habe. Was bringt Ihnen der 64-Bit-Zustand vor diesem Hintergrund?

@jkotas Ich habe das von dir verlinkte Problem untersucht. Es sagt:

Reaktion auf aspnet/Common#40

Beschreibung von https://github.com/aspnet/Common/issues/40 :

Finde den Fehler:

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

Komm schon. Dieses Argument ist, als würde man sagen, dass string sollte, da die Leute nicht erkennen, dass Substring einen neuen String zurückgibt. Mutable structs sind weitaus schlimmer in Bezug auf Fallstricke; Ich denke, wir sollten die Struktur unveränderlich halten.

bezüglich der Hash-Flooding-Sicherheit.

Dies hat zwei Seiten: Korrektes Design (robuste Datenstrukturen usw.); und Minderung der Probleme im bestehenden Design. Beides ist wichtig.

@karelz Zur Parameterbenennung

Mir ist aufgefallen, dass Sie in Ihrem Vorschlag dotnet/corefx#8034 (Kommentar) stillschweigend/versehentlich den Arg-Namen obj in den Wert geändert haben. Ich habe es wieder auf obj umgestellt, welches IMO besser erfasst, was Sie bekommen. Der Name-Wert ist in diesem Zusammenhang eher mit dem "int"-Hash-Wert selbst verbunden.
Ich bin offen für weitere Diskussionen über den Arg-Namen, falls erforderlich, aber ändern wir ihn absichtlich und verfolgen Sie den Unterschied zum zuletzt genehmigten Vorschlag.

Ich erwäge, in einem zukünftigen Vorschlag APIs hinzuzufügen, um Werte in großen Mengen zu kombinieren. Beispiel: CombineRange(ReadOnlySpan<T>) . Wenn wir das obj , müssten wir den Parameter dort objs , was sehr umständlich klingt. Wir sollten es stattdessen item ; in Zukunft können wir den Span-Parameter items . Angebot aktualisiert.

@jkotas stimme zu, aber der Punkt hier ist, dass wir nichts auf Combiner-Ebene abschwächen ...

Das einzige, was wir tun können, ist einen zufälligen Seed, der für alle Zustände und Zwecke, an den ich mich erinnere, den Code bei string und er pro Build behoben wird. (könnte falsch sein, denn das ist aber schon lange her). Eine ordnungsgemäße Implementierung von Random Seeds ist die einzige Abschwächung, die hier angewendet werden könnte.

Dies ist eine Herausforderung, geben Sie mir Ihre beste String- und / oder Speicher-Hash-Funktion mit einem festen Zufalls-Seed und ich werde einen Satz auf 32-Bit-Hash-Codes konstruieren, der nur Kollisionen erzeugt. Ich habe keine Angst vor einer solchen Herausforderung, weil es ziemlich einfach ist, die Wahrscheinlichkeitstheorie ist auf meiner Seite. Ich würde sogar hingehen und eine Wette abschließen, aber ich weiß, dass ich gewinnen werde, also ist es im Wesentlichen keine Wette mehr.

Darüber hinaus ... zeigt eine eingehendere Analyse, dass, selbst wenn die Minderung darin besteht, diese "zufälligen Seeds" pro Durchlauf eingebaut zu haben, kein komplizierterer Combiner erforderlich ist. Denn im Wesentlichen haben Sie das Problem an der Quelle entschärft.

Angenommen, Sie haben M1 und M2 mit verschiedenen zufälligen Samen rs1 und rs2 ....
M1 gibt h1 = hash('a', rs1) und h2=hash('b', rs1)
M2 gibt h1' = hash('a', rs2) und h2'=hash('b', rs2)
Der entscheidende Punkt hier ist, dass h1 und h1' sich mit einer Wahrscheinlichkeit von 1/ (int.MaxInt-1) (wenn hash gut genug ist), was für alle Zwecke gleich ist gut wie es wird.
Daher berücksichtigt jedes c(x,y) Sie sich entscheiden (wenn es gut genug ist), bereits die an der Quelle integrierte Minderung.

EDIT: Ich habe den Code gefunden, Sie verwenden Marvin32, die sich jetzt auf jeder Domain ändern. Die Abschwächung für Strings besteht also darin, zufällige Seeds pro Lauf zu verwenden. Was, wie ich bereits sagte, gut genug ist, um eine Abschwächung zu erreichen.

@jkotas

Wird ASP.NET Core in der Lage sein, seinen eigenen Hashcode-Combiner - der derzeit 64-Bit-Zustand hat - durch diesen zu ersetzen?

Absolut; es verwendet den gleichen Hashing-Algorithmus. Ich habe gerade diese Test-App erstellt , um die Anzahl der Kollisionen zu messen, und sie 10 Mal ausgeführt. Kein signifikanter Unterschied zur Verwendung von 64 Bit.

Ich habe mir separate Hash-Code-Typen vorgestellt, die alle die Größe Int haben (FnvHashCode usw.)

Führt dies nicht zu einer kombinatorischen Explosion? Es sollte Teil des API-Vorschlags sein, um deutlich zu machen, wozu dieses API-Design führt.

@jkotas , wird es nicht. Das Design dieser Klasse wird das Design für zukünftige Hashing-APIs nicht in Stein gemeißelt. Diese sollten als fortgeschrittenere Szenarien betrachtet werden, sollten in einen anderen Vorschlag wie dotnet/corefx#13757 aufgenommen werden und werden eine andere Designdiskussion haben. Ich glaube, es ist viel wichtiger, eine einfache API für einen allgemeinen Hashing-Algorithmus zu haben, für Neulinge, die Schwierigkeiten haben, GetHashCode zu überschreiben.

Ich stimme zu, dass es eine gute Idee ist, einen Hashcode-Combiner zu haben, der in 99% der Fälle einfach zu verwenden ist. Es muss jedoch mehr internen Status als nur 32-Bit zulassen.

Wann brauchen wir mehr internen Zustand als 32 Bit? Bearbeiten: Wenn es den Leuten ermöglichen soll, benutzerdefinierte Hashing-Logik einzufügen, sollte dies (wieder) als fortgeschrittenes Szenario betrachtet und in dotnet/corefx#13757 diskutiert werden.

Sie verwenden Marvin32, die sich jetzt auf jeder Domain ändern

Richtig, die Minderung der Zeichenfolgen-Hashcode-Randomisierung ist in .NET Core standardmäßig aktiviert. Es ist aus Kompatibilitätsgründen nicht standardmäßig für eigenständige Apps im vollständigen .NET Framework aktiviert. es wird nur durch Macken aktiviert (zB in Umgebungen mit hohem Risiko).

Wir haben immer noch den Code für nicht-randomisiertes Hashing in .NET Core, aber es sollte in Ordnung sein, ihn zu löschen. Ich gehe nicht davon aus, dass wir es noch einmal brauchen werden. Es würde auch die String-Hashcode-Berechnung etwas schneller machen, da nicht mehr geprüft wird, ob der nicht randomisierte Pfad verwendet werden soll.

Der Marvin32-Algorithmus, der zum Berechnen der randomisierten String-Hashcodes verwendet wird, hat einen internen 64-Bit-Zustand. Es wurde von den MS-Fachexperten ausgewählt. Ich bin mir ziemlich sicher, dass sie einen guten Grund hatten, den internen 64-Bit-Zustand zu verwenden, und sie haben ihn nicht nur verwendet, um die Dinge zu verlangsamen.

Ein Allzweck-Hash-Kombinierer sollte diese Abschwächung weiterentwickeln: Er sollte einen zufälligen Seed und einen ausreichend starken Hashcode-Kombinationsalgorithmus verwenden. Idealerweise würde es das gleiche Marvin32 als randomisiertes String-Hashing verwenden.

Der Marvin32-Algorithmus, der zum Berechnen der randomisierten String-Hashcodes verwendet wird, hat einen internen 64-Bit-Zustand. Es wurde von den MS-Fachexperten ausgewählt. Ich bin mir ziemlich sicher, dass sie einen guten Grund hatten, den internen 64-Bit-Zustand zu verwenden, und sie haben ihn nicht nur verwendet, um die Dinge zu verlangsamen.

@jkotas , der von Ihnen verlinkte Hashcode-Combiner verwendet kein Marvin32. Es verwendet den gleichen naiven DJBx33x-Algorithmus, der von nicht randomisierten string.GetHashCode .

Ein Allzweck-Hash-Kombinierer sollte diese Abschwächung weiterentwickeln: Er sollte einen zufälligen Seed und einen ausreichend starken Hashcode-Kombinationsalgorithmus verwenden. Idealerweise würde es das gleiche Marvin32 als randomisiertes String-Hashing verwenden.

Dieser Typ ist nicht für die Verwendung an Orten vorgesehen, die anfällig für Hash-DoS-Angriffe sind. Dies richtet sich an Personen, die es nicht besser wissen,/xor hinzuzufügen, und hilft, Dinge wie https://github.com/dotnet/coreclr/pull/4654 zu verhindern.

Ein Allzweck-Hash-Kombinierer sollte diese Abschwächung weiterentwickeln: Er sollte einen zufälligen Seed und einen ausreichend starken Hashcode-Kombinationsalgorithmus verwenden. Idealerweise würde es das gleiche Marvin32 als randomisiertes String-Hashing verwenden.

Dann sollten wir mit dem C#-Team sprechen, damit es einen abgeschwächten ValueTuple Hashing-Algorithmus implementiert. Weil dieser Code auch in Umgebungen mit hohem Risiko verwendet wird. Und natürlich Tuple https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Tuple.cs#L60 oder System.Numerics.HashHelpers (überall in der Ort).

Bevor wir dann entscheiden, wie wir ihn implementieren, würde ich mich mit den gleichen Fachexperten befassen, ob es sich lohnt, die Kosten für einen vollständig randomisierten Hashcode-Kombinationsalgorithmus zu zahlen (wenn er natürlich existiert), obwohl dies die API nicht ändern würde entweder entworfen (unter der vorgeschlagenen API können Sie einen 512-Bit-Zustand verwenden und haben immer noch die gleiche öffentliche API, wenn Sie natürlich bereit sind, die Kosten dafür zu zahlen).

Dies ist für Leute gedacht, die es nicht besser wissen, hinzuzufügen/xor

Genau deshalb ist es wichtig, dass es robust ist. Der Schlüsselwert von .NET besteht darin, dass es Probleme für Leute angeht, die es nicht besser wissen.

Und wenn wir schon dabei sind, vergessen wir nicht IntPtr https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/IntPtr.cs#L119
Das ist besonders schlimm, xor ist wahrscheinlich das Schlimmste, weil bad mit dab .

Implementieren eines abgeschwächten ValueTuple Hashing-Algorithmus

Guter Punkt. Ich bin mir nicht sicher, ob ValueTuple ausgeliefert wurde oder ob dies noch Zeit ist, dies zu tun. Geöffnet https://github.com/dotnet/corefx/issues/14046.

vergessen wir nicht IntPtr

Das sind Fehler der Vergangenheit ... die Messlatte für deren Behebung liegt viel höher.

@jkotas

Das sind Fehler der Vergangenheit ... die Messlatte für deren Behebung liegt viel höher.

Ich dachte, einer der Punkte von .Net Core ist, dass die Messlatte für solche "kleinen" Änderungen viel niedriger sein sollte. Wenn jemand auf die Implementierung von IntPtr.GetHashCode angewiesen ist (was er wirklich nicht sollte), kann er seine Version von .Net Core nicht aktualisieren.

die Latte für "kleine" Änderungen wie diese sollte viel niedriger sein

Ja, das ist es – verglichen mit dem vollständigen .NET Framework. Aber Sie müssen immer noch die Arbeit machen, um die Änderung durch das System zu bringen, und Sie werden möglicherweise feststellen, dass es den Aufwand einfach nicht wert ist. Jüngstes Beispiel ist die Änderung des Tuple<T> Hashing-Algorithmus, der zurückgesetzt wurde, weil er F# kaputt gemacht hat: https://github.com/dotnet/coreclr/pull/6767#issuecomment -256896016

@jkotas

Wenn wir HashCode 64-Bit machen würden, glauben Sie, dass ein unveränderliches Design die Leistungsfähigkeit in 32-Bit-Umgebungen zerstören würde? Ich stimme anderen Lesern zu, ein Builder-Muster scheint viel schlimmer zu sein.

Töte den Perf - nein. Für Syntaxzucker bezahlte Leistungsstrafen - ja.

Für Syntaxzucker bezahlte Leistungsstrafen - ja.

Könnte das JIT in Zukunft noch optimiert werden?

Tötet den Perf - nein.
Für Syntaxzucker bezahlte Leistungsstrafen - ja.

Es ist mehr als syntaktischer Zucker. Wenn wir bereit wären, HashCode einer Klasse zu machen, dann wäre es syntaktischer Zucker. Ein veränderlicher Werttyp ist jedoch eine Fehlerfarm.

Ich zitiere dich von vorhin:

Genau deshalb ist es wichtig, dass es robust ist. Der Schlüsselwert von .NET besteht darin, dass es Probleme für Leute angeht, die es nicht besser wissen.

Ich würde argumentieren, dass ein veränderlicher Werttyp für die Mehrheit der Leute, die es nicht besser wissen, keine robuste API ist.

Ich würde argumentieren, dass ein veränderlicher Werttyp für die Mehrheit der Leute, die es nicht besser wissen, keine robuste API ist.

Zustimmen. Ich denke, es ist bedauerlich, dass dies bei veränderlichen struct Builder-Typen der Fall ist. Ich benutze sie alle die Zeit , weil sie schön sind und fest sitzen. [MustNotCopy] Anmerkungen jemand?

MustNotCopy ist der Traum eines jeden Strukturliebhabers. @jaredpar?

MustNotCopy ist nur wie ein Stack, aber noch schwieriger zu verwenden 😄

Ich schlage vor, keine Klasse zu erstellen, sondern Erweiterungsmethoden zu erstellen, um Hash zu kombinieren

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

Das ist alles! Es ist schnell und einfach zu bedienen.

@AlexRadch Ich mag es nicht, dass die Methodenliste für alle Ganzzahlen verschmutzt wird, nicht nur für die, die als Hashes gedacht sind.

Sie haben auch Methoden, die eine Kette der Berechnung des Hash-Codes fortsetzen, aber wie starten Sie sie? Müssen Sie etwas nicht Offensichtliches tun, z. B. bei Null anfangen? Dh 0.CombineHash(this.FirstName).CombineHash(this.LastName) .

Update: Gemäß dem Kommentar in dotnet/corefx#14046 wurde entschieden, dass die vorhandene Hash-Formel für ValueTuple beibehalten wird:

@jamesqo Danke für die Hilfe.
Nach der letzten Diskussion mit @VSadov sind wir in fortzufahren , würden uns aber lieber mit der Einführung einer teureren Hash-Funktion zurückhalten.
Durch die Randomisierung bleibt die Möglichkeit, die Hash-Funktion bei Bedarf in Zukunft zu ändern.

@jkotas , können wir dann einfach den aktuellen ROL 5-basierten Hash für HashCode behalten und ihn auf 4 Byte verkleinern? Dies würde alle Probleme beim Kopieren von Strukturen beseitigen. Wir können HashCode.Empty einen zufälligen Hash-Wert darstellen lassen.

@svick
Ja, dies verunreinigt Methoden für alle Ganzzahlen, aber es kann in einen getrennten Namensraum gelegt werden und wenn Sie nicht mit Hashes arbeiten, werden Sie es nicht einschließen und nicht sehen.

0.CombineHash(this.FirstName).CombineHash(this.LastName) sollte als this.FirstName.GetHash().CombineHash(this.LastName)

Um ausgehend vom Seed zu implementieren, kann es die nächste statische Methode haben

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

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

Jede Klasse hat also einen anderen Startwert für die Zufallsverteilung von Hashes.

@jkotas , können wir dann einfach den aktuellen ROL 5-basierten Hash für HashCode behalten und ihn auf 4 Byte verkleinern?

Ich denke, dass ein Hashcode-Builder für öffentliche Plattformen den 64-Bit-Zustand verwenden muss, um robust zu sein. Wenn es nur 32-Bit ist, ist es anfällig für schlechte Ergebnisse, wenn es verwendet wird, um insbesondere mehr Elemente, Arrays oder Sammlungen zu hashen. Wie schreiben Sie eine Dokumentation darüber, wann es eine gute Idee ist, sie zu verwenden oder nicht? Ja, es sind zusätzliche Anweisungen zum Mischen der Bits, aber ich denke, es spielt keine Rolle. Diese Art von Anweisungen werden superschnell ausgeführt. Meine Erfahrung ist, dass es besser ist, mehr Bit-Mixing als weniger zu machen, da die Auswirkungen von zu wenig Bit-Mixen viel schwerwiegender sind als zu viel.

Außerdem habe ich immer noch Bedenken hinsichtlich der vorgeschlagenen Form der API. Ich glaube, dass das Problem als Hash-Code-Erstellung betrachtet werden sollte, nicht als Hash-Code-Kombination. Vielleicht ist es verfrüht, dies als Plattform-API hinzuzufügen, und wir sollten lieber abwarten, ob sich dafür bessere Muster ergeben. Dies hindert jemanden nicht daran, ein (Quell-)Nuget-Paket mit dieser API zu veröffentlichen oder es von corefx als internen Helfer zu verwenden.

@jkotas mit einem 64-Bit-Zustand garantiert nicht, dass Ihre Ausgabe die richtigen statistischen Eigenschaften hat. Die Kombinationsfunktion selbst muss so ausgelegt sein, dass sie einen internen 64-Bit-Zustand verwendet. Auch wenn die Kombinierfunktion (statistisch gesehen) gut ist, gibt es nicht mehr als weniger Mischen. Wenn das Hashing die Randomisierung, Lawine und andere statistische Eigenschaften von Interesse hat, wird das Mischen berücksichtigt, da es sich technisch gesehen um eine speziell gestaltete Hash-Funktion handelt.

Sehen Sie, was eine gute Hash-Funktion ausmacht (von denen einige eindeutig wie xor : http://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and -speed und https://research.neustar.biz/2012/02/02/choosing-a-good-hash-function-part-3/

@jamesqo Übrigens , ich habe gerade festgestellt, dass der Combiner nicht funktioniert für den Fall: "Ich kombiniere tatsächlich Hashes (keine Laufzeit-Hashes), weil sich der Seed jedes Mal ändert." ... öffentlicher Konstrukteur mit Seed?

@jkotas

Ich denke, dass ein Hashcode-Builder für öffentliche Plattformen den 64-Bit-Zustand verwenden muss, um robust zu sein. Wenn es nur 32-Bit ist, ist es anfällig für schlechte Ergebnisse, wenn es verwendet wird, um insbesondere mehr Elemente, Arrays oder Sammlungen zu hashen.

Spielt das eine Rolle, wenn es am Ende zu einem einzigen int verdichtet wird?

@jamesqo Nicht wirklich, die Zustandsgröße hängt nur von der Funktion ab, nicht von der Robustheit. Tatsächlich können Sie Ihre Hash-Funktion sogar verschlechtern, wenn der Mähdrescher nicht dafür ausgelegt ist, so zu funktionieren, und bestenfalls verschwenden Sie Ressourcen, weil Sie keine Zufälligkeit durch Zwang erlangen können.

Folgerung: Wenn Sie kohärent sind, stellen Sie sicher, dass die Funktion statistisch ausgezeichnet ist, oder Sie werden sie fast garantiert verschlechtern.

Dies hängt davon ab, ob eine Korrelation zwischen den Items besteht. Wenn keine Korrelation besteht, funktionieren 32-Bit-Zustand und einfaches Rotl (oder sogar xor) gut. Ob ein Zusammenhang besteht, hängt davon ab.

Überlegen Sie, ob jemand dies verwendet hat, um String-Hashcode aus einzelnen Zeichen zu erstellen. Nicht, dass es wahrscheinlich ist, dass jemand dies tatsächlich für String tun würde, aber es zeigt das Problem:

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

Es würde schlechte Ergebnisse für Strings mit 32-Bit-Zustand und einfachem Rotl liefern, da Zeichen in realen Strings dazu neigen, korreliert zu sein. Wie oft werden die Items, für die dies verwendet wird, korreliert und wie schlecht würde dies zu Ergebnissen führen? Schwer zu sagen, obwohl die Dinge im wirklichen Leben auf unerwartete Weise korrelieren.

Es wird großartig sein, die nächste Methode zur API-Unterstützung der Hash-Randomisierung hinzuzufügen.

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

@jkotas Ich habe es nicht getestet, also vertraue ich dir. Aber das sagt definitiv etwas über die Funktion aus, die wir verwenden wollen. Es ist einfach nicht gut genug , zumindest wenn man Geschwindigkeit gegen Zuverlässigkeit eintauschen möchte (niemand kann damit dumme Sachen machen). Ich befürworte ausnahmsweise das Design, dass dies keine Nicht-Krypto-Hashing-Funktion ist, sondern eine schnelle Möglichkeit, unkorrelierte Hash-Codes (die so zufällig wie möglich sind) zu kombinieren.

Wenn wir darauf abzielen, dass niemand dumme Dinge damit anstellt, wird die Verwendung eines 64-Bit-Zustands nichts behebt, wir verstecken nur das Problem. Es wäre immer noch möglich, eine Eingabe zu erstellen, die diese Korrelation ausnutzt. Was uns noch einmal auf das gleiche Argument verweist, das ich vor 18 Tagen angeführt habe. Siehe: https://github.com/dotnet/corefx/issues/8034#issuecomment -261301533

Ich befürworte ausnahmsweise das Design, dass dies keine Nicht-Krypto-Hashing-Funktion ist, sondern eine schnelle Möglichkeit, unkorrelierte Hash-Codes zu kombinieren

Der schnellste Weg, unkorrelierte Hash-Codes zu kombinieren, ist xor...

Stimmt, aber wir wissen, dass das letzte Mal nicht so gut funktioniert hat (IntPtr fällt mir ein). Rotation und XOR (aktuell) sind genauso schnell, ohne Verluste, wenn jemand korrelierte Dinge einfügt.

Fügen Sie Hashcode-Randomisierung mit public static HashCode CreateRandomized(Type type); oder mit public static HashCode CreateRandomized<T>(); Methoden oder mit beiden hinzu.

@jkotas Ich denke, ich habe dafür vielleicht ein besseres Muster gefunden. Was wäre, wenn wir C# 7-Ref-Returns verwenden würden? Anstatt jedes Mal ein HashCode , würden wir ein ref HashCode das in ein Register passt.

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

Die Nutzung bleibt wie zuvor:

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

Der einzige Nachteil ist, dass wir wieder bei einer veränderlichen Struktur sind. Aber ich glaube nicht, dass es eine Möglichkeit gibt, gleichzeitig kein Kopieren und Unveränderlichkeit zu haben.

( ref this funktioniert noch nicht, aber ich sehe eine PR in Roslyn, um es hier zu aktivieren


@AlexRadch Ich halte es nicht für

@jamesqo public static HashCode CreateRandomized<T>(); bekomme keinen Typ-Hash-Code. Es erstellt randomisierten HashCode für diesen Typ.

@jamesqo " ref this funktioniert noch nicht". Selbst wenn das Roslyn-Problem behoben ist, wird ref this für eine Weile nicht für das corefx-Repository verfügbar sein (ich bin mir nicht sicher, wie lange, @stephentoub kann wahrscheinlich Erwartungen setzen).

Die Designdiskussion läuft hier nicht zusammen. Außerdem sind die 200 Kommentare sehr schwer zu verstehen.
Wir planen, uns nächste Woche

Nebenbei: Ich schlage vor, dieses Thema zu schließen und ein neues mit dem "seligen Vorschlag" zu erstellen, wenn wir nächste Woche da sind, um die Belastung nach der langen Diskussion zu verringern. Lassen Sie es mich wissen, wenn Sie es für eine schlechte Idee halten.

@jcouv Ich bin damit Unsafe .)

@karelz OK :smile: Ich werde diesen Vorschlag später schließen, wenn ich Zeit habe, und einen neuen eröffnen. Ich stimme zu; Mein Browser kann 200+ Kommentare nicht so gut verarbeiten.

@karelz Ich bin auf einen Haken gestoßen; Es stellt sich heraus, dass der betreffende PR versucht hat, ref this Rückgaben für Referenztypen im Gegensatz zu Werttypen zu ermöglichen. ref this kann nicht sicher von Strukturen zurückgegeben werden; siehe hier warum. Der Kompromiss mit der Rücksendung wird also nicht funktionieren.

Wie auch immer, ich werde dieses Thema schließen. Ich habe hier ein weiteres Issue geöffnet: https://github.com/dotnet/corefx/issues/14354

Sollte in der Lage sein, ref "this" von einem Methodenbeitrag zur Erweiterung des Werttyps https://github.com/dotnet/roslyn/pull/15650 zurückzugeben, obwohl ich davon ausgeht, dass C#vNext ...

@benaadams

Sollte in der Lage sein, ref "this" von einer Werttyperweiterungsmethode post dotnet/roslyn#15650 zurückzugeben, obwohl ich davon ausgeht, dass C#vNext ...

Richtig. Es ist möglich, this von einer ref this Erweiterungsmethode zurückzugeben. Es ist jedoch nicht möglich, this von einer normalen Strukturinstanzmethode zurückzugeben. Es gibt viele blutige Details über die Lebensdauer, warum das so ist :(

@redknightlois

Wenn wir streng sein wollen, sollte der einzige Hash uint . Es kann als ein Versehen angesehen werden, dass das Framework unter diesem Licht int zurückgibt.

CLS-Konformität? Ganzzahlen ohne Vorzeichen sind nicht CLS-kompatibel.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

v0l picture v0l  ·  3Kommentare

matty-hall picture matty-hall  ·  3Kommentare

chunseoklee picture chunseoklee  ·  3Kommentare

jchannon picture jchannon  ·  3Kommentare

omariom picture omariom  ·  3Kommentare