为什么会打印出最后一个数字(1)?

41

代码:

<?php
$start = 0;
$stop  = 1;
$step = ($stop - $start)/10;
$i = $start + $step;
while ($i < $stop) {
    echo($i . "<br/>");
    $i += $step;
}
?>

输出结果:
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
1 <-- notice the 1 printed when it shouldn't

创建了一个fiddle

再来一个: 如果你设置$start = 1$stop = 2 它就可以正常工作。

使用: php 5.3.27

为什么会打印出1


1
令人惊讶的是,当你将区间分成20份时:$step = ($stop - $start)/20; 这也是可以的。 - user4035
3
可能是浮点数舍入误差。 - Blazemonger
1
与https://dev59.com/rW865IYBdhLWcg3wnf5t相关的内容。 - j08691
2
你的问题意味着你期望在9次迭代后$i恰好等于1。然而,你不能直接比较浮点数是否相等 - Jon
可能是浮点数计算有问题吗?的重复。 - miken32
3个回答

53
因为浮点数并不仅存在计算上的缺陷,有时其表示方法也存在缺陷,这就是本例的情况。
实际上你得到的并不是0.1、0.2等数值,这很容易验证:
$start = 0;
$stop  = 1;
$step = ($stop - $start)/10;
$i = $start + $step;
while ($i < $stop) {
   print(number_format($i, 32) . "<br />");
   $i += $step;
}

这里唯一的区别就是将 echo 替换为 number_format 调用。但结果却截然不同:
0.10000000000000000555111512312578
0.20000000000000001110223024625157
0.30000000000000004440892098500626
0.40000000000000002220446049250313
0.50000000000000000000000000000000
0.59999999999999997779553950749687
0.69999999999999995559107901499374
0.79999999999999993338661852249061
0.89999999999999991118215802998748
0.99999999999999988897769753748435

看到了吗?实际上只有一次是0.5 - 因为这个数字可以存储在浮点容器中。所有其他的都只是近似值。
如何解决这个问题?一个激进的方法是在类似的情况下使用整数而不是浮点数。很容易注意到,如果你这样做...
$start = 0;
$stop  = 10;
$step = (int)(($stop - $start) / 10);
$i = $start + $step;
while ($i < $stop) {
   print(number_format($i, 32) . "<br />");
   $i += $step;
}

"...它应该可以正常工作:

或者,您可以使用number_format将浮点数转换为某个字符串,然后将此字符串与预格式化的浮点数进行比较。像这样:"

$start = 0;
$stop  = 1;
$step = ($stop - $start) / 10;
$i = $start + $step;
while (number_format($i, 1) !== number_format($stop, 1)) {
   print(number_format($i, 32) . "\n");
   $i += $step;
}

4
一个解决办法是保持 $step 为整数:$start=0; $stop=10; $step=1; 并在循环内部进行除法运算。 - Blazemonger
最好的防护方法是通过将浮点运算转换为整数,通过将停止乘以10并将i增加1而不是0.1。@RichBradshaw - user4035
1
为了避免这种情况发生,在你有意使用浮点数的编程语言中,传统的解决方法是采用误差范围进行比较。一种更粗略的方式是使所有数字变大(乘以10或其他),这可以减少短暂的不精确性,但可能会导致舍入误差。 - ssube
@kcsoft 所有带有二进制浮点数的编程语言都会遇到这个问题。十进制浮点数是免疫的,包括IEEE 754-2008标准中的浮点数。 - Kevin A. Naudé
1
@KevinA.Naudé,我觉得你的回答非常有趣,提供了很好的关于为什么它能够这样工作的信息。你应该把它保留下来 :) - Jordan
显示剩余3条评论

13
问题在于变量 $i 中的数字不是1(打印时)。它实际上只是略小于1。因此,在测试中 ($i < $stop)为true时,数字被转换为十进制数(导致四舍五入到1)并显示出来。
那么为什么 $i 不是恰好为1呢?因为通过计算10 * 0.1得到,而0.1在二进制中无法完美表示。只有可以表示为有限个2的幂的和的数字才能完美表示。
那么为什么 $stop 恰好为1呢?因为它不是浮点格式。换句话说,从一开始就是确切的——它没有在使用浮点10 * 0.1系统内计算。
数学上,我们可以写成如下形式: enter image description here 64位二进制浮点数只能容纳逼近0.1的前27个非零项。其他26位尾数保持为零以表示零项。0.1不能被物理表示的原因是所需的项序列是无限的。另一方面,像1这样的数字只需要有限的项就可以表示,并且是可表示的。我们希望对所有数字都是如此。这就是为什么十进制浮点是如此重要的创新(尚未广泛使用)。它可以表示我们能写下的任何数字,并且完全如此。当然,可用数字的数量仍然是有限的。
回到给定的问题,由于0.1是循环变量的增量并且实际上无法表示,因此在循环中永远不会精确达到值1.0。

2

如果您的步骤始终是10的倍数,可以通过以下方法快速完成:

<?php
$start = 0;
$stop  = 1;
$step = ($stop - $start)/10;
$i = $start + $step;
while (round($i, 1) < $stop) { //Added round() to the while statement
    echo($i . "<br/>");
    $i += $step;
}
?>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接