微信号:phpdaily

介绍:PHP在线专注于PHP编程语言学习,PHP开发经验分享,工作问题解决以及PHP在线技能测评等多功能为一体的服务系统,希望给工作学习中的PHPER带来些帮助。

十个 PHP 开发者最容易犯的错误

2018-04-09 12:08 Charlie_Jade

PHP 语言让 WEB 端程序设计变得简单,这也是它能流行起来的原因。但也是因为它的简单,PHP 也慢慢发展成一个相对复杂的语言,层出不穷的框架,各种语言特性和版本差异都时常让搞的我们头大,不得不浪费大量时间去调试。这篇文章列出了十个最容易出错的地方,值得我们去注意。

易犯错误 #1: 在 foreach 循环后留下数组的引用

还不清楚 PHP 中 foreach 遍历的工作原理?如果你在想遍历数组时操作数组中每个元素,在 foreach 循环中使用引用会十分方便,例如

 
           
  1.    $arr = array(1, 2, 3, 4);

  2.    foreach ($arr as &$value) {

  3.            $value = $value * 2;

  4.    }

  5.    // $arr 现在是 array(2, 4, 6, 8)

问题是,如果你不注意的话这会导致一些意想不到的负面作用。在上述例子,在代码执行完以后, $value仍保留在作用域内,并保留着对数组最后一个元素的引用。之后与 $value 相关的操作会无意中修改数组中最后一个元素的值。

你要记住 foreach 并不会产生一个块级作用域。因此,在上面例子中 $value 是一个全局引用变量。在 foreach 遍历中,每一次迭代都会形成一个对 $arr 下一个元素的引用。当遍历结束后, $value 会引用 $arr 的最后一个元素,并保留在作用域中

这种行为会导致一些不易发现的,令人困惑的bug,以下是一个例子

 
           
  1.    $array = [1, 2, 3];

  2.    echo implode(',', $array), "\n";


  3.    foreach ($array as &$value) {}    // 通过引用遍历

  4.    echo implode(',', $array), "\n";


  5.    foreach ($array as $value) {}     // 通过赋值遍历

 
           
  1. echo implode(',', $array), "\n";

以上代码会输出

 
           
  1.    1,2,3

  2.    1,2,3

  3.    1,2,2

你没有看错,最后一行的最后一个值是 2 ,而不是 3 ,为什么?

在完成第一个 foreach 遍历后, $array 并没有改变,但是像上述解释的那样, $value 留下了一个对 $array 最后一个元素的危险的引用(因为 foreach 通过引用获得 $value

这导致当运行到第二个 foreach ,这个"奇怪的东西"发生了。当 $value 通过赋值获得, foreach 按顺序复制每个 $array 的元素到 $value 时,第二个 foreach 里面的细节是这样的

  • 第一步:复制 $array[0] (也就是 1 )到 $value ( $value 其实是 $array最后一个元素的引用,即 $array[2]),所以 $array[2] 现在等于 1。所以 $array 现在包含 [1, 2, 1]

  • 第二步:复制 $array[1](也就是 2 )到 $value ( $array[2] 的引用),所以 $array[2] 现在等于 2。所以 $array 现在包含 [1, 2, 2]

  • 第三步:复制 $array[2](现在等于 2 ) 到 $value ( $array[2] 的引用),所以 $array[2] 现在等于 2 。所以 $array 现在包含 [1, 2, 2]

为了在 foreach 中方便的使用引用而免遭这种麻烦,请在 foreach 执行完毕后 unset() 掉这个保留着引用的变量。例如

 
           
  1.    $arr = array(1, 2, 3, 4);

  2.    foreach ($arr as &$value) {

  3.        $value = $value * 2;

  4.    }

  5.    unset($value);   // $value 不再引用 $arr[3]

常见错误 #2: 误解 isset() 的行为

尽管名字叫 isset,但是 isset() 不仅会在变量不存在的时候返回 false,在变量值为 null 的时候也会返回 false

这种行为比最初出现的问题更为棘手,同时也是一种常见的错误源。

看看下面的代码:

 
           
  1. $data = fetchRecordFromStorage($storage, $identifier);

  2. if (!isset($data['keyShouldBeSet']) {

  3.    // do something here if 'keyShouldBeSet' is not set

  4. }

开发者想必是想确认 keyShouldBeSet 是否存在于 $data 中。然而,正如上面说的,如果 $data['keyShouldBeSet'] 存在并且值为 null 的时候, isset($data['keyShouldBeSet']) 也会返回 false。所以上面的逻辑是不严谨的。

我们来看另外一个例子:

 
           
  1. if ($_POST['active']) {

  2.    $postData = extractSomething($_POST);

  3. }


  4. // ...


  5. if (!isset($postData)) {

  6.    echo 'post not active';

  7. }

上述代码,通常认为,假如 $_POST['active'] 返回 true,那么 postData 必将存在,因此 isset($postData) 也将返回 true。反之, isset($postData) 返回 false 的唯一可能是 $_POST['active'] 也返回 false

然而事实并非如此!

如我所言,如果 $postData 存在且被设置为 nullisset($postData) 也会返回 false 。 也就是说,即使 $_POST['active'] 返回 trueisset($postData) 也可能会返回 false 。 再一次说明上面的逻辑不严谨。

顺便一提,如果上面代码的意图真的是再次确认 $_POST['active'] 是否返回 true,依赖 isset() 来做,不管对于哪种场景来说都是一种糟糕的决定。更好的做法是再次检查 $_POST['active'],即:

 
           
  1. if ($_POST['active']) {

  2.    $postData = extractSomething($_POST);

  3. }


  4. // ...


  5. if ($_POST['active']) {

  6.    echo 'post not active';

  7. }

对于这种情况,虽然检查一个变量是否真的存在很重要(即:区分一个变量是未被设置还是被设置为 null);但是使用 array_key_exists() 这个函数却是个更健壮的解决途径。

比如,我们可以像下面这样重写上面第一个例子:

 
           
  1. $data = fetchRecordFromStorage($storage, $identifier);

  2. if (! array_key_exists('keyShouldBeSet', $data)) {

  3.    // do this if 'keyShouldBeSet' isn't set

  4. }

另外,通过结合 array_key_exists() 和  get_defined_vars(), 我们能更加可靠的判断一个变量在当前作用域中是否存在:

 
           
  1. if (array_key_exists('varShouldBeSet', get_defined_vars())) {

  2.    // variable $varShouldBeSet exists in current scope

  3. }

常见错误 #3:关于通过引用返回与通过值返回的困惑

考虑下面的代码片段:

 
           
  1. class Config

  2. {

  3.    private $values = [];


  4.    public function getValues() {

  5.        return $this->values;

  6.    }

  7. }


  8. $config = new Config();


  9. $config->getValues()['test'] = 'test';

  10. echo $config->getValues()['test'];

如果你运行上面的代码,将得到下面的输出:

 
           
  1. PHP Notice:  Undefined index: test in /path/to/my/script.php on line 21

出了什么问题?

上面代码的问题在于没有搞清楚通过引用与通过值返回数组的区别。除非你明确告诉 PHP 通过引用返回一个数组(例如,使用 &),否则 PHP 默认将会「通过值」返回这个数组。这意味着这个数组的一份拷贝将会被返回,因此被调函数与调用者所访问的数组并不是同样的数组实例。

所以上面对 getValues() 的调用将会返回 $values 数组的一份拷贝,而不是对它的引用。考虑到这一点,让我们重新回顾一下以上例子中的两个关键行:

 
           
  1. // getValues() 返回了一个 $values 数组的拷贝

  2. // 所以`test`元素被添加到了这个拷贝中,而不是 $values 数组本身。

  3. $config->getValues()['test'] = 'test';



  4. // getValues() 又返回了另一份 $values 数组的拷贝

  5. // 且这份拷贝中并不包含一个`test`元素(这就是为什么我们会得到 「未定义索引」 消息)。

  6. echo $config->getValues()['test'];

一个可能的修改方法是存储第一次通过 getValues() 返回的 $values 数组拷贝,然后后续操作都在那份拷贝上进行;例如:

 
           
  1. $vals = $config->getValues();

  2. $vals['test'] = 'test';

  3. echo $vals['test'];

这段代码将会正常工作(例如,它将会输出 test而不会产生任何「未定义索引」消息),但是这个方法可能并不能满足你的需求。特别是上面的代码并不会修改原始的 $values数组。如果你想要修改原始的数组(例如添加一个 test元素),就需要修改 getValues()函数,让它返回一个 $values数组自身的引用。通过在函数名前面添加一个 &来说明这个函数将返回一个引用;例如:

 
           
  1. class Config

  2. {

  3.    private $values = [];


  4.    // 返回一个 $values 数组的引用

  5.    public function &getValues() {

  6.        return $this->values;

  7.    }

  8. }


  9. $config = new Config();


  10. $config->getValues()['test'] = 'test';

  11. echo $config->getValues()['test'];

这会输出期待的 test

但是现在让事情更困惑一些,请考虑下面的代码片段:

 
           
  1. class Config

  2. {

  3.    private $values;


  4.    // 使用数组对象而不是数组

  5.    public function __construct() {

  6.        $this->values = new ArrayObject();

  7.    }


  8.    public function getValues() {

  9.        return $this->values;

  10.    }

  11. }


  12. $config = new Config();


  13. $config->getValues()['test'] = 'test';