Maison > développement back-end > tutoriel php > 10 erreurs courantes commises par les programmeurs PHP (partager)

10 erreurs courantes commises par les programmeurs PHP (partager)

青灯夜游
Libérer: 2023-04-10 07:06:02
avant
3054 Les gens l'ont consulté

Cet article vous présentera les 10 erreurs les plus courantes commises par les développeurs PHP. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer. J'espère qu'il sera utile à tout le monde.

10 erreurs courantes commises par les programmeurs PHP (partager)

Le langage PHP facilite la programmation côté WEB, c'est pourquoi il devient populaire. Mais aussi en raison de sa simplicité, PHP s'est progressivement développé pour devenir un langage relativement complexe, avec des frameworks sans fin, diverses fonctionnalités de langage et des différences de versions qui nous rendent souvent confus et nous font perdre beaucoup de temps au débogage. Cet article répertorie les dix erreurs les plus courantes qui méritent notre attention.

Erreur n°1 : laisser une référence de tableau après une boucle foreach

Vous ne savez pas comment fonctionne le parcours foreach en PHP ? Si vous souhaitez parcourir le tableau et manipuler chaque élément du tableau, il peut être très pratique d'utiliser des références dans une boucle foreach, telle que

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
        $value = $value * 2;
}
// $arr 现在是 array(2, 4, 6, 8)
Copier après la connexion

Le problème est que cela peut conduire à des événements inattendus effets secondaires si vous ne faites pas attention. Dans l'exemple ci-dessus, une fois le code exécuté, $value reste dans la portée et conserve une référence au dernier élément du tableau. Les opérations ultérieures liées à $value modifient par inadvertance la valeur du dernier élément du tableau.

Vous devez vous rappeler que foreach ne crée pas de portée de bloc. Par conséquent, dans l'exemple ci-dessus, $value est une variable de référence globale. Dans un parcours foreach, chaque itération forme une référence à l'élément $arr suivant. Lorsque la traversée se termine, $value fera référence au dernier élément de $arr et restera dans la portée

Ce comportement peut conduire à des bugs difficiles à trouver et déroutants. Voici un exemple

.
$array = [1, 2, 3];
echo implode(',', $array), "\n";

foreach ($array as &$value) {}    // 通过引用遍历
echo implode(',', $array), "\n";

foreach ($array as $value) {}     // 通过赋值遍历
echo implode(',', $array), "\n";
Copier après la connexion

Le code ci-dessus affichera

1,2,3
1,2,3
1,2,2
Copier après la connexion

Vous avez bien lu, la dernière valeur de la dernière ligne est 2, pas 3, pourquoi ?

Après avoir terminé le premier foreach parcours, $array n'est pas modifié, mais comme expliqué ci-dessus, $value laisse une référence dangereuse au dernier élément de $array (Parce que foreach obtient $value par référence)

provoque ce "truc étrange" lors de l'exécution vers la seconde foreach. Lorsque $value est obtenu par affectation, foreach copie les éléments de chaque $array vers $value dans l'ordre, les détails à l'intérieur du deuxième foreach sont les suivants

  • la première étape : Copiez $array[0] (c'est-à-dire 1) dans $value ($value est en fait une référence au dernier élément de $array, qui est $array[2]), donc $array[2] est maintenant égal à 1. Donc $array contient maintenant [1, 2, 1]
  • Étape 2 : Copiez $array[1] (qui vaut 2) dans $value (une référence de $array[2]), donc $array[2] est maintenant égal à 2. Donc $array contient maintenant [1, 2, 2]
  • Étape 3 : Copiez $array[2] (maintenant égal à 2) dans $value (une référence de $array[2]), donc $array[2] est maintenant égal à 2. Donc $array contient maintenant [1, 2, 2]

Afin d'utiliser facilement les références dans foreach pour éviter ce problème, veuillez foreach après l'exécution de unset() Supprimez le. variable qui contient la référence. Par exemple,

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
}
unset($value);   // $value 不再引用 $arr[3]
Copier après la connexion

Erreur courante n°2 : mauvaise compréhension du comportement de isset()

Bien que le nom soit isset, isset() renverra non seulement false lorsque la variable n'existe pas, mais renverra également null lorsque la valeur de la variable est false.

Ce comportement est plus problématique qu'il n'y paraît initialement et est une source courante d'erreurs.

Regardez le code suivant :

$data = fetchRecordFromStorage($storage, $identifier);
if (!isset($data['keyShouldBeSet']) {
    // do something here if 'keyShouldBeSet' is not set
}
Copier après la connexion

Le développeur doit vouloir confirmer si keyShouldBeSet existe dans $data. Cependant, comme mentionné ci-dessus, si $data['keyShouldBeSet'] existe et que la valeur est null, isset($data['keyShouldBeSet']) renverra également false. La logique ci-dessus n’est donc pas rigoureuse.

Regardons un autre exemple :

if ($_POST['active']) {
    $postData = extractSomething($_POST);
}

// ...

if (!isset($postData)) {
    echo 'post not active';
}
Copier après la connexion

Dans le code ci-dessus, on pense généralement que si $_POST['active'] renvoie true, alors postData doit exister, donc isset($postData) va revenez également true. A l’inverse, la seule possibilité pour isset($postData) de revenir false est si $_POST['active'] revient également false.

Cependant, ce n’est pas le cas !

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

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

if ($_POST['active']) {
    $postData = extractSomething($_POST);
}

// ...

if ($_POST['active']) {
    echo 'post not active';
}
Copier après la connexion

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

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

$data = fetchRecordFromStorage($storage, $identifier);
if (! array_key_exists('keyShouldBeSet', $data)) {
    // do this if 'keyShouldBeSet' isn't set
}
Copier après la connexion

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

if (array_key_exists('varShouldBeSet', get_defined_vars())) {
    // variable $varShouldBeSet exists in current scope
}
Copier après la connexion

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

考虑下面的代码片段:

class Config
{
    private $values = [];

    public function getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
Copier après la connexion

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

PHP Notice:  Undefined index: test in /path/to/my/script.php on line 21
Copier après la connexion

出了什么问题?

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

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

// getValues() 返回了一个 $values 数组的拷贝
// 所以`test`元素被添加到了这个拷贝中,而不是 $values 数组本身。
$config->getValues()['test'] = 'test';


// getValues() 又返回了另一份 $values 数组的拷贝
// 且这份拷贝中并不包含一个`test`元素(这就是为什么我们会得到 「未定义索引」 消息)。
echo $config->getValues()['test'];
Copier après la connexion

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

$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];
Copier après la connexion

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

class Config
{
    private $values = [];

    // 返回一个 $values 数组的引用
    public function &getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
Copier après la connexion

这会输出期待的test

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

class Config
{
    private $values;

    // 使用数组对象而不是数组
    public function __construct() {
        $this->values = new ArrayObject();
    }

    public function getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
Copier après la connexion

如果你认为这段代码会导致与之前的数组例子一样的「未定义索引」错误,那就错了。实际上,这段代码将会正常运行。原因是,与数组不同,PHP 永远会将对象按引用传递。(ArrayObject 是一个 SPL 对象,它完全模仿数组的用法,但是却是以对象来工作。)

像以上例子说明的,你应该以引用还是拷贝来处理通常不是很明显就能看出来。因此,理解这些默认的行为(例如,变量和数组以值传递;对象以引用传递)并且仔细查看你将要调用的函数 API 文档,看看它是返回一个值,数组的拷贝,数组的引用或是对象的引用是必要的。

尽管如此,我们要认识到应该尽量避免返回一个数组或 ArrayObject,因为这会让调用者能够修改实例对象的私有数据。这就破坏了对象的封装性。所以最好的方式是使用传统的「getters」和「setters」,例如:

class Config
{
    private $values = [];

    public function setValue($key, $value) {
        $this->values[$key] = $value;
    }

    public function getValue($key) {
        return $this->values[$key];
    }
}

$config = new Config();

$config->setValue('testKey', 'testValue');
echo $config->getValue('testKey');    // 输出『testValue』
Copier après la connexion

这个方法让调用者可以在不对私有的$values数组本身进行公开访问的情况下设置或者获取数组中的任意值。

常见的错误 #4:在循环中执行查询

如果像这样的话,一定不难见到你的 PHP 无法正常工作。

$models = [];

foreach ($inputValues as $inputValue) {
    $models[] = $valueRepository->findByValue($inputValue);
}
Copier après la connexion

这里也许没有真正的错误, 但是如果你跟随着代码的逻辑走下去, 你也许会发现这个看似无害的调用$valueRepository->findByValue() 最终执行了这样一种查询,例如:

$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
Copier après la connexion

结果每轮循环都会产生一次对数据库的查询。 因此,假如你为这个循环提供了一个包含 1000 个值的数组,它会对资源产生 1000 单独的请求!如果这样的脚本在多个线程中被调用,他会有导致系统崩溃的潜在危险。

因此,至关重要的是,当你的代码要进行查询时,应该尽可能的收集需要用到的值,然后在一个查询中获取所有结果。

一个我们平时常常能见到查询效率低下的地方 (例如:在循环中)是使用一个数组中的值 (比如说很多的 ID )向表发起请求。检索每一个 ID 的所有的数据,代码将会迭代这个数组,每个 ID 进行一次SQL查询请求,它看起来常常是这样:

$data = [];
foreach ($ids as $id) {
    $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id);
    $data[] = $result->fetch_row();
}
Copier après la connexion

但是 只用一条 SQL 查询语句就可以更高效的完成相同的工作,比如像下面这样:

$data = [];
if (count($ids)) {
    $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids));
    while ($row = $result->fetch_row()) {
        $data[] = $row;
    }
}
Copier après la connexion

因此在你的代码直接或间接进行查询请求时,一定要认出这种查询。尽可能的通过一次查询得到想要的结果。然而,依然要小心谨慎,不然就可能会出现下面我们要讲的另一个易犯的错误...

常见问题 #5: 内存使用欺骗与低效

一次取多条记录肯定是比一条条的取高效,但是当我们使用 PHP 的 mysql 扩展的时候,这也可能成为一个导致 libmysqlclient 出现『内存不足』(out of memory)的条件。

我们在一个测试盒里演示一下,该测试盒的环境是:有限的内存(512MB RAM),MySQL,和 php-cli

我们将像下面这样引导一个数据表:

// 连接 mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');

// 创建 400 个字段
$query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT';
for ($col = 0; $col < 400; $col++) {
    $query .= ", `col$col` CHAR(10) NOT NULL";
}
$query .= ');';
$connection->query($query);

// 写入 2 百万行数据
for ($row = 0; $row < 2000000; $row++) {
    $query = "INSERT INTO `test` VALUES ($row";
    for ($col = 0; $col < 400; $col++) {
        $query .= ', ' . mt_rand(1000000000, 9999999999);
    }
    $query .= ')';
    $connection->query($query);
}
Copier après la connexion

OK,现在让我们一起来看一下内存使用情况:

// 连接 mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');
echo "Before: " . memory_get_peak_usage() . "\n";

$res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1');
echo "Limit 1: " . memory_get_peak_usage() . "\n";

$res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000');
echo "Limit 10000: " . memory_get_peak_usage() . "\n";
Copier après la connexion

输出结果是:

Before: 224704
Limit 1: 224704
Limit 10000: 224704
Copier après la connexion

Cool。 看来就内存使用而言,内部安全地管理了这个查询的内存。

为了更加明确这一点,我们把限制提高一倍,使其达到 100,000。 额~如果真这么干了,我们将会得到如下结果:

PHP Warning:  mysqli::query(): (HY000/2013):
              Lost connection to MySQL server during query in /root/test.php on line 11
Copier après la connexion

究竟发生了啥?

这就涉及到 PHP 的 mysql 模块的工作方式的问题了。它其实只是个 libmysqlclient 的代理,专门负责干脏活累活。每查出一部分数据后,它就立即把数据放入内存中。由于这块内存还没被 PHP 管理,所以,当我们在查询里增加限制的数量的时候, memory_get_peak_usage() 不会显示任何增加的资源使用情况 。我们被『内存管理没问题』这种自满的思想所欺骗了,所以才会导致上面的演示出现那种问题。 老实说,我们的内存管理确实是有缺陷的,并且我们也会遇到如上所示的问题。

如果使用 mysqlnd 模块的话,你至少可以避免上面那种欺骗(尽管它自身并不会提升你的内存利用率)。 mysqlnd 被编译成原生的 PHP 扩展,并且确实 使用 PHP 的内存管理器。

因此,如果使用 mysqlnd 而不是 mysql,我们将会得到更真实的内存利用率的信息:

Before: 232048
Limit 1: 324952
Limit 10000: 32572912
Copier après la connexion

顺便一提,这比刚才更糟糕。根据 PHP 的文档所说,mysql 使用 mysqlnd 两倍的内存来存储数据, 所以,原来使用 mysql 那个脚本真正使用的内存比这里显示的更多(大约是两倍)。

为了避免出现这种问题,考虑限制一下你查询的数量,使用一个较小的数字来循环,像这样:

$totalNumberToFetch = 10000;
$portionSize = 100;

for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) {
    $limitFrom = $portionSize * $i;
    $res = $connection->query(
                         "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize");
}
Copier après la connexion

当我们把这个常见错误和上面的 常见错误 #4 结合起来考虑的时候, 就会意识到我们的代码理想需要在两者间实现一个平衡。是让查询粒度化和重复化,还是让单个查询巨大化。生活亦是如此,平衡不可或缺;哪一个极端都不好,都可能会导致 PHP 无法正常运行。

常见错误 #6: 忽略 Unicode/UTF-8 的问题

从某种意义上说,这实际上是PHP本身的一个问题,而不是你在调试 PHP 时遇到的问题,但是它从未得到妥善的解决。 PHP 6 的核心就是要做到支持 Unicode。但是随着 PHP 6 在 2010 年的暂停而搁置了。

这并不意味着开发者能够避免 正确处理 UTF-8 并避免做出所有字符串必须是『古老的 ASCII』的假设。 没有正确处理非 ASCII 字符串的代码会因为引入粗糙的 海森堡bug(heisenbugs) 而变得臭名昭著。当一个名字包含 『Schrödinger』的人注册到你的系统时,即使简单的 strlen($_POST['name']) 调用也会出现问题。

下面是一些可以避免出现这种问题的清单:

  • 如果你对 UTF-8 还不了解,那么你至少应该了解下基础的东西。 这儿 有个很好的引子。
  • 确保使用 mb_* 函数代替老旧的字符串处理函数(需要先保证你的 PHP 构建版本开启了『多字节』(multibyte)扩展)。
  • 确保你的数据库和表设置了 Unicode 编码(许多 MySQL 的构建版本仍然默认使用 latin1 )。
  • 记住 json_encode() 会转换非 ASCII 标识(比如: 『Schrödinger』会被转换成 『Schru00f6dinger』),但是 serialize() 不会 转换。
  • 确保 PHP 文件也是 UTF-8 编码,以避免在连接硬编码字符串或者配置字符串常量的时候产生冲突。

常见错误 #7: 认为 $_POST 总是包含你 POST 的数据

不管它的名称,$_POST 数组不是总是包含你 POST 的数据,他也有可能会是空的。 为了理解这一点,让我们来看一下下面这个例子。假设我们使用 jQuery.ajax() 模拟一个服务请求,如下:

// js
$.ajax({
    url: 'http://my.site/some/path',
    method: 'post',
    data: JSON.stringify({a: 'a', b: 'b'}),
    contentType: 'application/json'
});
Copier après la connexion

(顺带一提,注意这里的 contentType: 'application/json' 。我们用 JSON 类型发送数据,这在接口中非常流行。这在 AngularJS $http service 里是默认的发送数据的类型。)

在我们举例子的服务端,我们简单的打印一下 $_POST 数组:

// php
var_dump($_POST);
Copier après la connexion

奇怪的是,结果如下:

array(0) { }
Copier après la connexion

为什么?我们的 JSON 串 {a: 'a', b: 'b'} 究竟发生了什么?

原因在于 当内容类型为 application/x-www-form-urlencoded 或者 multipart/form-data 的时候 PHP 只会自动解析一个 POST 的有效内容。这里面有历史的原因 --- 这两种内容类型是在 PHP 的 $_POST 实现前就已经在使用了的两个重要的类型。所以不管使用其他任何内容类型 (即使是那些现在很流行的,像 application/json), PHP 也不会自动加载到 POST 的有效内容。

既然 $_POST 是一个超级全局变量,如果我们重写 一次 (在我们的脚本里尽可能早的),被修改的值(包括 POST 的有效内容)将可以在我们的代码里被引用。这很重要因为 $_POST 已经被 PHP 框架和几乎所有的自定义的脚本普遍使用来获取和传递请求数据。

所以,举个例子,当处理一个内容类型为 application/json 的 POST 有效内容的时候 ,我们需要手动解析请求内容(decode 出 JSON 数据)并且覆盖 $_POST 变量,如下:

// php
$_POST = json_decode(file_get_contents('php://input'), true);
Copier après la connexion

然后当我们打印 $_POST 数组的时候,我们可以看到他正确的包含了 POST 的有效内容;如下:

array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }
Copier après la connexion

常见错误 #8: 认为 PHP 支持单字符数据类型

阅读下面的代码并思考会输出什么:

for ($c = 'a'; $c <= 'z'; $c++) {
    echo $c . "\n";
}
Copier après la connexion

如果你的答案是 az,那么你可能会对这是一个错误答案感到吃惊。

没错,它确实会输出 az,但是,它还会继续输出 aayz。我们一起来看一下这是为什么。

PHP 中没有 char 数据类型; 只能用 string 类型。记住一点,在 PHP 中增加 string 类型的 z 得到的是 aa

php> $c = 'z'; echo ++$c . "\n";
aa
Copier après la connexion

没那么令人混淆的是,aa 的字典顺序是 小于 z 的:

php> var_export((boolean)('aa' < 'z')) . "\n";
true
Copier après la connexion

这也是为什么上面那段简单的代码会输出 az, 然后 继续 输出 aayz。 它停在了 za,那是它遇到的第一个比 z 的:

php> var_export((boolean)('za' < 'z')) . "\n";
false
Copier après la connexion

事实上,在 PHP 里 有合适的 方式在循环中输出 az 的值:

for ($i = ord('a'); $i <= ord('z'); $i++) {
    echo chr($i) . "\n";
}
Copier après la connexion

或者是这样:

$letters = range('a', 'z');

for ($i = 0; $i < count($letters); $i++) {
    echo $letters[$i] . "\n";
}
Copier après la connexion

常见 错误 #9: 忽视代码规范

尽管忽视代码标准并不直接导致需要去调试 PHP 代码,但这可能是所有需要谈论的事情里最重要的一项。

在一个项目中忽视代码规范能够导致大量的问题。最乐观的预计,前后代码不一致(在此之前每个开发者都在“做自己的事情”)。但最差的结果,PHP 代码不能运行或者很难(有时是不可能的)去顺利通过,这对于 调试代码、提升性能、维护项目来说也是困难重重。并且这意味着降低你们团队的生产力,增加大量的额外(或者至少是本不必要的)精力消耗。

幸运的是对于 PHP 开发者来说,存在 PHP 编码标准建议(PSR),它由下面的五个标准组成:

  • PSR-0: 自动加载标准
  • PSR-1: 基础编码标准
  • PSR-2: 编码风格指导
  • PSR-3: 日志接口
  • PSR-4: 自动加载增强版

PSR 起初是由市场上最大的组织平台维护者创造的。 Zend, Drupal, Symfony, Joomla 和 其他 为这些标准做出了贡献,并一直遵守它们。甚至,多年前试图成为一个标准的 PEAR ,现在也加入到 PSR 中来。

某种意义上,你的代码标准是什么几乎是不重要的,只要你遵循一个标准并坚持下去,但一般来讲,跟随 PSR 是一个很不错的主意,除非你的项目上有其他让人难以抗拒的理由。越来越多的团队和项目正在遵从 PSR 。在这一点上,大部分的 PHP 开发者达成了共识,因此使用 PSR 代码标准,有利于使新加入团队的开发者对你的代码标准感到更加的熟悉与舒适。

常见错误 #10: 滥用 empty()

一些 PHP 开发者喜欢对几乎所有的事情使用 empty() 做布尔值检验。不过,在一些情况下,这会导致混乱。

首先,让我们回到数组和 ArrayObject 实例(和数组类似)。考虑到他们的相似性,很容易假设它们的行为是相同的。然而,事实证明这是一个危险的假设。举例,在 PHP 5.0 中:

// PHP 5.0 或后续版本:
$array = [];
var_dump(empty($array));        // 输出 bool(true)
$array = new ArrayObject();
var_dump(empty($array));        // 输出 bool(false)
// 为什么这两种方法不产生相同的输出呢?
Copier après la connexion

更糟糕的是,PHP 5.0之前的结果可能是不同的:

// PHP 5.0 之前:
$array = [];
var_dump(empty($array));        // 输出 bool(false)
$array = new ArrayObject();
var_dump(empty($array));        // 输出 bool(false)
Copier après la connexion

这种方法上的不幸是十分普遍的。比如,在 Zend Framework 2 下的 Zend\Db\TableGatewayTableGateway::select() 结果中调用 current() 时返回数据的方式,正如文档所表明的那样。开发者很容易就会变成此类数据错误的受害者。

为了避免这些问题的产生,更好的方法是使用 count() 去检验空数组结构:

// 注意这会在 PHP 的所有版本中发挥作用 (5.0 前后都是):
$array = [];
var_dump(count($array));        // 输出 int(0)
$array = new ArrayObject();
var_dump(count($array));        // 输出 int(0)
Copier après la connexion

顺便说一句, 由于 PHP 将 0 转换为 false , count() 能够被使用在 if() 条件内部去检验空数组。同样值得注意的是,在 PHP 中, count() 在数组中是常量复杂度 (O(1) 操作) ,这更清晰的表明它是正确的选择。

另一个使用 empty() 产生危险的例子是当它和魔术方法 _get() 一起使用。我们来定义两个类并使其都有一个 test 属性。

首先我们定义包含 test 公共属性的 Regular 类。

class Regular
{
    public $test = 'value';
}
Copier après la connexion

然后我们定义 Magic 类,这里使用魔术方法 __get() 来操作去访问它的 test 属性:

class Magic
{
    private $values = ['test' => 'value'];

    public function __get($key)
    {
        if (isset($this->values[$key])) {
            return $this->values[$key];
        }
    }
}
Copier après la connexion

好了,现在我们尝试去访问每个类中的 test 属性看看会发生什么:

$regular = new Regular();
var_dump($regular->test);    // 输出 string(4) "value"
$magic = new Magic();
var_dump($magic->test);      // 输出 string(4) "value"
Copier après la connexion

到目前为止还好。

但是现在当我们对其中的每一个都调用 empty() ,让我们看看会发生什么:

var_dump(empty($regular->test));    // 输出 bool(false)
var_dump(empty($magic->test));      // 输出 bool(true)
Copier après la connexion

咳。所以如果我们依赖 empty() ,我们很可能误认为 $magic 的属性 test 是空的,而实际上它被设置为 'value'

不幸的是,如果类使用魔术方法 __get() 来获取属性值,那么就没有万无一失的方法来检查该属性值是否为空。
在类的作用域之外,你仅仅只能检查是否将返回一个 null 值,这并不意味着没有设置相应的键,因为它实际上还可能被设置为 null

相反,如果我们试图去引用 Regular 类实例中不存在的属性,我们将得到一个类似于以下内容的通知:

Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10

Call Stack:
    0.0012     234704   1. {main}() /path/to/test.php:0
Copier après la connexion

所以这里的主要观点是 empty() 方法应该被谨慎地使用,因为如果不小心的话它可能导致混乱 -- 甚至潜在的误导 -- 结果。

Résumé

La facilité d'utilisation de PHP fait tomber les développeurs dans un faux sentiment de confort. Certaines nuances et caractéristiques du langage lui-même peuvent vous coûter beaucoup de temps. déboguer. Ceux-ci peuvent empêcher les programmes PHP de fonctionner correctement et provoquer des problèmes tels que celui décrit ici.

PHP a considérablement changé au cours de ses 20 ans d'histoire. Cela vaut la peine de prendre le temps de se familiariser avec les subtilités du langage lui-même, car cela permet de garantir que le logiciel que vous écrivez est plus évolutif, robuste et maintenable.

Apprentissage recommandé : "Tutoriel vidéo PHP"

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
php
source:juejin.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal