Проблема преобразования из int в float

Есть странное поведение, которое я не могу понять. Согласовано, что число точек с плавающей точкой является приближением, поэтому даже операции, которые, очевидно, возвращают число без десятичных чисел, могут быть приближены к чему-то с десятичными знаками.

Я делаю это:

int num = (int)(195.95F * 100);

и поскольку это операция с плавающей запятой, я получаю 19594 вместо 19595 года.. но это правильно.

Меня озадачивает то, что если я делаю

float flo = 195.95F * 100;
int num = (int) flo;

Я получаю правильный результат 19595 года.

Любая идея, почему это происходит?

Ответы

Ответ 1

Я посмотрел, был ли это компилятор, выполняющий математику, но он ведет себя таким образом, даже если вы его отключите:

static void Main()
{
    int i = (int)(GetF() * GetI()); // 19594
    float f = GetF() * GetI();
    int j = (int)f; // 19595
}
[MethodImpl(MethodImplOptions.NoInlining)]
static int GetI() { return 100; }
[MethodImpl(MethodImplOptions.NoInlining)]
static float GetF() { return 195.95F; }

Похоже, что разница в том, остается ли она в регистрах (шире, чем нормальный r4), или принудительно изменяется на переменную float:

L_0001: call float32 Program::GetF()
L_0006: call int32 Program::GetI()
L_000b: conv.r4 
L_000c: mul 
L_000d: conv.i4 
L_000e: stloc.0 

против

L_000f: call float32 Program::GetF()
L_0014: call int32 Program::GetI()
L_0019: conv.r4 
L_001a: mul 
L_001b: stloc.1 
L_001c: ldloc.1 
L_001d: conv.i4 
L_001e: stloc.2 

Единственное отличие - это stloc.1/ldloc.1.

Это подтверждается тем фактом, что если вы сделаете оптимизированную сборку (которая удалит локальную переменную), я получаю тот же ответ (19594) для обоих.

Ответ 2

этот код...

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            float result = 195.95F*100;
            int intresult = (int)(195.95F * 100);
        }
    }
}

дают этот IL

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       14 (0xe)
  .maxstack  1
  .locals init ([0] float32 result,
           [1] int32 intresult)
  IL_0000:  nop
  IL_0001:  ldc.r4     19595.
  IL_0006:  stloc.0
  IL_0007:  ldc.i4     0x4c8a
  IL_000c:  stloc.1
  IL_000d:  ret
} // end of method Program::Main

посмотрите на IL_00001 → компилятор сделал расчёт.. В противном случае существует проблема с двоичным преобразованием decimal →

Ответ 3

Отметьте ответ правильно, что это преобразование между nativefloat и float32/float64.

Это описано в спецификации CLR ECMA, но David Notario объясняет это намного лучше, чем я мог.

Ответ 4

Попробуйте преобразовать float в double во втором примере:

double flo = 195.95F * 100;
int num = (int) flo;

Я предполагаю, что в первом примере компилятор использует double для хранения промежуточного результата, и поэтому в случае с плавающей точкой вы теряете точность.

Ответ 5

При умножении на 100, это целое число, поэтому на этом шаге выполняется неявное преобразование. Если вы поместите "F" за 100, я буду держать пари, что они будут такими же.

Обычно я использую бокс /unboxing с круглыми скобками, когда это ссылочный тип. Когда это тип значения, я пытаюсь использовать статические методы Convert.

Попробуйте Convert.ToSingle(YourNumber); для более надежного преобразования.

НТН

Ответ 6

Я не могу ответить, почему второй работает, а первый - нет. Тем не менее, я могу сказать вам, что 195.95 является бесконечным десятичным числом в двоичном формате, и такие ошибки округления, как этот, неизбежно произойдут.

Попробуйте преобразовать в double, а не в float. Вы также можете использовать деньги или десятичный тип, а не float. Это сохранит номер по-разному и более точно.

Подробнее о числах с плавающей запятой и представлении IEEE см. здесь:

http://en.wikipedia.org/wiki/IEEE_754