PHP尽管已经成为“世界最好的编程语言”,但是在开发的过程中,它的有些特性仍旧给我们造成了一些不明所以的困扰。
下面,苦李历数本人在开发的过程中踩过的一些坑,希望各位看客能从中汲取教训,绕坑而行。

1、PHP浮点计算的误差

先上码为敬:

<?php
$a = 0.0008;
$b = 0.0007;
$c = $a - $b;
$d = 0.4568;
$e = 0.4567;
$f = $d -$e;
$c == $f ? print "true" : print "false";
echo ",".$c." ,".$f;

按照正常的逻辑,应该输出true,即$c和$f相等均为0.0001,但是实际结果却输出false。当然,这也印证了一句话,按正常逻辑行得通的话,那你写的肯定就不是PHP了。

下面,我们来分析一下造成这种非正常逻辑的原因。执行以上代码,$c与$f分别输出 0.0001和9.9999999999989E-5。为什么相差相同的浮点数执行的结果却不一样呢?

这就要参详一下php关于浮点数处理的机制了,php的浮点数采用IEEE 754标准,浮点数以64位的长度(双精度)为例, 会采用1位符号位(E),11指数位(Q),52位尾数(M)表示(一共64位)。其中,

  • 符号位:最高位表示数据的正负,0表示正数,1表示负数
  • 指数位:表示数据以2为底的幂,指数采用偏移码表示
  • 尾数:表示数据小数点后的有效数字

IEEE 754的具体标准细节可以自行补脑,这里苦李简单介绍下,将一个十进制的数转换成二进制存储的方法:

  • V=(-1)^s(1+M)2^(E-127)(单精度)
  • V=(-1)^s(1+M)2^(E-1023)(双精度)

这样存储高精度数的时候,必然就会在尾数上的精度有所缺失,那么进行运算的时候,就会产生需要末尾精度的误差,但是精度缺失也要看浮点数能否用二进制存储来表示,所以针对有些浮点数入0.0008,不会产生精度误差,而0.4567就会产出这样的误差。避免产生精度误差的方式有很多种,例如使用PHP的高精度数BC函数库,或者在计算结果之后加入round函数确定精度。

2、长数字字符串比较和“==”

示例代码:

$account1 = "110061203018010020273";
$account2 = "110061203018010020274";
if($account1 != $account2) {
    echo "true";
} else {
    echo "false";
}

当变量的赋值为长数字字符串,仅仅是末尾不相同,php会判断为两个账号为相同的账号。为什么我定义了account为字符串类型,而且两个末尾不同的字符串会会被php认定是相同呢?

问题就出在了“==”上,由于php是弱类型语言,而“==”是忽略变量类型的比较,php检测到字符串为纯数字,会自动转换成整形,而php中整数最大值为9223372036854775807,超出这个范围的整数就会砍掉末尾的数(php就是这么的无奈)。

问题的解决方法也很简单,将"=="改为“===”,“==="不仅比较变量的值,而且需要比较的变量类型也相同才会返回true,在php的代码尽量用“===”来代替不靠谱的“==”。

还有一个处理办法,就是阻止在“==”比较的过程中将变量强制转化成整数类型,具体处理就是在纯数字字符串前加一个字母前缀,比较的时候php会判断是两个字符串比较,就不会强制进行整形的转换了,当然还是“===”最为简单可靠。

另外“==”还有一个有趣的现象能证明PHP究竟是怎样一个不靠谱的编程语言:

"string" == 0 ? echo "true" : echo "false";
"string" == true ? echo "true" : echo "false";
0 == true ? echo "true" : echo "false";

前两个输出为true,后面一个输出为false,就是说php认为“string”既和0相等,也和true相等,但是0却与true不相等,多么混乱的逻辑关系!

但是清楚“==”的处理方式就不难理解了,“string”==0比较的时候,由于右边为0是个整数,那么“string”会强制转换为整数,但是“string”并没有数字,就会转换为0,那么0和0比较自然相等。当"string"==true比较的时候,比较右边是个布尔类型,而字符串只有在空串的时候转换成布尔类型才是false,自然“string”转换为布尔类型就为true,那么”string“和true也比较相等。

由此“==”不具备比较的传递性,而且使用的时候还需要对其处理方式有个深入的理解,因此还是推荐使用”===“进行变量比较。

3、不太靠谱的功能函数

(1)in_array

这个函数应该算是我们使用的比较多的,其作用是,用来判断一个元素是否存在某个数组中,但是比较的结果通常也会令我们有一种问候别人的冲动。

例如:

$arr = array("110061203018010020273","string");
in_array(0,$arr) ? echo "true" : echo "false";
in_array("110061203018010020274",$arr) ? echo "true" : echo "false";

结果会输出两个true,是不是对这样的现象感觉似曾相识呢?没错,in_array的比较方式和“==”如出一辙,会强制转换类型(此处应该有问候),对于存在这样的特殊比较的时候,苦李通常都是老老实实的foreach加“===”来判断。

(2)empty

empty这个函数是个很招人喜欢的函数,0、空串、false、空数组、null都可以用empty来判断,但是它却也是一个很难伺候的函数。

例如empty($a.$b),empty(trim($input))这些写法前者会报出一个parse error,syntax error, unexpected '.', expecting ')',后者直接报一个PHP Fatal error: Can't use function return value in write context。

在经历了种种不服气的测试之后,重新翻看empty的官方手册,发现empty还并不能称之为一个函数方法,而是一种语言结构,而它这种语言结构的参数只能是变量,任何运算式,函数结果,常量均不能作为其参数,同样属于语言结构还有echoprintissetunset 等等等等。

(3)foreach

foreach是对数组做循环处理的优雅方法,并且利&引用元素,很方便对数组的元素进行操作,但是也会有些困扰的问题。例如:

$balance = array(1000,2000,3000,4000);
$sum = 0;
foreach($balance as &$val){
    if($val >= 2000){
        $sum ++; 
        $val-= 2000;
    } 
}
$val = "test";
print_r($balance);

其输出结果为:

Array
(
    [0] => 1000
    [1] => 0
    [2] => 1000
    [3] => test
)

说明了在foreach中使用&引用后,当foreach结束后,$key和$val变量也都不会被自动释放掉,但是此时$val和count($balance)-1指向相同的内存地址。因此,此时修改$val的值也会改变了$balance[3]的值。

那么解决的办法也只能是尊重这个特性(也尼玛没有其他的办法了),使用完$val之后,使用unset($val)将$val显式的释放掉,或者在后续的代码不要使用相同名称的变量。

总结

虽然php在开发的过程中会踩这么多的坑,但是这丝毫不影响我们将其作为“最好的编程语言”,更是不妨碍我们把它作为最具效率的生产力工具,毕竟php也拥有很多优势,而且在鸟哥等php的开发作者的殚精竭虑下,php会朝着更好的方向发展,像hhvm,php7,而对于我们这些使用php的开发者,在理解了php的运行机制之后,也能很顺利的避开这些坑,同时提升我们的填坑水平。感谢主,少踩坑~

扫码关注李苦李公众号

李苦李公众号

标签: PHP