Qualcuno può spiegare questo strano comportamento con float firmati in C #?


247

Ecco l'esempio con i commenti:

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

Allora, cosa ne pensi di questo?


2
Fare in modo che le cose più strane c.d.Equals(d.d)valuti truecome fac.f.Equals(d.f)
Justin Niessner,

2
Non confrontare i galleggianti con un confronto esatto come .Equals. È semplicemente una cattiva idea.
Thorsten79,

6
@ Thorsten79: In che modo è rilevante qui?
Ben M,

2
Questo è molto strano. L'uso di un lungo invece di un doppio per f introduce lo stesso comportamento. E l'aggiunta di un altro campo corto lo corregge di nuovo ...
Jens

1
Strano - sembra accadere solo quando entrambi sono dello stesso tipo (float o double). Cambia uno in float (o decimale) e D2 funziona come D1.
tvanfosson,

Risposte:


387

Il bug è nelle seguenti due righe di System.ValueType: (Sono entrato nella fonte di riferimento)

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

(Entrambi i metodi sono [MethodImpl(MethodImplOptions.InternalCall)])

Quando tutti i campi hanno una larghezza di 8 byte, CanCompareBitsrestituisce erroneamente true, risultando in un confronto bit a bit di due valori diversi, ma semanticamente identici.

Quando almeno un campo non ha una larghezza di 8 byte, CanCompareBitsrestituisce false e il codice continua a utilizzare la riflessione per scorrere i campi e chiamare Equalsciascun valore, che considera correttamente -0.0uguale a 0.0.

Ecco la fonte di 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

159
Entrare in System.ValueType? È un bel fratello hardcore.
Pierreten,

2
Non spieghi quale sia il significato di "8 byte di larghezza". Una struttura con tutti i campi a 4 byte non avrebbe lo stesso risultato? Immagino che solo un campo a 4 byte e un campo a 8 byte si inneschi IsNotTightlyPacked.
Gabe,

1
@Gabe Ho scritto primaThe bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
SLaks

1
Con .NET che ora è un software open source, ecco un link all'implementazione CLR Core di ValueTypeHelper :: CanCompareBits . Non volevo aggiornare la tua risposta poiché l'implementazione è leggermente cambiata rispetto alla fonte di riferimento che hai pubblicato.
Indispensabile il

59

Ho trovato la risposta su http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx .

Il pezzo principale è il commento alla fonte CanCompareBits, che ValueType.Equalsusa per determinare se usare il memcmpconfronto stile:

Il commento di CanCompareBits dice "Restituisce vero se il tipo di valore non contiene puntatore ed è strettamente compresso". E FastEqualsCheck usa "memcmp" per accelerare il confronto.

L'autore prosegue affermando esattamente il problema descritto dall'OP:

Immagina di avere una struttura che contiene solo un galleggiante. Cosa accadrà se uno contiene +0,0 e l'altro contiene -0,0? Dovrebbero essere uguali, ma la rappresentazione binaria sottostante è diversa. Se annidate un'altra struttura che sovrascrive il metodo Equals, anche quell'ottimizzazione fallirà.


Mi chiedo se il comportamento di Equals(Object)per double, floate Decimalcambiato durante le prime bozze di .net; Penserei che sia più importante avere il X.Equals((Object)Y)ritorno virtuale solo truequando Xe Ysono indistinguibili, piuttosto che avere quel metodo corrisponda al comportamento di altri sovraccarichi (soprattutto dato che, a causa della coercizione implicita del tipo, i Equalsmetodi sovraccaricati non definiscono nemmeno una relazione di equivalenza !, ad esempio, 1.0f.Equals(1.0)produce falso, ma 1.0.Equals(1.0f)produce vero!) Il vero problema IMHO non è con il modo in cui le strutture vengono confrontate ...
supercat

1
... ma con il modo in cui quei tipi di valore hanno la precedenza Equalsper significare qualcosa di diverso dall'equivalenza. Supponiamo, ad esempio, che si voglia scrivere un metodo che prende un oggetto immutabile e, se non è stato ancora memorizzato nella cache, lo esegue ToStringe memorizza nella cache il risultato; se è stato memorizzato nella cache, è sufficiente restituire la stringa memorizzata nella cache. Non è una cosa irragionevole da fare, ma fallirebbe male Decimaldal momento che due valori potrebbero confrontare uguali ma produrre stringhe diverse.
Supercat,

52

La congettura di Vilx è corretta. Ciò che "CanCompareBits" fa è verificare se il tipo di valore in questione è "compresso" in memoria. Una struttura strettamente compatta viene confrontata semplicemente confrontando i bit binari che compongono la struttura; una struttura vagamente compatta viene confrontata chiamando Equals su tutti i membri.

Questo spiega l'osservazione di SLaks che riprende con strutture che sono tutte doppie; tali strutture sono sempre strettamente imballate.

Sfortunatamente, come abbiamo visto qui, ciò introduce una differenza semantica perché il confronto bit per bit dei doppi e il confronto dei doppi dei doppi danno risultati diversi.


3
Allora perché non è un bug? Anche se MS consiglia di sostituire sempre i tipi di valore uguali.
Alexander Efimov,

14
Mi fa impazzire. Non sono un esperto degli interni del CLR.
Eric Lippert,

4
... non lo sei? Sicuramente la tua conoscenza degli interni di C # porterebbe a notevoli conoscenze su come funziona il CLR.
CaptainCasey

37
@CaptainCasey: ho trascorso cinque anni a studiare gli interni del compilatore C # e probabilmente in totale un paio d'ore a studiare gli interni del CLR. Ricorda, sono un consumatore del CLR; Capisco la sua area pubblica abbastanza bene, ma i suoi interni sono una scatola nera per me.
Eric Lippert,

1
Errore mio, ho pensato che i compilatori CLR e VB / C # fossero più strettamente accoppiati ... quindi C # / VB -> CIL -> CLR
CaptainCasey

22

Mezza risposta:

Reflector ci dice che ValueType.Equals()fa qualcosa del genere:

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

Sfortunatamente entrambi CanCompareBits()e FastEquals()(entrambi i metodi statici) sono extern ( [MethodImpl(MethodImplOptions.InternalCall)]) e non hanno fonti disponibili.

Torniamo a indovinare perché un caso può essere confrontato per bit e l'altro no (forse problemi di allineamento?)



14

Caso di prova più semplice:

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

EDIT : il bug si verifica anche con float, ma si verifica solo se i campi nella struttura si sommano a un multiplo di 8 byte.


Sembra una regola di ottimizzazione che vada: se è tutto raddoppiato rispetto a un bit-compare, altrimenti separa il doppio. Chiamate uguali
Henk Holterman

Non penso che questo sia lo stesso test case di quello che il problema presentato qui sembra essere che il valore predefinito per Bad.f non è 0, mentre l'altro caso sembra essere un problema Int vs. Double.
Driss Zouak,

6
@Driss: il valore predefinito per double è 0 . Hai torto.
SL

10

Deve essere correlato un confronto bit per bit, poiché 0.0dovrebbe differire -0.0solo dal bit del segnale.


5

…Cosa ne pensi di questo?

Sostituisci sempre Equals e GetHashCode sui tipi di valore. Sarà veloce e corretto.


A parte un avvertimento sul fatto che ciò è necessario solo quando l'uguaglianza è rilevante, questo è esattamente quello che stavo pensando. Per quanto sia divertente osservare le stranezze del comportamento di uguaglianza del tipo di valore predefinito come fanno le risposte con il voto più alto, c'è un motivo per cui esiste CA1815 .
Joe Amenta,

@JoeAmenta Ci scusiamo per una risposta in ritardo. A mio avviso (solo a mio avviso, ovviamente), l'uguaglianza è sempre ( ) rilevante per i tipi di valore. L'implementazione di uguaglianza predefinita non è accettabile nei casi comuni. ( ) Tranne casi molto speciali. Molto. Molto speciale. Quando sai esattamente cosa stai facendo e perché.
Viacheslav Ivanov,

Penso che concordiamo sul fatto che l'override dei controlli di uguaglianza per i tipi di valore sia virtualmente sempre possibile e significativo con pochissime eccezioni e di solito lo renderà strettamente più corretto. Il punto che stavo cercando di comunicare con la parola "rilevante" era che ci sono alcuni tipi di valore le cui istanze non verranno mai confrontate con altre istanze per l'uguaglianza, quindi l'override comporterebbe un codice morto che deve essere mantenuto. Quei (e gli strani casi speciali a cui alludi) sarebbero gli unici posti in cui lo salterei.
Joe Amenta,

4

Solo un aggiornamento per questo bug di 10 anni: è stato corretto ( Disclaimer : sono l'autore di questo PR) in .NET Core che sarebbe probabilmente stato rilasciato in .NET Core 2.1.0.

Il post sul blog ha spiegato il bug e come l'ho risolto.


2

Se fai D2 in questo modo

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

è vero.

se lo fai così

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

È ancora falso.

i t sembra come se fosse false se l'struct contiene solo doppie.


1

Deve essere correlato a zero, poiché ha cambiato la linea

dd = -0,0

per:

dd = 0,0

risulta vero il confronto ...


Al contrario, i NaN potrebbero confrontarsi uguali tra loro per una modifica, quando usano effettivamente lo stesso modello di bit.
Harold,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.