Серьезные ошибки с отмененными/обнуляемыми преобразованиями из int, позволяющие преобразовать из десятичной
Я думаю, что этот вопрос принесет мне мгновенную славу здесь в Stack Overflow.
Предположим, что у вас есть следующий тип:
// 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
, которая принимает значение a System.Decimal
, является предпочтительной, если кто-то задается вопросом.)
Почему на Земле разрешено преобразование с decimal
в NumberFixedPoint2
? (Кстати, в приведенном выше коде, если NumberFixedPoint2
изменяется от структуры к классу, ничего не меняется.)
Знаете ли вы, что спецификация языка С# говорит, что неявное преобразование из 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
:
Обычно это указывает на ошибку в компиляторе, который сгенерировал программу.
Кто-нибудь (например, Эрик Липперт или кто-то, кто работал с отмененными преобразованиями в компиляторе С#) знает причину этих ошибок? Как, что является достаточным условием, что мы не сталкиваемся с ними в нашем коде? Поскольку тип NumberFixedPoint2
на самом деле является тем, что мы имеем в реальном коде (управляем другими людьми деньгами и т.д.).
Ответы
Ответ 1
Ваша вторая часть (с использованием типов с нулевым значением), по-видимому, очень похожа на эту известную ошибку в текущем компиляторе. Из ответа на Connect вопрос:
Пока у нас нет планов решить эту проблему в следующей версии Visual Studio, мы планируем расследовать исправление в Roslyn
Таким образом, эта ошибка, мы надеемся, будет исправлена в будущей версии Visual Studio и компиляторов.
Ответ 2
Я просто отвечаю на первую часть вопроса, чтобы начать. (Я полагаю, что вторая часть должна быть отдельным вопросом, скорее всего, будет ошибкой.)
Там только явное преобразование от 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 спецификации С# 4 (Пользовательские явные конверсии). Я не собираюсь копировать весь текст, поскольку это было бы утомительно - именно то, что в нашем случае есть соответствующие результаты. Аналогично, я не буду использовать индексы, так как они плохо работают с шрифтом кода здесь:)
- Определите типы
S0
и T0
: S0
is decimal
, а T0
- NumberFixedPoint2
.
- Найдите набор типов
D
, из которых будут рассмотрены используемые операторы преобразования: just { 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 Хорошо, мне показалось удивительным, по крайней мере. Я не знаю об этом раньше.