目次
よくある間違い #1: foreach ループの後に配列参照を残す
名前は isset ですが、関数の動作を誤解するただし、
常见错误 #3:关于通过引用返回与通过值返回的困惑
常见的错误 #4:在循环中执行查询
常见问题 #5: 内存使用欺骗与低效
常见错误 #6: 忽略 Unicode/UTF-8 的问题
常见错误 #7: 认为 $_POST 总是包含你 POST 的数据
常见错误 #8: 认为 PHP 支持单字符数据类型
常见 错误 #9: 忽视代码规范
常见错误 #10:  滥用 empty()
概要
ホームページ バックエンド開発 PHPチュートリアル PHP プログラマーが犯しやすい 10 の間違い (共有)

PHP プログラマーが犯しやすい 10 の間違い (共有)

May 31, 2021 pm 07:19 PM
php

この記事では、PHP 開発者が犯す最も一般的な 10 の間違いを紹介します。一定の参考値があるので、困っている友達が参考になれば幸いです。

PHP プログラマーが犯しやすい 10 の間違い (共有)

#PHP 言語を使用すると、WEB 側のプログラミングが簡単になるため、非常に人気があります。しかし、そのシンプルさゆえに、PHP は徐々に比較的複雑な言語へと発展してきました。無限のフレームワーク、さまざまな言語機能、バージョンの違いにより、私たちはしばしば混乱し、デバッグに多くの時間を浪費しなければなりません。この記事では、注目に値する最も一般的な 10 個の間違いをリストします。

よくある間違い #1: foreach ループの後に配列参照を残す

PHP で foreach## がどのように動作するかが明確ではありません# トラバースはどのように機能しますか?配列を反復処理するときに配列内の各要素を操作したい場合は、

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
        $value = $value * 2;
}
// $arr 现在是 array(2, 4, 6, 8)
ログイン後にコピー
などの foreach

ループで参照を使用すると非常に便利です。問題は次のとおりです。注意しないと問題が発生する可能性があることに注意してください。上記の例では、コードが実行された後、

$value はスコープ内に残り、配列の最後の要素への参照を保持します。 $value に関連する後続の操作により、配列内の最後の要素の値が誤って変更される可能性があります。

foreach はブロックレベルのスコープを作成しないことに注意してください。したがって、上記の例では、$value はグローバル参照変数です。 foreach トラバーサルでは、各反復で $arr 次の要素への参照が形成されます。トラバーサルが完了すると、$value$arr の最後の要素を参照し、スコープ内に残ります。

この動作は、そうでないいくつかの問題を引き起こします。わかりにくいバグです。以下は例です

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

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

foreach ($array as $value) {}     // 通过赋值遍历
echo implode(',', $array), "\n";
ログイン後にコピー

上記のコードは

1,2,3
1,2,3
1,2,2
ログイン後にコピー

を出力します 正しく読みました、最後の行の最後の値は 3 ではなく 2 です、なぜですか?

最初の

foreach トラバーサルの完了後、$array は変更されませんが、上で説明したように、$value は残ります。 $array の最後の要素 (foreach が参照によって $value を取得するため)

これは、2 番目の

foreach# への実行時に発生します。 ##、この「奇妙なこと」が起こりました。 $value が代入によって取得されると、foreach は各 $array の要素を $value に順番にコピーします。 foreach 内部の詳細は次のとおりです。最初のステップ:

$array[0]
    (つまり 1) を
  • $value## にコピーします。 ($value は実際には $array の最後の要素への参照、つまり $array[2])、したがって $array[2] は 1 に等しくなります。したがって、$array には [1, 2, 1] が含まれるようになります。ステップ 2: $array[1] (つまり 2) を
  • $ にコピーします。 value
  • ($array[2] への参照) なので、$array[2] は 2 になります。したがって、$array には [1, 2, 2] が含まれます。 ステップ 3: $array[2] (現在は 2) を
  • $ にコピーします。 value
  • ($array[2] への参照) なので、$array[2] は 2 になります。したがって、$array には [1, 2, 2] が含まれるようになりました。foreach
  • で参照を便利に使用し、この問題を回避するには、
してください。 foreach

実行後 unset() 参照を保持している変数を削除します。例: <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class='brush:php;toolbar:false;'>$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value 不再引用 $arr[3]</pre><div class="contentsignin">ログイン後にコピー</div></div>よくある間違い #2: isset()

名前は isset ですが、関数の動作を誤解するただし、

##isset()

は、変数が存在しない場合に false を返すだけでなく、変数値が の場合にも を返します。 null falseこの動作は、見た目よりもさらに問題が多く、一般的なエラーの原因となります。 次のコードを見てください:

$data = fetchRecordFromStorage($storage, $identifier);
if (!isset($data['keyShouldBeSet']) {
    // do something here if 'keyShouldBeSet' is not set
}
ログイン後にコピー

開発者は、

keyShouldBeSet

$data

に存在するかどうかを確認する必要があります。ただし、前述したように、

$data['keyShouldBeSet'] が存在し、値が null の場合、isset($data['keyShouldBeSet']) false も返します。したがって、上記のロジックは厳密ではありません。 別の例を見てみましょう:

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

// ...

if (!isset($postData)) {
    echo 'post not active';
}
ログイン後にコピー
上記のコードは、$_POST['active']

true

を返す場合、一般的に信じられています。

postData は確実に存在するため、isset($postData)true を返します。逆に、isset($postData)false を返す唯一の可能性は、$_POST['active']false# を返すことです。 ##。 しかし、そうではありません。 <p>如我所言,如果<code>$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';
}
ログイン後にコピー

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

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

$data = fetchRecordFromStorage($storage, $identifier);
if (! array_key_exists('keyShouldBeSet', $data)) {
    // do this if 'keyShouldBeSet' isn't set
}
ログイン後にコピー

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

if (array_key_exists('varShouldBeSet', get_defined_vars())) {
    // variable $varShouldBeSet exists in current scope
}
ログイン後にコピー

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

考虑下面的代码片段:

class Config
{
    private $values = [];

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

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
ログイン後にコピー

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

PHP Notice:  Undefined index: test in /path/to/my/script.php on line 21
ログイン後にコピー

出了什么问题?

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

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

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


// getValues() 又返回了另一份 $values 数组的拷贝
// 且这份拷贝中并不包含一个`test`元素(这就是为什么我们会得到 「未定义索引」 消息)。
echo $config->getValues()['test'];
ログイン後にコピー

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

$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];
ログイン後にコピー

这段代码将会正常工作(例如,它将会输出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'];
ログイン後にコピー

这会输出期待的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'];
ログイン後にコピー

如果你认为这段代码会导致与之前的数组例子一样的「未定义索引」错误,那就错了。实际上,这段代码将会正常运行。原因是,与数组不同,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』
ログイン後にコピー

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

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

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

$models = [];

foreach ($inputValues as $inputValue) {
    $models[] = $valueRepository->findByValue($inputValue);
}
ログイン後にコピー

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

$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
ログイン後にコピー

结果每轮循环都会产生一次对数据库的查询。 因此,假如你为这个循环提供了一个包含 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();
}
ログイン後にコピー

但是 只用一条 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;
    }
}
ログイン後にコピー

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

常见问题 #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);
}
ログイン後にコピー

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";
ログイン後にコピー

输出结果是:

Before: 224704
Limit 1: 224704
Limit 10000: 224704
ログイン後にコピー

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

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

PHP Warning:  mysqli::query(): (HY000/2013):
              Lost connection to MySQL server during query in /root/test.php on line 11
ログイン後にコピー

究竟发生了啥?

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

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

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

Before: 232048
Limit 1: 324952
Limit 10000: 32572912
ログイン後にコピー

顺便一提,这比刚才更糟糕。根据 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");
}
ログイン後にコピー

当我们把这个常见错误和上面的 常见错误 #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'
});
ログイン後にコピー

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

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

// php
var_dump($_POST);
ログイン後にコピー

奇怪的是,结果如下:

array(0) { }
ログイン後にコピー

为什么?我们的 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);
ログイン後にコピー

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

array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }
ログイン後にコピー

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

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

for ($c = 'a'; $c <= 'z'; $c++) {
    echo $c . "\n";
}
ログイン後にコピー

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

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

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

php> $c = 'z'; echo ++$c . "\n";
aa
ログイン後にコピー

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

php> var_export((boolean)('aa' < 'z')) . "\n";
true
ログイン後にコピー

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

php> var_export((boolean)('za' < 'z')) . "\n";
false
ログイン後にコピー

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

for ($i = ord('a'); $i <= ord('z'); $i++) {
    echo chr($i) . "\n";
}
ログイン後にコピー

或者是这样:

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

for ($i = 0; $i < count($letters); $i++) {
    echo $letters[$i] . "\n";
}
ログイン後にコピー

常见 错误 #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)
// 为什么这两种方法不产生相同的输出呢?
ログイン後にコピー

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

// PHP 5.0 之前:
$array = [];
var_dump(empty($array));        // 输出 bool(false)
$array = new ArrayObject();
var_dump(empty($array));        // 输出 bool(false)
ログイン後にコピー

这种方法上的不幸是十分普遍的。比如,在 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)
ログイン後にコピー

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

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

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

class Regular
{
    public $test = 'value';
}
ログイン後にコピー

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

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

    public function __get($key)
    {
        if (isset($this->values[$key])) {
            return $this->values[$key];
        }
    }
}
ログイン後にコピー

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

$regular = new Regular();
var_dump($regular->test);    // 输出 string(4) "value"
$magic = new Magic();
var_dump($magic->test);      // 输出 string(4) "value"
ログイン後にコピー

到目前为止还好。

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

var_dump(empty($regular->test));    // 输出 bool(false)
var_dump(empty($magic->test));      // 输出 bool(true)
ログイン後にコピー

咳。所以如果我们依赖 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
ログイン後にコピー

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

概要

PHP は使いやすいため、開発者は誤った安心感に陥ります。言語自体のニュアンスや特性によっては、理解するのに多大な時間がかかる場合があります。デバッグです。これらにより、PHP プログラムが適切に動作できなくなり、ここで説明するような問題が発生する可能性があります。

PHP は 20 年の歴史の中で大きく変化しました。時間をかけて言語自体の微妙な点を理解することは、作成するソフトウェアの拡張性、堅牢性、保守性を高めるのに役立つため、価値があります。

推奨学習: 「PHP ビデオ チュートリアル

以上がPHP プログラマーが犯しやすい 10 の間違い (共有)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

Ubuntu および Debian 用の PHP 8.4 インストールおよびアップグレード ガイド Ubuntu および Debian 用の PHP 8.4 インストールおよびアップグレード ガイド Dec 24, 2024 pm 04:42 PM

PHP 8.4 では、いくつかの新機能、セキュリティの改善、パフォーマンスの改善が行われ、かなりの量の機能の非推奨と削除が行われています。 このガイドでは、Ubuntu、Debian、またはその派生版に PHP 8.4 をインストールする方法、または PHP 8.4 にアップグレードする方法について説明します。

今まで知らなかったことを後悔している 7 つの PHP 関数 今まで知らなかったことを後悔している 7 つの PHP 関数 Nov 13, 2024 am 09:42 AM

あなたが経験豊富な PHP 開発者であれば、すでにそこにいて、すでにそれを行っていると感じているかもしれません。あなたは、運用を達成するために、かなりの数のアプリケーションを開発し、数百万行のコードをデバッグし、大量のスクリプトを微調整してきました。

PHP 開発用に Visual Studio Code (VS Code) をセットアップする方法 PHP 開発用に Visual Studio Code (VS Code) をセットアップする方法 Dec 20, 2024 am 11:31 AM

Visual Studio Code (VS Code とも呼ばれる) は、すべての主要なオペレーティング システムで利用できる無料のソース コード エディター (統合開発環境 (IDE)) です。 多くのプログラミング言語の拡張機能の大規模なコレクションを備えた VS Code は、

JSON Web Tokens(JWT)とPHP APIでのユースケースを説明してください。 JSON Web Tokens(JWT)とPHP APIでのユースケースを説明してください。 Apr 05, 2025 am 12:04 AM

JWTは、JSONに基づくオープン標準であり、主にアイデンティティ認証と情報交換のために、当事者間で情報を安全に送信するために使用されます。 1。JWTは、ヘッダー、ペイロード、署名の3つの部分で構成されています。 2。JWTの実用的な原則には、JWTの生成、JWTの検証、ペイロードの解析という3つのステップが含まれます。 3. PHPでの認証にJWTを使用する場合、JWTを生成および検証でき、ユーザーの役割と許可情報を高度な使用に含めることができます。 4.一般的なエラーには、署名検証障害、トークンの有効期限、およびペイロードが大きくなります。デバッグスキルには、デバッグツールの使用とロギングが含まれます。 5.パフォーマンスの最適化とベストプラクティスには、適切な署名アルゴリズムの使用、有効期間を合理的に設定することが含まれます。

母音を文字列にカウントするPHPプログラム 母音を文字列にカウントするPHPプログラム Feb 07, 2025 pm 12:12 PM

文字列は、文字、数字、シンボルを含む一連の文字です。このチュートリアルでは、さまざまな方法を使用してPHPの特定の文字列内の母音の数を計算する方法を学びます。英語の母音は、a、e、i、o、u、そしてそれらは大文字または小文字である可能性があります。 母音とは何ですか? 母音は、特定の発音を表すアルファベットのある文字です。大文字と小文字など、英語には5つの母音があります。 a、e、i、o、u 例1 入力:string = "tutorialspoint" 出力:6 説明する 文字列「TutorialSpoint」の母音は、u、o、i、a、o、iです。合計で6元があります

PHPでHTML/XMLを解析および処理するにはどうすればよいですか? PHPでHTML/XMLを解析および処理するにはどうすればよいですか? Feb 07, 2025 am 11:57 AM

このチュートリアルでは、PHPを使用してXMLドキュメントを効率的に処理する方法を示しています。 XML(拡張可能なマークアップ言語)は、人間の読みやすさとマシン解析の両方に合わせて設計された多用途のテキストベースのマークアップ言語です。一般的にデータストレージに使用されます

PHPでの後期静的結合を説明します(静的::)。 PHPでの後期静的結合を説明します(静的::)。 Apr 03, 2025 am 12:04 AM

静的結合(静的::) PHPで後期静的結合(LSB)を実装し、クラスを定義するのではなく、静的コンテキストで呼び出しクラスを参照できるようにします。 1)解析プロセスは実行時に実行されます。2)継承関係のコールクラスを検索します。3)パフォーマンスオーバーヘッドをもたらす可能性があります。

PHPマジックメソッド(__construct、__destruct、__call、__get、__setなど)とは何ですか? PHPマジックメソッド(__construct、__destruct、__call、__get、__setなど)とは何ですか? Apr 03, 2025 am 12:03 AM

PHPの魔法の方法は何ですか? PHPの魔法の方法には次のものが含まれます。1。\ _ \ _コンストラクト、オブジェクトの初期化に使用されます。 2。\ _ \ _リソースのクリーンアップに使用される破壊。 3。\ _ \ _呼び出し、存在しないメソッド呼び出しを処理します。 4。\ _ \ _ get、dynamic属性アクセスを実装します。 5。\ _ \ _セット、動的属性設定を実装します。これらの方法は、特定の状況で自動的に呼び出され、コードの柔軟性と効率を向上させます。

See all articles