C#Float表达式:将结果float转换为int时的奇怪行为

C# Float expression: strange behavior when casting the result float to int

我有以下简单的代码:

1
2
3
int speed1 = (int)(6.2f * 10);
float tmp = 6.2f * 10;
int speed2 = (int)tmp;

speed1speed2的值应该相同,但实际上,我有:

1
2
speed1 = 61
speed2 = 62

我知道我可能应该使用数学。圆而不是铸造,但我想知道为什么值是不同的。

我查看了生成的字节码,但是除了存储和加载之外,操作码是相同的。

我还尝试了Java中的相同代码,我正确地获得了62和62。

有人能解释一下吗?

编辑:在实际代码中,它不是直接的6.2f*10,而是一个函数调用*一个常量。我有以下字节码:

对于speed1

1
2
3
4
5
6
IL_01b3:  ldloc.s    V_8
IL_01b5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ba:  ldc.r4     10.
IL_01bf:  mul
IL_01c0:  conv.i4
IL_01c1:  stloc.s    V_9

对于speed2

1
2
3
4
5
6
7
8
IL_01c3:  ldloc.s    V_8
IL_01c5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ca:  ldc.r4     10.
IL_01cf:  mul
IL_01d0:  stloc.s    V_10
IL_01d2:  ldloc.s    V_10
IL_01d4:  conv.i4
IL_01d5:  stloc.s    V_11

我们可以看到操作数是浮动的,唯一的区别是stloc/ldloc

至于虚拟机,我尝试使用mono/win7、mono/macos和.net/windows,结果是一样的。


首先,我假设您知道,由于浮点舍入(它实际上是值61.9999809265137,当表示为double),6.2f * 10不完全是62,并且您的问题只是为什么两个看似相同的计算会导致错误的值。

答案是,在(int)(6.2f * 10)的情况下,将double值61.9999809265137截断为整数,得到61。

如果是float f = 6.2f * 10,则取双值61.9999809265137,四舍五入到最近的float,即62。然后将该float截断为整数,结果是62。

练习:解释以下操作顺序的结果。

1
2
3
double d = 6.2f * 10;
int tmp2 = (int)d;
// evaluate tmp2

更新:如注释所述,由于第二个参数隐式转换为float,所以表达式6.2f * 10正式为float,这比隐式转换为double要好。

实际问题是,编译器允许(但不需要)使用比形式类型精度更高的中介(第11.2.2节)。这就是您在不同系统上看到不同行为的原因:在表达式(int)(6.2f * 10)中,编译器可以选择在转换为int之前将值6.2f * 10保持为高精度中间形式。如果是这样,那么结果是61。如果没有,则结果是62。

在第二个示例中,对float的显式赋值强制舍入在转换为整数之前进行。


描述

浮点数很少精确。6.2f类似于6.1999998...。如果您将其强制转换为int,它将截断它,而这个*10将得到61。

看看乔恩·斯基茨·埃多克斯(JonSkeets-EDOCX1)(25)的课。通过这个类,您可以真正地将浮点数的值可视化为字符串。doublefloat都是浮点数,decimal不是(定点数)。

样品

1
2
DoubleConverter.ToExactString((6.2f * 10))
// output 61.9999980926513671875

更多信息

  • 乔恩·斯基特的双重转换课程
  • assert.areequal()与system.double混淆
  • 每一个计算机科学家都应该知道什么是浮点运算


看看IL:

1
2
3
4
5
6
7
IL_0000:  ldc.i4.s    3D              // speed1 = 61
IL_0002:  stloc.0
IL_0003:  ldc.r4      00 00 78 42     // tmp = 62.0f
IL_0008:  stloc.1
IL_0009:  ldloc.1
IL_000A:  conv.i4
IL_000B:  stloc.2

编译器将编译时常量表达式减少到它们的常量值,我认为在将常量转换为int时,它在某一点上做了错误的近似。在speed2的情况下,这种转换不是由编译器进行的,而是由clr进行的,它们似乎应用了不同的规则…


我编译并反汇编了这段代码(在win7/.net 4.0上)。我猜编译器将浮动常量表达式计算为double。

1
2
3
4
5
6
7
8
9
10
int speed1 = (int)(6.2f * 10);
   mov         dword ptr [rbp+8],3Dh       //result is precalculated (61)

float tmp = 6.2f * 10;
   movss       xmm0,dword ptr [000004E8h]  //precalculated (float format, xmm0=0x42780000 (62.0))
   movss       dword ptr [rbp+0Ch],xmm0

int speed2 = (int)tmp;
   cvttss2si   eax,dword ptr [rbp+0Ch]     //instrunction converts float to Int32 (eax=62)
   mov         dword ptr [rbp+10h],eax

我的猜测是,具有浮点精度的6.2f实表示是6.1999999,而62f可能与62.00000001类似。(int)casting总是截断十进制值,所以这就是您得到这种行为的原因。

编辑:根据评论,我将int的行为重新表述为更精确的定义。


Single只包含7位数字,当将其转换为Int32时,编译器将截断所有浮点数字。在转换过程中,可能会丢失一个或多个有效数字。

1
Int32 speed0 = (Int32)(6.2f * 100000000);

给出61999980的结果,所以(Int32)(6.2f*10)给出61。

当两个单值相乘时是不同的,在这种情况下,没有截断操作,只有近似值。

请参阅http://msdn.microsoft.com/en-us/library/system.single.aspx


是否有原因将类型转换为int而不是解析?

1
int speed1 = (int)(6.2f * 10)

然后阅读

1
int speed1 = Int.Parse((6.2f * 10).ToString());

区别可能与四舍五入有关:如果您将其强制转换为double,您可能会得到类似于61.78426的结果。

请注意以下输出

1
2
int speed1 = (int)(6.2f * 10);//61
double speed2 = (6.2f * 10);//61.9999980926514

这就是为什么你得到不同的价值观!