这是此问题的后续内容:Why does a division result differ based on the cast type?
快速摘要:
byte b1 = (byte)(64 / 0.8f); // b1 is 79
int b2 = (int)(64 / 0.8f); // b2 is 79
float fl = (64 / 0.8f); // fl is 80
问题是:为什么结果会根据演员类型而有所不同?在找到答案的时候,我遇到了一个我无法解释的问题。
var bytes = BitConverter.GetBytes(64 / 0.8f).Reverse(); // Reverse endianness
var bits = bytes.Select(b => Convert.ToString(b, 2).PadLeft(8, '0'));
Console.WriteLine(string.Join(" ", bits));
这输出以下内容:
01000010 10100000 00000000 00000000
以IEEE 754格式分解:
0 10000101 01000000000000000000000
星座:
0 => Positive
指数:
10000101 => 133 in base 10
尾数:
01000000000000000000000 => 0*2^-1 + 1*2^-2 + 0*2^-3 ... = 1/4 = 0.25
十进制表示:
(1 + 0.25) * 2^(133 - 127) (Subtract single precision bias)
这恰好导致80.那么为什么投射结果有所不同?
答案 0 :(得分:4)
我在另一个帖子中的答案并不完全正确:实际上,在运行时计算时, (byte)(64 / 0.8f)
为80 。
在运行时将包含float
结果的64 / 0.8f
转换为byte
时,结果实际为80.但是,当转换为完成时,情况并非如此。作业的一部分:
float f1 = (64 / 0.8f);
byte b1 = (byte) f1;
byte b2 = (byte)(64 / 0.8f);
Console.WriteLine(b1); //80
Console.WriteLine(b2); //79
当b1包含预期结果时,b2关闭。根据反汇编,b2分配如下:
mov dword ptr [ebp-48h],4Fh
因此,编译器似乎在运行时计算结果的不同结果。但是,我不知道这是否是预期的行为。
编辑:也许是Pascal Cuoq所描述的效果:在编译期间,C#编译器使用double
来计算表达式。这导致79,xxx被截断为79(这里的double包含足够的精度以引起问题)
然而,使用float,我们实际上并没有遇到问题,因为浮点数"错误"不在浮动范围内。
在运行期间,这个也打印79:
double d1 = (64 / 0.8f);
byte b3 = (byte) d1;
Console.WriteLine(b3); //79
EDIT2:根据Pascal Cuoq的要求,我运行了以下代码:
int sixtyfour = Int32.Parse("64");
byte b4 = (byte)(sixtyfour / 0.8f);
Console.WriteLine(b4); //79
结果是79.所以上面说的编译器和运行时计算不同的结果是不正确的。
EDIT3 :将之前的代码更改为(再次归功于Pascal Cuoq)时,结果为80:
byte b5 = (byte)(float)(sixtyfour / 0.8f);
Console.WriteLine(b5); //80
但请注意,写作时并非如此(结果为79):
byte b6 = (byte)(float)(64 / 0.8f);
Console.WriteLine(b6); //79
所以这里似乎正在发生的事情:(byte)(64 / 0.8f)
未被评估为float
,而是评估为double
(在将其转换为byte
之前)。这会导致舍入误差(使用float
进行计算时不会发生)。在转换为double之前浮动的显式转换(由ReSharper,BTW标记为冗余)"解决"这个问题。但是,当计算在编译期间完成时(仅在使用常量时可能),显式转换为float
似乎被忽略/优化了。
TLDR:浮点计算比最初看起来更复杂。
答案 1 :(得分:3)
C#语言规范允许to compute intermediate floating-point results at a precision greater than that of the type。这很有可能发生在这里。
虽然计算到更高精度的64 / 0.8
略低于80(因为0.8无法在二进制浮点中精确表示),并且在截断为整数类型时转换为79
,如果结果除法转换为float
,它舍入为80.0f
。
(从浮点到浮点的转换是最接近技术上的,它们是根据FPU的舍入模式完成的,但C#不允许将FPU的舍入模式从“更改为”到最近“default。从浮点到整数类型的转换截断。”
答案 2 :(得分:0)
即使C#遵循Java(恕我直言,不幸)领导要求显式转换,任何时候将指定为double
的内容存储到float
,C#编译器生成的代码允许.NET运行时以double
执行计算,并在许多上下文中使用这些double
值,根据语言规则,表达式的类型应为float
。
幸运的是,C#编译器确实提供了至少一种方法来确保应该舍入到最接近的可表示float
的事物实际上是:将它们显式地转换为float
。
如果您将表达式编写为(byte)(float)(sixtyFour / 0.8f)
,则应该在截断小数部分之前强制将结果四舍五入到最接近的可表示float
值。虽然对float
的强制转换可能看似多余(表达式的编译时类型已经是float
),但强制转换将变为“应该是float
的东西但是真的{ {1}}“进入实际为double
的内容。
从历史上看,某些语言会指定所有浮点运算都在类型float
上执行; double
存在不是为了加速计算,而是为了减少存储需求。通常不需要将常量指定为类型float
,因为除以0.800000000000000044(float
值0.8)并不慢于除以0.800000011920929(值double
)。 C#有点令人讨厌,因为“精度损失”而不允许0.8f
,而是偏向不太精确的float1 = float2 / 0.8;
,甚至不介意可能错误的float1 = float2 / 0.8f;
。虽然操作是在double1 = float1 / 0.8f;
值之间执行的,但这并不意味着结果实际上是float
- 它只是意味着编译器将允许它静默舍入为float
在某些情况下,但不会在其他情况下强迫它。