为什么分割结果因铸件类型而异?(跟进)
本文关键字:跟进 类型 结果 分割 为什么 | 更新日期: 2023-09-27 18:20:49
这是这个问题的后续问题:为什么除法结果因铸造类型而异?
快速摘要:
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。那么,为什么铸造结果会有所不同呢?
我在另一个线程中的答案并不完全正确:实际上,在运行时计算时,(byte)(64 / 0.8f)
是 80。
当将包含 64 / 0.8f
结果的float
转换为运行时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(因为双精度包含足够的精度来导致问题,此处(。
但是,使用 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
完成计算时不会发生舍入误差(。在强制转换为双精度之前显式浮动(被 ReSharper 标记为冗余,顺便说一句("解决了"这个问题。但是,当计算在编译时完成时(仅在使用常量时可能(,显式强制转换为float
似乎被忽略/优化掉。
TLDR:浮点计算比最初看起来要复杂得多。
C# 语言规范允许以大于类型精度的精度计算中间浮点结果。这很可能是这里正在发生的事情。
虽然计算到更高精度64 / 0.8
略低于 80(因为 0.8 不能在二进制浮点中精确表示(,并且在截断为整数类型时转换为 79
,但如果将除法的结果转换为 float
,则四舍五入为 80.0f
。
(从浮点到浮点的转换是最近的 - 从技术上讲,它们是根据 FPU 的舍入模式完成的,但 C# 不允许将 FPU 的舍入模式从其"到最近"默认值更改。从浮点类型到整数类型的转换将被截断。
尽管 C# 遵循 Java(恕我直言不幸的是(的领导,每当将指定为 double
的东西存储到float
时都需要显式强制转换,但 C# 编译器生成的代码允许 .NET 运行时double
执行计算,并在表达式类型应该的许多上下文中使用这些double
值, 根据语言规则,float
.
幸运的是,C# 编译器确实提供了至少一种方法来确保应该舍入到最接近的可表示float
的内容实际上是:将它们显式转换为 float
。
如果将表达式编写为 (byte)(float)(sixtyFour / 0.8f)
,则应强制结果在截断小数部分之前四舍五入到最接近的可表示float
值。 尽管强制转换为float
可能看起来是多余的(表达式的编译时类型已经是float
(,但强制转换会将"应该是float
但实际上double
的东西"变成实际上是float
的东西。
从历史上看,某些语言会指定所有浮点运算都在类型 double
上执行; float
的存在不是为了加快计算速度,而是为了减少存储需求。 通常不需要指定常量作为类型float
,因为除以 0.8000000000000000044(double
值 0.8(并不比除以 0.800000011920929(值0.8f
(慢。 C#有点烦人地不允许float1 = float2 / 0.8;
因为"精度损失",而是偏爱不太精确的float1 = float2 / 0.8f;
,甚至不介意可能错误的double1 = float1 / 0.8f;
。 在float
值之间执行操作的事实并不意味着结果实际上是一个float
- 它只是意味着编译器将允许它在某些上下文中静默地舍入为float
,但在其他上下文中不会强制它。