float 拥有一些未显示的“隐藏”精度。试试看invoice.total.ToString("R"),你可能会发现它不完全是36000。
或者,这可能是由于您的运行时为中间结果invoice.total * 0.08f 选择了“更广泛”的存储位置,例如 64 位或 80 位 CPU 寄存器或类似位置。
编辑:您可以通过更改来消除运行时选择过宽的存储位置所产生的影响
(int)(invoice.total * 0.08f)
进入
(int)(float)(invoice.total * 0.08f)
从float 到float(原文如此!)的额外演员表看起来像是无操作,但它确实会强制运行时舍入并丢弃不需要的精度。这没有很好的记录。 [将提供参考。]您可能想阅读的相关主题:Are floating-point numbers consistent in C#? Can they be?
你的例子实际上是典型的,所以我决定更详细一点。这些东西在Differences Among IEEE 754 Implementations 部分中有很好的描述,该部分是作为 David Goldberg 的What Every Computer Scientist Should Know About Floating-Point Arithmetic 的附录(由匿名作者编写)。所以假设我们有这样的代码:
static int SO_24548957_I()
{
float t = 36000f; // exactly representable
float r = 0.08f; // this is not representable, rounded
float temporary = t * r;
int v = (int)temporary;
return v; // always(?) 2880
}
一切似乎都很好,但我们决定将临时变量重构掉,所以我们写道:
static int SO_24548957_II()
{
float t = 36000f; // exactly representable
float r = 0.08f; // this is not representable, rounded
int v = (int)(t * r);
return v; // could be 2880 or 2879 depending on strange things
}
然后砰!我们程序的行为发生了变化。如果您为平台x86(或Any CPU 选择Prefer 32-bit)编译,您可以在大多数系统上看到更改(至少在我的系统上!)。优化与否(发布或调试模式)在理论上可能是相关的,硬件架构当然也很重要。
对于许多人来说,2880 和 2879 在符合 IEEE-754 的系统上都可以是正确答案,这让很多人感到非常惊讶,但请阅读我提供的链接。
为了详细说明“不可表示”的含义,让我们看看 C# 编译器在遇到符号 0.08f 时必须做什么。由于float(32 位二进制浮点)的工作方式,我们将不得不在以下两者之间进行选择:
10737418 / 2**27 == 0.079 999 998 2...
和
10737419 / 2**27 == 0.080 000 005 6...
其中** 表示求幂(即“幂”)。由于第一个更接近所需的数学值,我们必须选择那个。所以实际值比期望值小一点。现在,当我们进行乘法并希望再次存储在Single 中时,作为乘法算法的一部分,我们还必须再次舍入以产生最接近精确“数学”的乘积表示" (实际)因子 36000 和 0.0799999982... 的乘积。在这种情况下,您 很幸运,最接近的 Single 实际上是 2880,因此在我们的例子中,乘法过程涉及到该值的四舍五入。
因此上面的第一个代码示例给出了2880。
但是,在上面的第二个代码示例中,乘法可能会在一些处理许多位(通常为 64 或 80)的 CPU 硬件中完成(在运行时的选择中,我们无法真正帮助)。在这种情况下,任何两个 32 位浮点数的乘积,就像我们的一样,可以在不需要对最终结果进行四舍五入的情况下计算出来,因为 64 位或 80 位足以容纳两个 32 位浮点数的完整乘积。很明显这个产品小于2880,因为
0.0799999982... 小于 0.08。
因此上面的第二个方法示例可以返回2879。
为了比较,这段代码:
static int SO_24548957_III()
{
float t = 36000f; // exactly representable
float r = 0.08f; // this is not representable, rounded
double temporary = t * (double)r;
int v = (int)temporary;
return v; // always(?) 2879
}
总是给出2879,因为我们明确告诉编译器将Single 转换为Double,这意味着添加一堆二进制零,所以我们肯定会得到2879 的情况。
经验教训: (1) 对于二进制浮点,将子表达式分解为临时变量可能会改变结果。 (2) 对于二进制浮点,x86 与 x64 之类的 C# 编译器设置可能会改变结果。
当然,正如大家到处说的那样,不要将float 或double 用于货币应用程序;在那里使用decimal。