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

我有以下简单的代码:

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

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

speed1 = 61
speed2 = 62

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

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

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

有人能解释一下吗?

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

speed1:

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:

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,结果都是一样的。

20019 次浏览

我的猜测是,具有浮点精度的 6.2f实际表示是 6.1999999,而 62f可能类似于 62.00000001(int)铸造总是 截断十进制值,这就是为什么你得到的行为。

编辑 : 根据评论,我已经将 int铸造的行为重新定义为一个更精确的定义。

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

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

就会读

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

差别可能与舍入有关: 如果你选择 double,你可能会得到类似61.78426的值。

请注意以下输出

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

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

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

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

float f = 6.2f * 10的情况下,取双倍值61.99999809265137和 四舍五入到最接近的 float,即62。然后将该 float截断为一个整数,结果是62。

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

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

更新: 正如注释中指出的那样,表达式 6.2f * 10在形式上是一个 float,因为第二个参数有一个到 float的隐式转换,它是 好多了,而不是到 double的隐式转换。

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

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

描述

漂浮的数字很少精确。 6.2f类似于 6.1999998...。 如果你将它强制转换成一个 int,它会截断它,而 this * 10的结果是61。

看看 Jon Skeets 的 DoubleConverter课程。使用这个类,您可以真正将浮点数的值可视化为字符串。Doublefloat都是 浮动数字,小数不是(它是一个定点数)。

样本

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

更多资讯

看看 IL:

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 进行的,它们似乎应用了不同的规则..。

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

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

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

这是不同的,当两个单被乘,在这种情况下,没有截断运算,但只有近似。

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

我编译并反汇编了这段代码(在 Win7/. NET 4.0上)。 我想编译器会将浮点常量表达式计算为 double。

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

简短的回答是,这两个数字经历了不同的转换序列。要得到同样的结果,一个更明确的方法是:

    var speed1 = (int)((double)6.2f * 10);        // = 61
var speed2 = (int)(float)((double)6.2f * 10); // = 62

由于浮点表示的固有精度不足,乘法的结果是 稍微少一点的两倍,而不是62。将其转换为浮点数将四舍五入到最接近的浮点数值,该值刚好略高于62。转换为 int 会丢弃分数部分,因此分别得到61和62。

您的问题可能是为什么这些转换对于示例中的两个变量发生的方式不同。因为看看源代码,甚至看看 IL,它们看起来经历了完全相同的计算和转换。

但是。网络类型系统是鬼鬼祟祟的。虽然它表面上支持大量的数值类型,但是只支持三种不同类型的算术运算: intlongdouble。任何其他数值类型的操作数(如 shortbytefloat和各种无符号类型)在操作之前都会转换成这三种超类型之一。

在 CLR 级别,堆栈只支持这三种超类型。加载指令将从内存中读取一个值,将其扩展到这三种类型之一,并将其放在堆栈上。存储操作将在堆栈上获取一个值,并在存储该值时将其截断为目标内存槽的大小。

操作 ldc.r4意味着将这个单精度浮点类型的常数 a 加载到堆栈上。但是当放置在堆栈上时,它被展开为一个双精度值。所以 mul运算发生在两个双精度数上,结果是一个双精度数。现在您可以看到发生了什么: 对于 speed 1,生成的 double 直接转换为 int。对于 sped2,栈上的 double 以浮点形式存储在内存中,这导致它的精度降低了一半。然后将其加载回堆栈,从而再次扩展为一个 double。但是对 float 的截断导致了精度的损失,这意味着对 int 的强制转换将产生不同的结果。