Q:

Получение неправильного ответа

Почему при вводе числа 2 в e_power_enter программа выводит в MessageBox ответ 19, а не 20?
(engine.power типа float)
int k;
engine.power = float.Parse(e_power_enter.Text) / 100;            
k = (int)(engine.power * 1000);
MessageBox.Show(Convert.ToString(k));

A:

Дело в том, что float/double в C# (а также в Java, С++ и т. д.) — двоичные дроби. Они представлены внутри как набор из целого числа (мантиссы) и степени двойки (порядка). Число 2 может быть представлено в виде двоичной дроби, а вот 0.02 — нет, т. к. оно равно 1/50 (а в знаменателе не только степени двойки). Поэтому число 0.02 не может быть точно представлено в виде float.
Что же содержится в переменной engine.power? Там содержится двоичная дробь, наиболее близкая к 0.02. Проверим это таким кодом:
float enginepower = 2f / 100;
Console.WriteLine(enginepower * 100 - 2);

Он выдаёт не 0, как можно было бы ожидать, а -4,470348E-08 (на моей машине).
Это значит, что значение engine.power реально немного меньше двух. При умножении на 1000 результат будет немного меньше 20, приведение к int отбрасывает дробную часть, и результат получается равным 19.

А что нужно делать? Есть несколько вариантов.

Вместо неустойчивого к мелким ошибкам отбрасывания дробной части 
(int)(engine.power * 1000)

использовать гораздо более здравое округление:
(int)Math.Round(engine.power * 1000)

Если вам похожие проблемы встречаются часто, имеет смысл перейти на тип данных decimal, в котором числа внутри хранятся как десятичные, а не двоичные дроби. Учтите, что операции с этим типом данных медленнее, т. к. нету нативной поддержки процессорами.

Продвинутое расследование, с копанием в ассемблерном выхлопе и спецификации.
На моей машине вот такой код:
float f = 0.02f;
float ff = f * 1000;
int k = (int)(ff);

вычисляет в k значение 20, а вот такой:
float f = 0.02f;
int k = (int)(f * 1000);

— 19. (Это в Debug-режиме; в Release-режиме обе версии текста производят один и тот же ассемблерный код и одинаковый результат — 19.) Расследую, почему так.
Произведённый JIT ассемблерный код такой:
            ; float f = 0.02f;
mov         dword ptr [ebp-40h],3CA3D70Ah   ; 32bit f = 0.02f
            ; float ff = f * 1000;
fld         dword ptr [ebp-40h]             ; extend f to 80bit prec and push
fmul        dword ptr ds:[1453D00h]         ; multiply by 32bit 1000f
fstp        dword ptr [ebp-44h]             ; pop and convert 80bit result to 32 bit -> ff
            ; int k = (int)(ff);
fld         dword ptr [ebp-44h]             ; extend ff to 80bit prec and push
fstp        qword ptr [ebp-50h]             ; pop and convert to 64bit -> double temp
movsd       xmm0,mmword ptr [ebp-50h]       ; extend temp to 128bit and copy to xmm0
cvttsd2si   eax,xmm0                        ; truncate to 32bit int eax
mov         dword ptr [ebp-48h],eax         ; store to k

и
            ; float f = 0.02f;
mov         dword ptr [ebp-40h],3CA3D70Ah   ; 32bit f = 0.02f
            ; int k = (int)(f * 1000);
fld         dword ptr [ebp-40h]             ; extend f to 80bit prec and push
fmul        dword ptr ds:[1393CF4h]         ; multiply by 32bit 1000f
fstp        qword ptr [ebp-4Ch]             ; pop and convert to 64bit -> double temp
movsd       xmm0,mmword ptr [ebp-4Ch]       ; extend temp to 128bit and copy to xmm0
cvttsd2si   eax,xmm0                        ; truncate to 32bit int eax
mov         dword ptr [ebp-44h],eax         ; store to k

Это можно условно записать так:
float32 f = 2f / 100;
float80 r1 = f;
r1 *= float32(1000f);
float32 ff = r1;
r1 = ff;              // <-- тут потеря точности
float64 temp = r1;
float128 r2 = temp;
int32 k = (int32)r2;

и
float32 f = 2f / 100;
float80 r1 = f;
r1 *= float32(1000f);
float64 temp = r1;
float128 r2 = temp;
int32 k = (int32)r2;

Разница, как мы видим, в том, что для вычисления промежуточного результата значение f * 1000 обрезается до 32-битной точности, а потом загружается назад в 80-битный регистр, а оттуда через 64-битное значение загружается в XMM-регистр. Во втором варианте кода сохранение промежуточного результата отсутствует, код не обрезает значение, и до 64 бит обрезается более точное 80-битное значение, что и приводит к разнице в результате.

Явное разрешение на такие разные пути вычисления есть в спецификации языка, §4.1.6 Floating Point Types:

Floating-point operations may be performed with higher precision than the result type of the operation. For example, some hardware architectures support an “extended” or “long double” floating-point type with greater range and precision than the double type, and implicitly perform all floating-point operations using this higher precision type.

Ещё по теме: Strange behavior when casting a float to int in C# (особенно верхний ответ).

