Poważne błędy z podniesieniem / nullable konwersji z int, umożliwiając konwersję z dziesiętnych

Myślę, że to pytanie przyniesie mi natychmiastową sławę tutaj na Stack Overflow.

Załóżmy, że masz następujący typ:

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

Został napisany w taki sposób, że dozwolone są tylko bezpieczne konwersje, które nie tracą precyzji. Jednak, gdy wypróbuję ten kod:

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

Jestem zaskoczony, że to kompiluje i po uruchomieniu pisze 2. Konwersja z int (o wartości 2) na NumberFixedPoint2 jest tutaj ważna. (Preferowane jest przeciążenie WriteLine, które zajmuje System.Decimal, gdyby ktoś się zastanawiał.)

Dlaczego, u licha, konwersja z decimal na NumberFixedPoint2 jest dozwolona? (Nawiasem mówiąc, w powyższym kodzie, Jeśli NumberFixedPoint2 zostanie zmieniona ze struktury na klasę, nic się nie zmieni.)

Czy wiesz, czy specyfikacja języka C# mówi, że niejawna konwersja z int do typu niestandardowego "implikuje" istnienie "bezpośredniej" jawnej konwersji z decimal do tego typu niestandardowego?

[19]}staje się znacznie gorzej. Spróbuj użyć tego kodu:
    static void Main()
    {
        decimal? moreBad = 7.3890560989m;
        NumberFixedPoint2? moreBadNfp2 = (NumberFixedPoint2?)moreBad;
        Console.WriteLine(moreBadNfp2.Value);
    }

Jak widzisz, my mieć (podniósł) Nullable<> konwersje tutaj. Ale o tak, to się skompiluje.

Po skompilowaniu w x86 "platform", kod ten wypisuje nieprzewidywalną wartość liczbową. Który z nich zmienia się od czasu do czasu. Jako przykład, pewnego razu dostałem 2289956. To jeden poważny błąd!

Podczas kompilacji dla platformy x64 powyższy kod zawiesza aplikację z System.InvalidProgramException z Komunikatem Common Language Runtime wykrył nieprawidłowy program. Zgodnie z dokumentacją klasy InvalidProgramException:

Ogólnie oznacza to błąd w kompilatorze, który wygenerował program.

Czy ktoś (jak Eric Lippert, lub ktoś, kto pracował z podniesieniami konwersji w kompilatorze C#) zna przyczynę tych błędów? Na przykład, jaki jest wystarczający warunek, że nie natkniemy się na nie w naszym kodzie? Ponieważ typ NumberFixedPoint2 jest w rzeczywistości czymś, co mamy w prawdziwym kodzie (zarządzanie cudzymi pieniędzmi i rzeczy).

Author: Jeppe Stig Nielsen, 2013-08-20

2 answers

Twoja druga część (używając typów nullable) wydaje się być bardzo podobna do tego znanego błędu w obecnym kompilatorze. z odpowiedzi na problem Connect:

Chociaż obecnie nie mamy planów rozwiązania tego problemu w następnej wersji Visual Studio, planujemy zbadać poprawkę w Roslyn

W związku z tym, mam nadzieję, że ten błąd zostanie poprawiony w przyszłym wydaniu Visual Studio i kompilatorów.

 24
Author: Reed Copsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-20 19:07:41

Odpowiadam na pierwszą część pytania na początek. (Sugeruję, że druga część powinna być osobnym pytaniem; bardziej prawdopodobne jest, że będzie to błąd.)

Istnieje tylko Jawna konwersja Z decimal na int, ale ta konwersja jest niejawnie wywoływana w Twoim kodzie. Konwersja odbywa się w tym 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)

Uważam, że to jest poprawne zachowanie zgodnie ze specyfikacją, mimo że zaskakujące1. Przejdźmy do sekcji 6.4.5 specyfikacji C # 4 (User-Defined Explicit Conversions). Nie będę kopiował całego tekstu, bo byłby nudny - tylko jakie są istotne wyniki w naszym przypadku. Podobnie Nie będę używał indeksów dolnych, ponieważ nie działają one dobrze z czcionką kodu:)

  • określić typy S0 i T0: S0 jest decimal i T0 jest NumberFixedPoint2.
  • Znajdź zbiór typów, D, z których będą brane pod uwagę użyte zdefiniowane operatory konwersji: just { decimal, NumberFixedPoint2 }
  • Znajdź zbiór odpowiednich operatorów konwersji zdefiniowanych przez użytkownika i podnoszonych, U. decimal obejmuje int (Sekcja 6.4.3) ponieważ istnieje standardowa konwersja implicit z int na decimal. Tak więc operator jawnej konwersji jest w U i jest rzeczywiście jedynym członkiem U
  • Znajdź najbardziej konkretny typ źródła, Sx, z operatorów w U
    • operator nie konwertuje z S (decimal) więc pierwsza kula jest out
    • operator nie konwertuje z typu, który obejmuje S (decimal int, nie odwrotnie), więc drugi pocisk wypadł
    • zostaje trzecia kula, która mówi o" najbardziej obejmującym typie " - Cóż, mamy tylko jeden typ, więc jest w porządku: Sx jest int.
  • Znajdź najbardziej konkretny typ docelowy, Tx, z operatorów w U
    • operator przechodzi prosto do NumberFixedPoint2 więc Tx jest NumberFixedPoint2.
  • Znajdź najbardziej konkretny operator konwersji:
    • U zawiera dokładnie jeden operator, który rzeczywiście przekształca się z Sx na Tx, więc jest to najbardziej konkretny operator
  • na koniec zastosuj konwersję:
    • Jeśli S Nie jest Sx, to wykonywana jest standardowa konwersja jawna z S na Sx. (czyli decimal do int.)
    • wywoływany jest najbardziej konkretny operator konwersji zdefiniowany przez użytkownika (Twój operator)
    • T jest Tx więc nie ma potrzeby konwersji w trzecim pocisku

Linia pogrubiona jest bitem, który potwierdza, że standardowa Jawna konwersja jest naprawdę wykonalna, gdy tylko Jawna konwersja z innego typu jest faktycznie określona.


1 przynajmniej mnie to zaskoczyło. Nie jestem świadoma tego wcześniej.

 44
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-20 19:15:11