了解 PHP 8 字符串与数字比较的改进

在 PHP 中,比较两个数值从来都不是问题。比较两个字符串也不是问题。问题出现在字符串与数值数据(硬编码的数字,或包含 floatint 类型数据的变量)之间的非严格比较。在这种情况下,如果执行非严格比较,PHP 将始终把字符串转换为数值。

只有当字符串只包含数字(或数值,如正数、负数或小数分隔符)时,字符串到数值的转换才会 100% 成功。本节将介绍如何防止涉及字符串和数字数据的非严格比较不准确。要想编写出行为一致、可预测的代码,掌握本章介绍的概念至关重要。

在了解字符串与数字比较的细节之前,我们首先需要了解非严格比较的含义。

了解严格和非严格比较

类型杂耍(type juggling)的概念是 PHP 语言的重要组成部分。从 PHP 语言诞生的第一天起,它就具备了这种功能。类型转换涉及在执行操作前进行内部数据类型转换。这种能力对于语言的成功至关重要。

PHP 最初是为在网络环境中运行而设计的,因此需要一种方法来处理作为 HTTP 数据包一部分传输的数据。HTTP 头和正文以文本形式传输,PHP 接收到的是存储在一组超级全局变量(包括 $_SERVER$_GET$_POST 等)中的字符串。因此,在执行涉及数字的操作时,PHP 语言需要一种快速处理字符串值的方法。这就是类型比较过程的工作。

严格的比较首先要检查数据类型。如果数据类型匹配,则继续比较。调用严格比较的操作符包括 ===!== 等。某些函数有强制执行严格数据类型的选项。in_array() 就是一个例子。如果将第三个参数设置为 TRUE,就会进行严格类型搜索。下面是 in_array() 的方法签名:

in_array(mixed $needle, array $haystack, bool $strict = false)

非严格比较是指在比较之前不进行数据类型检查。执行非严格比较的操作符包括 ==!=<> 等。值得注意的是,switch {} 语言结构在其 case 语句中执行非严格比较。如果进行的非严格比较涉及不同数据类型的操作数,则会执行类型杂耍。

现在让我们详细了解一下数字字符串。

检查数字字符串

数字字符串 是仅包含数字或数字字符(如加号 (+)、减号 (-) 和小数分隔符)的字符串。

需要注意的是,PHP 8 内部使用句号字符(.)作为小数分隔符。如果需要在不使用句号作为小数分隔符的本地语言(例如在法国,逗号(,)被用作小数分隔符)中显示数字,请使用 number_format() 函数(参见 https://www.php.net/number_format )。更多信息,请参阅本章 "利用本地语言的独立性" 一节。

数字字符串也可以使用工程符号(也称为科学符号)组成。非完形数字字符串是指除数字、加号、减号或十进制分隔符外,还包含其他值的数字字符串。前导数字字符串以数字字符串开头,但后面跟着非数字字符。任何既不是数字也不是前导数字的字符串都会被 PHP 引擎视为非数字字符串。

在以前的 PHP 版本中,类型杂耍会不一致地解析包含数字的字符串。在 PHP 8 中,只有数字字符串才能干净利落地转换为数字:不能出现前导或尾部空白或其他非数字字符。

举个例子,看看 PHP 7 和 PHP 8 在处理数字字符串时的不同之处:

// /repo/ch06/php8_num_str_handling.php
$test = [
    0 => '111',
    1 => ' 111',
    2 => '111 ',
    3 => '111xyz'
];
$patt = "%d : %3d : '%-s'\n";
foreach ($test as $key => $val) {
    $num = 111 + $val;
    printf($patt, $key, $num, $val);
}

下面是在 PHP 7 中运行的输出结果:

root@php8_tips_php7 [ /repo/ch06 ]#
php php8_num_str_handling.php
0 : 222 : '111'
1 : 222 : ' 111'
PHP Notice: A non well formed numeric value encountered in /
repo/ch06/php8_num_str_handling.php on line 11
2 : 222 : '111 '
PHP Notice: A non well formed numeric value encountered in /
repo/ch06/php8_num_str_handling.php on line 11
3 : 222 : '111xyz'

从输出中可以看出,PHP 7 认为尾部有空格的字符串是非格式化字符串。但是,带前导空格的字符串被认为是格式良好的字符串,可以通过而不会生成通知。带有非空格字符的字符串仍会被处理,但会产生通知。

下面是在 PHP 8 中运行的相同代码示例:

root@php8_tips_php8 [ /repo/ch06 ]#
php php8_num_str_handling.php
0 : 222 : '111'
1 : 222 : ' 111'
2 : 222 : '111 '
PHP Warning: A non-numeric value encountered in /repo/ch06/
php8_num_str_handling.php on line 11
3 : 222 : '111xyz'

PHP 8 对包含前导空格或尾部空格的数字字符串一视同仁,不会生成 "通知" 或 "警告",这一点要一致得多。不过,最后一个字符串在 PHP 7 中以前是 "通知",现在则会生成 "警告"。

有关数字字符串的信息,请参阅 PHP 文档: https://www.php.net/manual/en/language.types.numeric-strings.php

有关类型杂耍的更多信息,请参阅以下网址: https://www.php.net/manual/en/language.types.typejuggling.php

现在您已经知道什么是格式正确的数字字符串,什么是非格式正确的数字字符串,让我们把注意力转向在 PHP 8 中处理数字字符串时可能出现的向后兼容中断这一更严重的问题。

检测涉及数字字符串的向后兼容中断

您必须了解 PHP 8 升级后哪些地方可能会导致代码崩溃。在本小节中,我们将向您展示一些极其细微的差别,它们可能会产生严重的后果。

任何时候使用非格式化的数字字符串都有可能导致代码崩溃:

  • 使用 is_numeric()

  • 在字符串偏移量(例如,$str['4x'])中

  • 使用位运算符

  • 当递增或递减一个变量时,如果该变量的值是一个格式不完善的 数值字符串

以下是一些修复代码的建议:

  • 对于可能包含前导或尾部空白的数字字符串(例如,嵌入到已发布表单数据中的数字字符串),请考虑使用 trim()

  • 如果您的代码依赖于以数字开头的字符串,请使用显式类型转换来确保数字被正确插值。

  • 不要依赖空字符串(例如,$str = '' )来干净利落地转换为 0。

在下面的代码示例中,一个尾部有空格的非格式化字符串被赋值给 $age

// /repo/ch06/php8_num_str_is_numeric.php
$age = '77 ';
echo (is_numeric($age))
    ? "Age must be a number\n"
    : "Age is $age\n";

在 PHP 7 中运行这段代码时,is_numeric() 返回 TRUE。下面是 PHP 7 的输出结果:

root@php8_tips_php7 [ /repo/ch06 ]#
php php8_num_str_is_numeric.php
Age is 77

另一方面,当我们在 PHP 8 中运行这段代码时,is_numeric() 返回 FALSE,因为字符串不被视为数字。下面是 PHP 8 的输出结果:

root@php8_tips_php8 [ /repo/ch06 ]#
php php8_num_str_is_numeric.php
Age must be a number

正如您所看到的,PHP 7 和 PHP 8 在字符串处理方面的差异会导致应用程序出现不同的行为,并可能带来灾难性的结果。现在让我们来看看涉及格式良好的字符串的不一致结果。

处理不一致的字符串-数字比较结果

要完成涉及字符串和数字数据的非严格比较,PHP 引擎首先会执行一个类型转换操作,在内部将字符串转换为数字,然后再执行比较。不过,即使是格式良好的数字字符串,也可能产生从人类角度看毫无意义的结果。

举例来说,请看下面的代码示例:

  1. 首先,我们在值为 0 的变量 $zero 和值为 ABC 的变量 $string 之间进行非严格比较:

    $zero = 0;
    $string = 'ABC';
    $result = ($zero == $string) ? 'is' : 'is not';
    echo "The value $zero $result the same as $string\n"2
  2. 下面的非严格比较使用 in_array(),以查找 $array 数组中的零值:

    $array = [1 => 'A', 2 => 'B', 3 => 'C'];
    $result = (in_array($zero, $array))
        ? 'is in' : 'is not in';
    echo "The value $zero $result\n"
        . var_export($array, TRUE);
  3. 最后,我们在前导数字字符串 42abc88 和硬编码数字 42 之间进行非严格比较:

    $mixed = '42abc88';
    $result = ($mixed == 42) ? 'is' : 'is not';
    echo "\nThe value $mixed $result the same as 42\n";

PHP 7 中运行的结果超出了人类的理解范围!以下是 PHP 7 结果:

root@php8_tips_php7 [ /repo/ch06 ]#
php php7_compare_num_str.php
The value 0 is the same as ABC
The value 0 is in
array (1 => 'A', 2 => 'B', 3 => 'C')
The value 42abc88 is the same as 42

从人类的角度来看,这些结果都毫无意义!而从计算机的角度来看,却完全说得通。字符串 ABC 转换成数字后,其值为零。同样,在进行数组搜索时,每个数组元素都只有一个字符串值,最终被插值为零。

前导数字字符串的情况比较棘手。在 PHP 7 中,插值算法会转换数字字符,直到遇到第一个非数字字符。一旦出现这种情况,插值就会停止。因此,字符串 42abc88 在比较时会变成整数 42。现在让我们看看 PHP 8 是如何处理字符串到数字的比较的。

了解 PHP 8 中所做的比较更改

在 PHP 8 中,如果将字符串与数字进行比较,只有数字字符串才被认为是有效的。使用指数符号的字符串以及带有前导或尾部空白的数字字符串也可用于比较。需要注意的是,PHP 8 在转换字符串之前会进行这种判断,这一点非常重要。

请看上一小节(处理字符串与数字比较结果不一致的问题)中描述的代码示例在 PHP 8 中运行的输出结果:

root@php8_tips_php8 [ /repo/ch06 ]#
php php7_compare_num_str.php
The value 0 is not the same as ABC
The value 0 is not in
array (1 => 'A', 2 => 'B', 3 => 'C')
The value 42abc88 is not the same as 42

因此,从输出结果可以看出,在 PHP 8 升级后,应用程序有很大可能会改变其行为。作为 PHP 8 字符串处理的最后一点,让我们来看看如何避免升级问题。

避免 PHP 8 升级期间出现问题

您面临的主要问题是 PHP 8 在处理涉及不同数据类型操作数的非严格比较时存在差异。如果一个操作数是 intfloat,而另一个操作数是字符串,那么升级后就会出现潜在问题。如果字符串是有效的数字字符串,非严格比较将顺利进行。

受影响的操作符如下 <=>==!=>>=<<=。 如果选项标志设置为默认,下列函数会受到影响:

  • in_array()

  • array_search()

  • array_keys()

  • sort()

  • rsort()

  • asort()

  • arsort()

  • array_multisort()

有关 PHP 8 中改进的数字字符串处理的更多信息,请参阅以下链接: https://wiki.php.net/rfc/saner-numeric-strings 。此处记录了 PHP 8 的一个相关更改: https://wiki.php.net/rfc/string_to_number_comparison

最好的做法是通过为函数或方法提供类型提示,尽量减少 PHP 类型杂耍。还可以在比较之前强制数据类型。最后,可以考虑使用严格比较,尽管这并不适合所有情况。

现在,您已经了解了如何在 PHP 8 中正确处理涉及数字字符串的比较,现在让我们来看看 PHP 8 在算术、位运算和连接操作方面的变化。