59 votes

Graves bogues avec les conversions levées/nullables à partir de int, permettant la conversion à partir de décimal

Je pense que cette question va m'apporter une gloire instantanée ici sur Stack Overflow.

Supposons que vous ayez le type suivant :

// represents a decimal number with at most two decimal places after the period
struct NumberFixedPoint2
{
    decimal number;

    // an integer has no fractional part; can convert to this type
    public static implicit operator NumberFixedPoint2(int integer)
    {
        return new NumberFixedPoint2 { number = integer };
    }

    // this type is a decimal number; can convert to System.Decimal
    public static implicit operator decimal(NumberFixedPoint2 nfp2)
    {
        return nfp2.number;
    }

    /* will add more nice members later */
}

Il a été écrit de telle sorte que seules les conversions sûres qui ne perdent pas en précision sont autorisées. Cependant, lorsque j'essaie ce code :

    static void Main()
    {
        decimal bad = 2.718281828m;
        NumberFixedPoint2 badNfp2 = (NumberFixedPoint2)bad;
        Console.WriteLine(badNfp2);
    }

Je suis surpris que cela compile et, quand on l'exécute, écrit 2 . La conversion de int (de valeur 2 ) à NumberFixedPoint2 est important ici. (Une surcharge de WriteLine qui prend en compte un System.Decimal est préférable, au cas où quelqu'un se poserait la question).

Pourquoi diable la conversion de decimal a NumberFixedPoint2 autorisé ? (A propos, dans le code ci-dessus, si NumberFixedPoint2 passe d'un struct à une classe, rien ne change).

Savez-vous si la spécification du langage C# dit qu'une conversion implicite de int à un type personnalisé "implique" l'existence d'une conversion explicite "directe" de decimal à ce type personnalisé ?

Cela devient bien pire. Essayez plutôt ce code :

    static void Main()
    {
        decimal? moreBad = 7.3890560989m;
        NumberFixedPoint2? moreBadNfp2 = (NumberFixedPoint2?)moreBad;
        Console.WriteLine(moreBadNfp2.Value);
    }

Comme vous le voyez, nous avons (soulevé) Nullable<> conversions ici. Mais oh oui, ça se compile.

Lorsqu'il est compilé en x86 "plateforme", ce code écrit une valeur numérique imprévisible. Laquelle varie de temps en temps. Par exemple, à une occasion, j'ai obtenu 2289956 . Ça, c'est un sérieux bug !

Lorsqu'il est compilé pour le x64 le code ci-dessus fait planter l'application avec un message de type System.InvalidProgramException avec message Common Language Runtime a détecté un programme invalide. Selon la documentation de l InvalidProgramException classe :

En général, cela indique un bogue dans le compilateur qui a généré le programme.

Quelqu'un (comme Eric Lippert, ou quelqu'un qui a travaillé avec des conversions liftées dans le compilateur C#) connaît-il la cause de ces bogues ? Par exemple, quelle est la condition suffisante pour que nous ne les rencontrions pas dans notre code ? Parce que le type NumberFixedPoint2 est en fait quelque chose que nous avons dans le code réel (gérer l'argent d'autres personnes et autres).

44voto

Jon Skeet Points 692016

Pour commencer, je ne réponds qu'à la première partie de la question. (Je suggère que la deuxième partie fasse l'objet d'une question distincte ; il s'agit plus vraisemblablement d'un bogue).

Il y a seulement un explicite conversion de decimal a int mais cette conversion est en cours implicitement appelé dans votre code. La conversion se fait dans cet IL :

IL_0010:  stloc.0
IL_0011:  ldloc.0
IL_0012:  call       int32 [mscorlib]System.Decimal::op_Explicit(valuetype [mscorlib]System.Decimal)
IL_0017:  call       valuetype NumberFixedPoint2 NumberFixedPoint2::op_Implicit(int32)

Je crois que c'est le correct comportement selon la spécification, même si c'est surprenant. 1 . Parcourons la section 6.4.5 de la spécification C# 4 (User-Defined Explicit Conversions). Je ne vais pas recopier tout le texte, car ce serait fastidieux - juste les résultats pertinents dans notre cas. De même, je ne vais pas utiliser d'indices, car ils ne fonctionnent pas bien avec la police de code ici :)

  • Déterminer les types S0 y T0 : S0 es decimal y T0 es NumberFixedPoint2 .
  • Trouvez l'ensemble des types, D à partir desquels les opérateurs de conversion définis par l'utilisateur seront considérés : juste { decimal, NumberFixedPoint2 }
  • Trouver l'ensemble des opérateurs de conversion applicables, définis par l'utilisateur et levés, U . decimal englobe int (section 6.4.3) parce qu'il existe une conversion implicite standard à partir de int a decimal . Ainsi, l'opérateur de conversion explicite es en U et est en effet le seul membre de U
  • Trouvez le type de source le plus spécifique, Sx des opérateurs dans U
    • L'opérateur ne convertit pas de S ( decimal ) donc la première balle est sortie
    • L'opérateur ne convertit pas d'un type qui englobe S ( decimal englobe int et non l'inverse), donc la deuxième balle est éliminée.
    • Il ne reste que la troisième puce, qui parle du "type le plus englobant" - eh bien, nous n'avons qu'un seul type, donc c'est bon : Sx es int .
  • Trouvez le type de cible le plus spécifique, Tx des opérateurs dans U
    • L'opérateur passe directement à NumberFixedPoint2 donc Tx es NumberFixedPoint2 .
  • Trouvez l'opérateur de conversion le plus spécifique :
    • U contient exactement un opérateur, qui convertit en effet de Sx a Tx C'est donc l'opérateur le plus spécifique
  • Enfin, appliquez la conversion :
    • Si S n'est pas Sx puis une conversion explicite standard de S a Sx est effectuée. (Donc c'est decimal a int .)
    • L'opérateur de conversion le plus spécifique défini par l'utilisateur est invoqué (votre opérateur).
    • T es Tx donc il n'y a pas besoin de la conversion dans le troisième point.

La ligne en gras est celle qui confirme qu'une conversion explicite standard est réellement réalisable, alors que seule une conversion explicite à partir d'un type différent est effectivement spécifiée.


1 J'ai trouvé ça surprenant, en tout cas. Je ne suis pas au courant d'avoir vu ça avant.

24voto

Reed Copsey Points 315315

Votre deuxième partie (utilisation de types nullables) semble être très similaire à ceci bogue connu dans le compilateur actuel. De la réponse sur la question de la connexion :

Bien que nous n'ayons pas prévu de résoudre ce problème dans la prochaine version de Visual Studio, nous envisageons d'étudier un correctif dans Roslyn.

En tant que tel, ce bogue sera, nous l'espérons, corrigé dans une prochaine version de Visual Studio et des compilateurs.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X