Kann jemand dieses seltsame Verhalten mit signierten Floats in C # erklären?

247

Hier ist das Beispiel mit Kommentaren:

class Program
{
    // first version of structure
    public struct D1
    {
        public double d;
        public int f;
    }

    // during some changes in code then we got D2 from D1
    // Field f type became double while it was int before
    public struct D2 
    {
        public double d;
        public double f;
    }

    static void Main(string[] args)
    {
        // Scenario with the first version
        D1 a = new D1();
        D1 b = new D1();
        a.f = b.f = 1;
        a.d = 0.0;
        b.d = -0.0;
        bool r1 = a.Equals(b); // gives true, all is ok

        // The same scenario with the new one
        D2 c = new D2();
        D2 d = new D2();
        c.f = d.f = 1;
        c.d = 0.0;
        d.d = -0.0;
        bool r2 = c.Equals(d); // false! this is not the expected result        
    }
}

Sag mal, wie findest du das?

Alexander Efimov
quelle
2
Um die Dinge Fremde zu machen c.d.Equals(d.d)auswertet , um trueebenso wiec.f.Equals(d.f)
Justin Niessner
2
Vergleichen Sie Floats nicht mit exakten Vergleichen wie .Equals. Es ist einfach eine schlechte Idee.
Thorsten79
6
@ Thorsten79: Wie ist das hier relevant?
Ben M
2
Das ist höchst seltsam. Die Verwendung eines langen statt eines doppelten für f führt zum gleichen Verhalten. Und das Hinzufügen eines weiteren kurzen Feldes korrigiert es erneut ...
Jens
1
Seltsam - es scheint nur zu passieren, wenn beide vom gleichen Typ sind (float oder double). Ändern Sie eins in float (oder dezimal) und D2 funktioniert genauso wie D1.
Tvanfosson

Antworten:

387

Der Fehler ist in den folgenden zwei Zeilen von System.ValueType: (Ich trat in die Referenzquelle)

if (CanCompareBits(this)) 
    return FastEqualsCheck(thisObj, obj);

(Beide Methoden sind [MethodImpl(MethodImplOptions.InternalCall)])

Wenn alle Felder 8 Byte breit sind, wird CanCompareBitsfälschlicherweise true zurückgegeben, was zu einem bitweisen Vergleich zweier verschiedener, aber semantisch identischer Werte führt.

Wenn mindestens ein Feld nicht 8 Byte breit ist, wird CanCompareBitsfalse zurückgegeben, und der Code verwendet die Reflexion, um die Felder zu durchlaufen und Equalsjeden Wert aufzurufen , der korrekt -0.0als gleich behandelt wird 0.0.

Hier ist die Quelle für CanCompareBitsSSCLI:

FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
    WRAPPER_CONTRACT;
    STATIC_CONTRACT_SO_TOLERANT;

    _ASSERTE(obj != NULL);
    MethodTable* mt = obj->GetMethodTable();
    FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND
SLaks
quelle
158
Einstieg in System.ValueType? Das ist ziemlich hardcore, Bruder.
Pierreten
2
Sie erklären nicht, welche Bedeutung "8 Bytes breit" hat. Würde eine Struktur mit allen 4-Byte-Feldern nicht das gleiche Ergebnis haben? Ich vermute, dass ein einzelnes 4-Byte-Feld und ein 8-Byte-Feld nur ausgelöst werden IsNotTightlyPacked.
Gabe
1
@ Gabe Ich schrieb früher, dassThe bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
SLaks
1
Da .NET jetzt Open Source-Software ist, finden Sie hier einen Link zur Core CLR-Implementierung von ValueTypeHelper :: CanCompareBits . Ich wollte Ihre Antwort nicht aktualisieren, da die Implementierung gegenüber der von Ihnen veröffentlichten Referenzquelle geringfügig geändert wurde.
Unsichtbarer
59

Ich fand die Antwort unter http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx .

Das Kernstück ist der Quellkommentar, anhand CanCompareBitsdessen ValueType.Equalsbestimmt wird, ob ein memcmpStilvergleich verwendet werden soll:

Der Kommentar von CanCompareBits lautet "Return true, wenn der Wertetyp keinen Zeiger enthält und dicht gepackt ist". Und FastEqualsCheck verwendet "memcmp", um den Vergleich zu beschleunigen.

Der Autor führt das vom OP beschriebene Problem genau an:

Stellen Sie sich vor, Sie haben eine Struktur, die nur einen Float enthält. Was passiert, wenn einer +0,0 und der andere -0,0 enthält? Sie sollten gleich sein, aber die zugrunde liegende binäre Darstellung ist unterschiedlich. Wenn Sie eine andere Struktur verschachteln, die die Equals-Methode überschreibt, schlägt diese Optimierung ebenfalls fehl.

Ben M.
quelle
Ich frage mich, ob sich das Verhalten von Equals(Object)for double, floatund Decimalwährend der frühen Entwürfe von .net geändert hat. Ich würde denken , dass es wichtiger ist , die virtuelle haben X.Equals((Object)Y)nur Rückkehr , truewenn Xund Ysind nicht zu unterscheiden, als das Verfahren zu haben , das Verhalten anderer Überlastungen zu entsprechen (vor allem da, wegen der impliziten Typumwandlung, überladene EqualsMethoden definieren nicht einmal eine Äquivalenzbeziehung !, zB 1.0f.Equals(1.0)ergibt falsch, aber 1.0.Equals(1.0f)ergibt wahr!) Das eigentliche Problem ist
meiner Meinung nach
1
... aber mit der Art und Weise, wie diese Werttypen überschreiben Equals, um etwas anderes als Äquivalenz zu bedeuten. Angenommen, man möchte beispielsweise eine Methode schreiben, die ein unveränderliches Objekt nimmt und, falls es noch nicht zwischengespeichert wurde, es ausführt ToStringund das Ergebnis zwischenspeichert. Wenn es zwischengespeichert wurde, geben Sie einfach die zwischengespeicherte Zeichenfolge zurück. Keine unvernünftige Sache, aber es würde schlimm scheitern, Decimalda zwei Werte gleich sind, aber unterschiedliche Zeichenfolgen ergeben.
Supercat
52

Vilx 'Vermutung ist richtig. "CanCompareBits" prüft, ob der betreffende Werttyp im Speicher "dicht gepackt" ist. Eine dicht gepackte Struktur wird verglichen, indem einfach die Binärbits verglichen werden, aus denen die Struktur besteht. Eine lose gepackte Struktur wird verglichen, indem für alle Mitglieder Equals aufgerufen wird.

Dies erklärt die Beobachtung von SLaks, dass es sich um Strukturen handelt, die alle doppelt sind. solche Strukturen sind immer dicht gepackt.

Wie wir hier gesehen haben, führt dies leider zu einem semantischen Unterschied, da der bitweise Vergleich von Doppelwerten und der Gleichheitsvergleich von Doppelwerten unterschiedliche Ergebnisse liefert.

Eric Lippert
quelle
3
Warum ist es dann kein Fehler? Obwohl MS empfiehlt, Gleich auf Werttypen immer zu überschreiben.
Alexander Efimov
14
Schlägt mich zum Teufel. Ich bin kein Experte für die Interna der CLR.
Eric Lippert
4
... bist du nicht? Sicherlich würde Ihr Wissen über die C # -Interna zu erheblichem Wissen über die Funktionsweise der CLR führen.
CaptainCasey
37
@CaptainCasey: Ich habe fünf Jahre lang die Interna des C # -Compilers und wahrscheinlich insgesamt ein paar Stunden die Interna der CLR studiert. Denken Sie daran, ich bin ein Verbraucher der CLR; Ich verstehe seine öffentliche Oberfläche ziemlich gut, aber seine Einbauten sind für mich eine Black Box.
Eric Lippert
1
Mein Fehler, ich dachte, die CLR- und die VB / C # -Compiler
wären
22

Eine halbe Antwort:

Der Reflektor sagt uns, dass er ValueType.Equals()so etwas macht:

if (CanCompareBits(this))
    return FastEqualsCheck(this, obj);
else
    // Use reflection to step through each member and call .Equals() on each one.

Leider sind beide CanCompareBits()und FastEquals()(beide statische Methoden) extern ( [MethodImpl(MethodImplOptions.InternalCall)]) und haben keine Quelle zur Verfügung.

Zurück zur Vermutung, warum ein Fall mit Bits verglichen werden kann und der andere nicht (Ausrichtungsprobleme vielleicht?)

Vilx-
quelle
17

Es wird geben gilt für mich, mit Monos gmcs 2.4.2.3.

Matthew Flaschen
quelle
5
Ja, ich habe es auch in Mono versucht, und es gibt mir auch wahr. Sieht aus wie MS etwas Magie im Inneren macht :)
Alexander Efimov
3
Interessant, wir alle versenden nach Mono?
WeNeedAnswers
14

Einfacherer Testfall:

Console.WriteLine("Good: " + new Good().Equals(new Good { d = -.0 }));
Console.WriteLine("Bad: " + new Bad().Equals(new Bad { d = -.0 }));

public struct Good {
    public double d;
    public int f;
}

public struct Bad {
    public double d;
}

BEARBEITEN : Der Fehler tritt auch bei Floats auf, jedoch nur, wenn die Felder in der Struktur ein Vielfaches von 8 Bytes ergeben.

SLaks
quelle
Sieht aus wie eine Optimierungsregel, die lautet: Wenn sich alles verdoppelt, als ein Bitvergleich durchzuführen, führen Sie ein separates Doppel durch. Gleiche Anrufe
Henk Holterman
Ich denke nicht, dass dies der gleiche Testfall ist wie das hier vorgestellte Problem, dass der Standardwert für Bad.f nicht 0 ist, während der andere Fall ein Int vs. Double-Problem zu sein scheint.
Driss Zouak
6
@Driss: Der Standardwert für double ist 0 . Du liegst falsch.
SLaks
10

Es muss sich auf einen bitweisen Vergleich beziehen, da 0.0sich dieser -0.0nur durch das Signalbit unterscheiden sollte .

João Angelo
quelle
5

…was denkst du darüber?

Überschreiben Sie bei Werttypen immer Equals und GetHashCode. Es wird schnell und korrekt sein.

Viacheslav Ivanov
quelle
Abgesehen von einer Einschränkung, dass dies nur notwendig ist, wenn Gleichheit relevant ist, habe ich genau das gedacht. So unterhaltsam es auch ist, Macken des Gleichheitsverhaltens des Standardwerttyps zu betrachten, wie es die Antworten mit den höchsten Stimmen tun, es gibt einen Grund, warum CA1815 existiert.
Joe Amenta
@ JoeAmenta Entschuldigung für eine späte Antwort. Aus meiner Sicht (natürlich nur aus meiner Sicht) ist die Gleichheit für Werttypen immer ( ) relevant. Die Standardimplementierung der Gleichstellung ist in allgemeinen Fällen nicht akzeptabel. ( ) Außer in ganz besonderen Fällen. Sehr. Sehr besonders. Wenn Sie genau wussten, was Sie tun und warum.
Viacheslav Ivanov
Ich denke, wir sind uns einig, dass das Überschreiben der Gleichheitsprüfungen für Werttypen mit sehr wenigen Ausnahmen praktisch immer möglich und sinnvoll ist und in der Regel streng korrekter wird. Der Punkt, den ich mit dem Wort "relevant" zu vermitteln versuchte, war, dass es einige Werttypen gibt, deren Instanzen niemals mit anderen Instanzen für Gleichheit verglichen werden, so dass das Überschreiben zu totem Code führen würde, der beibehalten werden muss. Diese (und die seltsamen Sonderfälle, auf die Sie anspielen) wären die einzigen Orte, an denen ich sie überspringen würde.
Joe Amenta
4

Nur ein Update für diesen 10 Jahre alten Fehler: Er wurde in .NET Core behoben ( Haftungsausschluss : Ich bin der Autor dieser PR) und wahrscheinlich in .NET Core 2.1.0 veröffentlicht.

Der Blog-Beitrag erklärte den Fehler und wie ich ihn behoben habe.

Jim Ma
quelle
2

Wenn du D2 so machst

public struct D2
{
    public double d;
    public double f;
    public string s;
}

es ist wahr.

wenn du es so machst

public struct D2
{
    public double d;
    public double f;
    public double u;
}

Es ist immer noch falsch.

Es scheint falsch zu sein, wenn die Struktur nur Doubles enthält.

Morten Anderson
quelle
1

Es muss nullbezogen sein, da die Zeile geändert wird

dd = -0,0

zu:

dd = 0,0

führt dazu, dass der Vergleich wahr ist ...

user243357
quelle
Umgekehrt könnten NaNs zur Abwechslung gleich miteinander verglichen werden, wenn sie tatsächlich dasselbe Bitmuster verwenden.
Harold