web-dev-qa-db-fra.com

Quelqu'un peut-il expliquer ce comportement étrange avec des flotteurs signés en C #?

Voici l'exemple avec des commentaires:

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

Alors, qu'est-ce que tu en penses?

247
Alexander Efimov

Le bogue se trouve dans les deux lignes suivantes de System.ValueType: (Je suis entré dans la source de référence)

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

(Les deux méthodes sont [MethodImpl(MethodImplOptions.InternalCall)])

Lorsque tous les champs ont une largeur de 8 octets, CanCompareBits renvoie par erreur true, ce qui entraîne une comparaison au niveau du bit de deux valeurs différentes, mais sémantiquement identiques.

Lorsqu'au moins un champ ne fait pas 8 octets de large, CanCompareBits renvoie false, et le code continue à utiliser la réflexion pour parcourir les champs et appeler Equals pour chaque valeur, qui traite correctement -0.0 Égal à 0.0.

Voici la source de CanCompareBits de SSCLI:

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
386
SLaks

J'ai trouvé la réponse sur http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx .

L'élément central est le commentaire source sur CanCompareBits, qui ValueType.Equals utilise pour déterminer s'il faut utiliser la comparaison de style memcmp:

Le commentaire de CanCompareBits dit "Retourne vrai si le type de valeur ne contient pas de pointeur et est bien emballé". Et FastEqualsCheck utilise "memcmp" pour accélérer la comparaison.

L'auteur poursuit en énonçant exactement le problème décrit par le PO:

Imaginez que vous ayez une structure qui ne contient qu'un flotteur. Que se passera-t-il si l'un contient +0,0 et l'autre contient -0,0? Ils doivent être identiques, mais les représentations binaires sous-jacentes sont différentes. Si vous imbriquez une autre structure qui remplace la méthode Equals, cette optimisation échouera également.

59
Ben M

La conjecture de Vilx est correcte. "CanCompareBits" vérifie si le type de valeur en question est "étroitement compressé" en mémoire. Une structure très compacte est comparée en comparant simplement les bits binaires qui composent la structure; une structure peu serrée est comparée en appelant Equals sur tous les membres.

Ceci explique l'observation de SLaks qui reproche avec des structures qui sont toutes doubles; ces structures sont toujours bien emballées.

Malheureusement, comme nous l'avons vu ici, cela introduit une différence sémantique car la comparaison au niveau du bit des doubles et la comparaison égale des doubles donne des résultats différents.

52
Eric Lippert

Une demi-réponse:

Reflector nous dit que ValueType.Equals() fait quelque chose comme ceci:

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

Malheureusement, les deux CanCompareBits() et FastEquals() (les deux méthodes statiques) sont extern ([MethodImpl(MethodImplOptions.InternalCall)]) et n'ont aucune source disponible.

Retour à deviner pourquoi un cas peut être comparé par bits, et l'autre pas (problèmes d'alignement peut-être?)

22
Vilx-

Cela ne donne vrai pour moi, avec les gmcs 2.4.2.3 de Mono.

17
Matthew Flaschen

Cas de test plus simple:

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 : Le bogue se produit également avec les flottants, mais ne se produit que si les champs de la structure totalisent un multiple de 8 octets.

14
SLaks

Elle doit être liée à une comparaison bit à bit, car 0.0 devrait différer de -0.0 uniquement par le bit de signal.

10
João Angelo

…Que pensez-vous de ceci?

Remplacez toujours Equals et GetHashCode sur les types de valeur. Ce sera rapide et correct.

5
Viacheslav Ivanov

Juste une mise à jour pour ce bug de 10 ans: il a été corrigé ( Avertissement : je suis l'auteur de ce PR ) dans .NET Core qui serait probablement publié dans .NET Core 2.1.0.

Le article de blog a expliqué le bogue et comment je l'ai corrigé.

4
Jim Ma

Si vous faites D2 comme ça

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

c'est vrai.

si tu le fais comme ça

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

C'est toujours faux.

i t semble faux si la structure ne contient que des doubles.

2
Morten Anderson

Il doit être lié à zéro, car le changement de ligne

d.d = -0,0

à:

d.d = 0,0

résultats dans la comparaison étant vrai ...

1
user243357