Серьезные ошибки с поднятыми / обнуляемыми преобразованиями из int, позволяющими преобразование из десятичного числа
Я думаю, что этот вопрос принесет мне мгновенную славу здесь, на переполнении стека.
Предположим, у вас есть следующий тип:
// 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 */
}
Он написан так, что разрешены только безопасные преобразования, которые не теряют точности. Тем не менее, когда я пытаюсь этот код:
static void Main()
{
decimal bad = 2.718281828m;
NumberFixedPoint2 badNfp2 = (NumberFixedPoint2)bad;
Console.WriteLine(badNfp2);
}
Я удивлен, что это компилируется и, когда запускается, пишет 2
, Преобразование из int
(стоимости 2
) чтобы NumberFixedPoint2
здесь важно (Перегрузка WriteLine
это занимает System.Decimal
предпочтительнее, на случай, если кто-нибудь спросит.)
Почему на Земле происходит преобразование из decimal
в NumberFixedPoint2
позволил? (Кстати, в приведенном выше коде, если NumberFixedPoint2
изменяется от структуры к классу, ничего не меняется.)
Знаете ли вы, если спецификация языка C# говорит, что неявное преобразование из int
к пользовательскому типу "подразумевает" существование "прямого" явного преобразования из decimal
к этому пользовательскому типу?
Становится намного хуже. Попробуйте этот код вместо этого:
static void Main()
{
decimal? moreBad = 7.3890560989m;
NumberFixedPoint2? moreBadNfp2 = (NumberFixedPoint2?)moreBad;
Console.WriteLine(moreBadNfp2.Value);
}
Как видите, мы (подняли) Nullable<>
преобразования здесь. Но да, это компилируется.
При компиляции в x86 "платформе" этот код записывает непредсказуемое числовое значение. Какой из них меняется время от времени. Как пример, однажды я получил 2289956
, Это серьезная ошибка!
При компиляции для платформы x64 приведенный выше код вызывает сбой приложения с System.InvalidProgramException
с сообщением Common Language Runtime обнаружил недопустимую программу. Согласно документации InvalidProgramException
учебный класс:
Обычно это указывает на ошибку в компиляторе, который сгенерировал программу.
Кто-нибудь (как Эрик Липперт или кто-то, кто работал с поднятыми преобразованиями в компиляторе C#) знает причину этих ошибок? Например, что является достаточным условием, чтобы мы не сталкивались с ними в нашем коде? Потому что тип NumberFixedPoint2
на самом деле это то, что мы имеем в реальном коде (управление деньгами и вещами других людей).
2 ответа
Ваша вторая часть (с использованием обнуляемых типов) очень похожа на эту известную ошибку в текущем компиляторе. Из ответа на вопрос подключения:
Хотя в настоящее время у нас нет планов решения этой проблемы в следующем выпуске Visual Studio, мы планируем исследовать исправление в Roslyn.
Таким образом, мы надеемся, что эта ошибка будет исправлена в следующем выпуске Visual Studio и компиляторов.
Я просто отвечаю на первую часть вопроса для начала. (Я предлагаю, чтобы вторая часть была отдельным вопросом; скорее всего, это ошибка.)
Там только явное преобразование из decimal
в int
, но это преобразование неявно вызывается в вашем коде. Преобразование происходит в этом 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)
Я считаю, что это правильное поведение в соответствии со спецификацией, хотя это удивительно 1. Давайте проработаем раздел 6.4.5 спецификации C# 4 (пользовательские явные преобразования). Я не собираюсь копировать весь текст, так как это было бы утомительно - каковы соответствующие результаты в нашем случае. Точно так же я не собираюсь использовать подписки, так как они плохо работают со шрифтом кода здесь:)
- Определить типы
S0
а такжеT0
:S0
являетсяdecimal
, а такжеT0
являетсяNumberFixedPoint2
, - Найти множество типов,
D
, из которых будут рассматриваться используемые операторы преобразования:{ decimal, NumberFixedPoint2 }
- Найти набор применимых пользовательских и отмененных операторов преобразования,
U
,decimal
охватываетint
(раздел 6.4.3), потому что есть стандартное неявное преобразование изint
вdecimal
, Таким образом, оператор явного преобразования находится вU
и действительно единственный членU
- Найти наиболее конкретный тип источника,
Sx
из операторов вU
- Оператор не конвертирует из
S
(decimal
) так что первая пуля вышла - Оператор не конвертирует из типа, который охватывает
S
(decimal
охватываетint
не наоборот) так что вторая пуля - Это просто оставляет третий пункт, который говорит о "наиболее охватывающем типе" - ну, у нас есть только один тип, так что все в порядке:
Sx
являетсяint
,
- Оператор не конвертирует из
- Найти наиболее конкретный тип цели,
Tx
из операторов вU
- Оператор разговаривает прямо с
NumberFixedPoint2
такTx
являетсяNumberFixedPoint2
,
- Оператор разговаривает прямо с
- Найдите наиболее конкретного оператора преобразования:
U
содержит ровно один оператор, который действительно конвертирует изSx
вTx
так что это самый конкретный оператор
- Наконец, примените преобразование:
- Если
S
не являетсяSx
, то стандартное явное преобразование изS
вSx
выполняется. (Так вотdecimal
вint
.) - Вызывается самый определенный пользовательский оператор преобразования (ваш оператор)
T
являетсяTx
так что нет необходимости в преобразовании в третью пулю
- Если
Строка, выделенная жирным шрифтом, является битом, который подтверждает, что стандартное явное преобразование действительно возможно, когда фактически указывается только явное преобразование из другого типа.
+1 Ну, я нашел это удивительным, по крайней мере. Я не знаю, чтобы увидеть это раньше.