Warum fügt Visual Studio einer generierten Hashcode-Berechnung "-1937169414" hinzu?

9

Wenn Sie das Refactoring-Menü von Visual Studio verwenden, um einer Klasse wie dieser eine GetHashCode-Implementierung hinzuzufügen:

Generieren Sie das GetHashCode-Menü

und wählen Sie die einzige int-Eigenschaft in der Klasse aus:

Mitgliederauswahlbildschirm

Dieser Code wird in .NET Framework generiert:

public override int GetHashCode()
{
    return -1937169414 + Value.GetHashCode();
}

(Es wird HashCode.Combine(Value)stattdessen auf .NET Core generiert. Ich bin mir nicht sicher, ob es denselben Wert enthält.)

Was ist das Besondere an diesem Wert? Warum wird Visual Studio nicht Value.GetHashCode()direkt verwendet? Soweit ich weiß, hat dies keinen wirklichen Einfluss auf die Hash-Verteilung. Da es sich nur um eine Addition handelt, würden sich aufeinanderfolgende Werte immer noch zusammen ansammeln.

BEARBEITEN: Ich habe dies nur mit verschiedenen Klassen mit ValueEigenschaften versucht, aber anscheinend wirkt sich der Eigenschaftsname auf die generierte Anzahl aus. Wenn Sie beispielsweise die Eigenschaft in umbenennen Halue, lautet die Nummer 387336856. Vielen Dank an Gökhan Kurt, der darauf hingewiesen hat.

Sedat Kapanoglu
quelle
Siehe docs.microsoft.com/en-us/dotnet/api/… im Abschnitt "Bemerkungen". "Hash-Codes für identische Zeichenfolgen können sich in .NET-Implementierungen, in .NET-Versionen und in .NET-Plattformen (z. B. 32-Bit und 64-Bit) für eine einzelne Version von .NET unterscheiden. In einigen Fällen können sie sogar unterschiedlich sein nach Anwendungsdomäne "
Link
@ Link wie ist das relevant? Das ist nicht einmal eine Zeichenfolge, die Eigenschaft ist eine int.
Sedat Kapanoglu
[HashCode]. Kombinieren?
Ry-
Entschuldigung, falscher Link: docs.microsoft.com/en-us/dotnet/api/… Dieses Verhalten gilt auch für Object.GetHashcode @SedatKapanoglu
Link
2
-1937169414ist eine ganzzahlige Multiplikation von -1521134295und -783812246. Die bedeutendere Zahl ist hier, -1521134295die in jeder Hashcode-Berechnung erscheint. -783812246ist die Startnummer. Eine Startnummer wird basierend auf der Anzahl der Elemente in der Gleichung ausgewählt. In anonymen Klassen wird die Startnummer basierend auf Feldnamen berechnet. Es gibt also so viele Startzahlen wie ganze Zahlen. Wir können davon ausgehen, dass eine Startnummer zufällig ist. Was die Bedeutung von -1521134295betrifft, denke ich, dass es die Kollision reduziert und nur ein Insider-Entwickler in der Lage wäre, genau zu beantworten, wie.
Gökhan Kurt

Antworten:

2

Wenn Sie -1521134295in den Microsoft-Repositorys suchen, werden Sie feststellen, dass diese häufig angezeigt werden

Die meisten Suchergebnisse befinden sich in den GetHashCodeFunktionen, aber alle haben die folgende Form

int hashCode = SOME_CONSTANT;
hashCode = hashCode * -1521134295 + field1.GetHashCode();
hashCode = hashCode * -1521134295 + field2.GetHashCode();
// ...
return hashCode;

Die erste hashCode * -1521134295 = SOME_CONSTANT * -1521134295wird während der Generierungszeit vom Generator oder während der Kompilierungszeit von CSC vormultipliziert. Das ist der Grund dafür -1937169414in Ihrem Code

Wenn Sie sich eingehender mit den Ergebnissen befassen , wird der Teil zur Codegenerierung angezeigt , der in der Funktion CreateGetHashCodeMethodStatements enthalten ist

const int hashFactor = -1521134295;

var initHash = 0;
var baseHashCode = GetBaseGetHashCodeMethod(containingType);
if (baseHashCode != null)
{
    initHash = initHash * hashFactor + Hash.GetFNVHashCode(baseHashCode.Name);
}

foreach (var symbol in members)
{
    initHash = initHash * hashFactor + Hash.GetFNVHashCode(symbol.Name);
}

Wie Sie sehen können, hängt der Hash von den Symbolnamen ab. In dieser Funktion wird die Konstante auch aufgerufen permuteValue, wahrscheinlich weil die Bits nach der Multiplikation irgendwie umlaufen

// -1521134295
var permuteValue = CreateLiteralExpression(factory, hashFactor);

Es gibt einige Muster, wenn wir den Wert binär anzeigen: 101001 010101010101010 101001 01001oder 10100 1010101010101010 10100 10100 1. Aber wenn wir einen beliebigen Wert damit multiplizieren, gibt es viele überlappende Übertragungen, sodass ich nicht sehen konnte, wie es funktioniert. Der Ausgang kann auch eine andere Anzahl von gesetzten Bits haben, so dass es sich nicht wirklich um eine Permutation handelt

Sie finden den anderen Generator in Roslyns AnonymousTypeGetHashCodeMethodSymbol, der die Konstante aufruftHASH_FACTOR

//  Method body:
//
//  HASH_FACTOR = 0xa5555529;
//  INIT_HASH = (...((0 * HASH_FACTOR) + GetFNVHashCode(backingFld_1.Name)) * HASH_FACTOR
//                                     + GetFNVHashCode(backingFld_2.Name)) * HASH_FACTOR
//                                     + ...
//                                     + GetFNVHashCode(backingFld_N.Name)

Der wahre Grund für die Wahl dieses Wertes ist noch unklar

phuclv
quelle
Das ist großartige Forschung, danke. Ich wusste nicht, dass Hash-Code in Roslyn generiert wird. Ich dachte, es wäre Visual Studio.
Sedat Kapanoglu
3

Wie GökhanKurt in den Kommentaren erklärte, ändert sich die Anzahl basierend auf den beteiligten Eigenschaftsnamen. Wenn Sie die Eigenschaft in umbenennen Halue, lautet die Nummer stattdessen 387336856. Ich hatte es mit verschiedenen Klassen versucht, aber nicht daran gedacht, das Anwesen umzubenennen.

Gökhans Kommentar ließ mich seinen Zweck verstehen. Das Versetzen von Hashwerten basiert auf einem deterministischen, aber zufällig verteilten Versatz. Auf diese Weise ist das Kombinieren von Hash-Werten für verschiedene Klassen selbst mit einer einfachen Addition immer noch leicht resistent gegen Hash-Kollisionen.

Wenn Sie beispielsweise zwei Klassen mit ähnlichen GetHashCode-Implementierungen haben:

public class A
{
    public int Value { get; set;}
    public int GetHashCode() => Value;
}

public class B
{
    public int Value { get; set;}
    public override int GetHashCode() => Value;
}

und wenn Sie eine andere Klasse haben, die Verweise auf diese beiden enthält:

public class C
{
    public A ValueA { get; set; }
    public B ValueB { get; set; }
    public override int GetHashCode()
    {
        return ValueA.GetHashCode() + ValueB.GetHashCode();
    }
}

Eine schlechte Kombination wie diese würde zu Hash-Kollisionen neigen, da sich der resultierende Hash-Code für verschiedene Werte von ValueA und ValueB im selben Bereich ansammeln würde, wenn ihre Werte nahe beieinander liegen. Es spielt wirklich keine Rolle, ob Sie Multiplikations- oder bitweise Operationen verwenden, um sie zu kombinieren. Sie sind dennoch anfällig für Kollisionen ohne gleichmäßig verteilten Versatz. Da viele bei der Programmierung verwendete ganzzahlige Werte um 0 akkumuliert werden, ist es sinnvoll, einen solchen Offset zu verwenden

Anscheinend ist es eine gute Praxis, einen zufälligen Versatz mit guten Bitmustern zu haben.

Ich bin mir immer noch nicht sicher, warum sie keine völlig zufälligen Offsets verwenden, wahrscheinlich um keinen Code zu beschädigen, der auf dem Determinismus von GetHashCode () beruht, aber es wäre großartig, einen Kommentar vom Visual Studio-Team dazu zu erhalten.

Sedat Kapanoglu
quelle