[关闭]
@levinzhang 2021-01-03T15:31:35.000000Z 字数 19839 阅读 414

PHP 7:函数的增强

by

摘要:

在本文中,我们将会探讨PHP 7中函数的增强,包括用于数组常量定义的define()、从Generator函数中返回值、Generator代理等。


核心要点

PHP 7系列前面的文章中,我们讨论了PHP类型系统的新特性。在本文中,我们将会探讨PHP 7在函数方面的改善。

PHP支持多种类型的函数,包括用户自定义函数、内部函数、变量函数以及匿名函数。

定义数组常量的新函数

PHP 7.0添加了名为define()的新函数,用来在运行时定义命名的数组常量。define()的语法如下所示:

bool define ( string $name , mixed $value [, bool $case_insensitive = FALSE ] )

表1讨论了该函数的参数

表1 define()函数的参数

参数 描述
name 常量的名称。这个名称可以是保留字,但不建议这样做。
value 常量的值。这个值必须是一个标量值(整型、浮点、字符串、布尔值或NULL)或数组。
case_insensitive 常量是否区分大小写,默认是区分大小写的。在PHP 7.3.0中,不推荐采用不区分大小写的方式。

创建PHP脚本constant.php并定义名为CONSTANT的常量,如下所示。

define("CONSTANT", "Hello PHP");

我们还可以使用const关键字来定义常量:

  1. const CONSTANT_2 = 'Hello php';
  2. const Constant = 'HELLO PHP';

define()函数可以用来定义数组常量:

define("Catalog", ['Oracle Magazine','Java Magazine']);

数组常量的值可以使用数组元素访问的方式进行输出。

  1. echo Catalog[0]
  2. echo Catalog[1]

constant.php文件如下所示:

  1. <?php
  2. define("CONSTANT", "Hello PHP");
  3. echo CONSTANT."<br>";
  4. const CONSTANT_2 = 'Hello php';
  5. echo CONSTANT_2."<br>";
  6. const Constant = 'HELLO PHP';
  7. echo Constant."<br>";
  8. define("Catalog", ['Oracle Magazine','Java Magazine']);
  9. echo Catalog[0]."<br>";
  10. echo Catalog[1]
  11. ?>

运行脚本将会输出脚本中定义的常量值。

  1. Hello PHP
  2. Hello php
  3. HELLO PHP
  4. Oracle Magazine
  5. Java Magazine

全局定义的常量,如TRUEFALSE,不能进行重定义。为了阐述这一点,我们创建一个const.php脚本,该脚本定义了常量TRUE,并将它的值设置成了20

  1. <?php
  2. define('TRUE', 20);
  3. echo TRUE;
  4. ?>

如果你运行该脚本的话,它将会输出的值是1,这也是TRUE全局定义的值。

绑定对象作用域到闭包的新函数

闭包是用来表示匿名函数的类。PHP 7.0引入了新的Closure::call()函数,能够非常简便地将对象作用域临时绑定到一个闭包并调用它。为了阐述这一点,创建脚本closure.php并复制如下清单所示的代码:

  1. <?php
  2. class Hello {
  3. private function getMsg() {
  4. echo "Hello";
  5. }
  6. }
  7. $getMsg = function() {return $this->getMsg();};
  8. echo $getMsg->call(new Hello);
  9. ?>

在上述脚本中,Hello是一个具有函数getMsg()的类。Closure::call()函数用来创建一个Hello实例,并绑定它的作用域到一个调用getMsg方法的闭包。运行该脚本将会输出Hello消息。

Expectation

在讨论expectation之前,我们先看一下传统的断言。传统的assert()方法定义如下所示。它会检查如果代码的预期值(称为断言)是不是为FALSE,如果是FALSE的话,它会打印出描述消息,并且默认会中止程序。如果断言不是FALSE的话,assert()没有任何效果。

bool assert ( mixed $assertion [, string $description ] )

按照设计,断言用于开发和测试期的调试,而不是运行期的操作。assert()的行为可以通过assert_options()或者.ini文件进行配置。

传统上,assert()的第一个参数应该是一个要评估计算的字符串或者作为断言进行测试的boolean条件。如果所提供的是一个字符串的话,它会以PHP代码的形式进行评估计算。如果所提供的是一个boolean条件的话,那么在传递给assert_options()定义的断言回调函数(如果存在的话)之前,它将会被转换成字符串。

断言在PHP 7中进行了彻底修改,现在被称为expectation,并且assert()是PHP 7中的一个语言结构。作为对assert()的增强,expectations被添加了进来,并且具有如下的语法:

bool assert ( mixed $assertion [, Throwable $exception ] )

有了expectations之后,assert()的第一个参数可能是会返回一个值的表达式,而不再是一个要评估执行的PHP代码字符串或者要测试的boolean条件。表达式会被执行,得到的结果会被用来确定断言是否成功。从PHP 7开始,使用字符串作为第一个参数已经被废弃掉了。assert_options()依然能够与expectations协同使用,但是并不推荐这么做。相反,我们应该使用两个新的php.ini配置指令,如表2所示。

表2 Expectations的配置指令

配置指令 类型 描述 支持的值 默认值
zend.assertions integer 配置是否要生成断言代码并运行。 1:生成和运行断言代码(开发模式)** 0:生成断言代码但是并不会在运行时执行 **-1:不生成断言代码(生产模式)。使用该配置以便于使用expectations。 1
assert.exception 对于失败的断言抛出AssertionError或自定义异常 1:当断言失败的时候,要么抛出以异常的形式提供的对象,要么在没有提供异常的情况下抛出一个新的AssertionError对象。使用该配置以便于使用expectations。** **0:使用或生成上述的Throwable,但是只生成一个告警,而不抛出异常或AssertionError。 0

借助expectations,第二个参数可能会是一个Throwable对象,而不再是字符串描述。为了在断言失败的时候抛出Throwable对象或异常,assert.exception指令必须设置为1。

相对于传统的断言,expectations有如下的优势,不过为了向后兼容,传统的断言依然是支持的:

作为如何使用expectations的样例,我们创建一个名为expectation.php的脚本,并复制如下的代码清单到脚本中。这个脚本将这两个配置指令都设置成了1。assert语言结构将第一个参数设置为true,第二个参数设置为一个自定义的AssertionError

  1. <?php
  2. ini_set('assert.exception', 1);
  3. ini_set('zend.assertions', 1);
  4. assert(true, new AssertionError('Assertion failed.'));
  5. ?>

如果运行这个脚本的话,将不会抛出AssertionError

接下来,将第一个参数设置为false

assert(false, new AssertionError('Assertion failed.'));

如果再次运行脚本的话,expectation将会失败并抛出AssertonError

Uncaught AssertionError: Assertion failed

为了阐述在assert()中借助表达式和自定义AssertionError使用expectations,我们首先从传统的使用assert()方式开始,测试一个变量是数字的断言。这里使用字符串来测试变量,并在断言失败的时候输出消息。

  1. $numValue = '123string';
  2. assert('is_numeric_Value($numValue)' , "Assertion that $numValue is a number failed." );

接下来,使用表达式作为assert()的第一个参数。然后,使用AssertionError对象来抛出自定义的错误信息。

  1. $num = 10;
  2. assert($num > 50 , new AssertionError("Assertion that $num is greater than 50 failed.") );

Generator支持return表达式

Generator函数是能够返回一个迭代对象的函数,比如foreach。Generator函数是简单的迭代器,它们不需要类实现Iterator接口。Generator函数的语法与普通函数的语法一样,只不过函数中包含一个或多个yield语句,当Generator函数所返回的迭代器被迭代的时候,每个yield语句都会生成一个值。Generator函数必须要像正常的函数那样进行调用,并提供所有必要的参数。

PHP 7.0为Generator函数添加了一个新的特性,那就是支持在所有的yield语句之后声明一个return语句。在Generator函数中所返回的值可以通过Generator函数返回对象的getReturn()函数来进行访问。我们不要将Generator函数返回的迭代器对象与Generator函数返回的值混淆。迭代器对象并不包含返回值,它只包含yield所生成的值。如果Generator函数需要执行某些计算并返回一个最终值的话,那么能够返回值就是非常有用的。Generators可以声明GeneratorIteratorTraversableiterable返回类型。

为了阐述如何使用这个新特性,我们创建名为gen_return.php的脚本,该脚本定义了带有多个yield语句的Generator函数并且在调用Generator函数的时候传入1作为参数。接下来,使用foreach迭代它的返回值并输出yield所生成的值。最后,使用getReturn()函数输出返回值。gen_return.php脚本如下所示:

  1. <?php
  2. $gen_return = (function($var) {
  3. $x=$var+2;
  4. yield $var;
  5. yield $x;
  6. $y=$x+2;
  7. return $y;
  8. })(1);
  9. foreach ($gen_return as $value) {
  10. echo $value, PHP_EOL;
  11. }
  12. echo $gen_return->getReturn(), PHP_EOL;
  13. ?>

如果运行脚本的话,将会得到如下的输出:

1 3 5

Generator委托

PHP 7.0添加了对Generator委托的支持,这意味着一个Generator可以通过yield from关键字委托给另外一个Generator、Traversable对象或数组。为了阐述Generator委托功能,我们创建名为gen_yield_from.php的脚本并定义两个Generator函数,即gen($var)gen2($var),其中gen($var)通过如下的语句委托给了gen2($var)

yield from gen2($var);

随后,在一个foreach循环中遍历gen($var)所返回的迭代器对象。脚本gen_yield_from.php如下所示:

  1. <?php
  2. function gen($var)
  3. {
  4. yield $var;
  5. $x=$var+2;
  6. yield $x;
  7. yield from gen2($var);
  8. }
  9. function gen2($var)
  10. {
  11. $y=$var+1;
  12. yield $var;
  13. yield $y;
  14. }
  15. foreach (gen(1) as $val)
  16. {
  17. echo $val, PHP_EOL;
  18. }
  19. ?>

运行脚本将会输出这两个Generator函数所生成的值:

1 3 1 2

整数除法的新函数

PHP 7.0为整数除法添加了一个新函数intdiv()。这个函数会返回一个整数,该整数代表了两个整数的商,它具有如下的语法。

int intdiv ( int $dividend , int $divisor )

创建名为int_div.php的脚本以使用intdiv()函数,我们在这里添加一些整数除法的样例。像PHP_INT_MAXPHP_INT_MIN这样的PHP常量可以用作该函数的参数。

  1. <?php
  2. var_dump(intdiv(4, 2));
  3. var_dump(intdiv(5, 3));
  4. var_dump(intdiv(-4, 2));
  5. var_dump(intdiv(-7, 3));
  6. var_dump(intdiv(4, -2));
  7. var_dump(intdiv(5, -3));
  8. var_dump(intdiv(-4, -2));
  9. var_dump(intdiv(-5, -2));
  10. var_dump(intdiv(PHP_INT_MAX, PHP_INT_MAX));
  11. var_dump(intdiv(PHP_INT_MIN, PHP_INT_MIN));
  12. ?>

如果运行脚本的话,将会得到如下所示的输出:

  1. int(2) int(1) int(-2) int(-2) int(-2) int(-1) int(2) int(2) int(1) int(1)

如果存在ArithmeticErrors的话,它将会输出到浏览器中。为了阐述这一点,添加如下的函数调用并再次运行脚本。

var_dump(intdiv(PHP_INT_MIN, -1));

在本例中,将会生成一个ArithmeticError表明PHP_INT_MIN除以-1的结果不是一个整数:

Uncaught ArithmeticError: Division of PHP_INT_MIN by -1 is not an integer

添加如下的函数调用到int_div.php脚本中并再次运行该脚本。

var_dump(intdiv(1, 0));

这次,将会抛出DivisionByZeroError

Uncaught DivisionByZeroError: Division by zero

新的会话选项

session_start函数可以用来开始一个新的会话(session)或恢复一个之前已经存在的会话。PHP 7.0添加了对名为options的新参数的支持,该参数是一个相关选项的数组,它们可以覆盖php.ini中的会话配置指令。在php.ini中,这些会话配置指令均以session.开头,但是在以函数入参的形式为session_start提供options参数数组的时候,session.前缀要省略。除了会话配置指令,还新增了一个read_and_close选项,如果该选项设置为TRUE,在读取之后,该会话会关闭,因为保持会话处于打开状态可能没有必要。作为样例,我们创建一个名为session_start.php的脚本并复制如下所示的代码清单。在样例脚本中,session_start(options)函数调用通过数组设置了一些配置指令:

  1. <?php
  2. session_start([
  3. 'name' => 'PHPSESSID',
  4. 'cache_limiter' => 'private',
  5. 'use_cookies' => '0'
  6. ]);

在运行脚本的时候,脚本中的会话配置选项将会覆盖掉php.ini中所定义的会话配置指令(如果存在的话)。该脚本不会生成任何输出。

在PHP 7.1中,如果session_start()开启会话失败的话,它将会返回FALSE并且不会初始化$_SESSION

使用回调执行正则表达式搜索和替换的新函数

PHP 7.0增加了一个新的函数preg_replace_callback_array(),以便于使用回调进行正则表达式搜索和替换。这个函数与preg_replace_callback()函数类似,只不过回调是基于每个模式调用的。函数的语法如下所示:

mixed preg_replace_callback_array ( array $patterns_and_callbacks , mixed $subject [, int $limit = -1 [, int &$count ]] )

如果$subject参数是一个数组的话,它会返回一个字符串组成的数组,如果$subject是字符串的话,它会返回一个字符串。如果匹配上了的话,那么返回的数组和字符串就是新的主题(subject),如果找不到匹配项的话,那么将会返回未改变的主题。该函数的参数如表3所示。

表3 preg_replace_callback_array的函数参数

参数 类型 描述
$patterns_and_callbacks 数组 声明相关的数组,匹配模式(键)与回调(值)
$subject mixed 声明要搜索和替换的字符串或字符串数组
$limit int 指定每个subject字符串中每个模式的最大替换数的限制。默认是-1,也就是没有限制。
&$count int 替换完成的数量,存储在$count变量中。

为了阐述这个新的功能,创建样例脚本prereg.php并复制如下的代码清单到脚本中。主题或要搜索的样例字符串设置成了'AAaaaaa Bbbbb'。patterns_and_callbacks参数设置成了寻找匹配'A'和'b'的数量。

  1. <?php
  2. $subject = 'AAaaaaa Bbbbb';
  3. preg_replace_callback_array(
  4. [
  5. '~[A]+~i' => function ($match) {
  6. echo strlen($match[0]), ' matches for "A" found', PHP_EOL;
  7. },
  8. '~[b]+~i' => function ($match) {
  9. echo strlen($match[0]), ' matches for "b" found', PHP_EOL;
  10. }
  11. ],
  12. $subject
  13. );
  14. ?>

如果运行脚本的话,匹配'A'和'b'的数量将会被打印出来:

7 matches for "A" found 5 matches for "b" found

生成加密安全的整数和字节的新函数

新增了两个生成加密安全的整数和字节的新函数。这些函数在表4中进行了讨论。

表4 新的加密函数

函数 语法 参数 返回值 描述
random_bytes() string random_bytes ( int $length ) int类型的$length,代表了以字节形式返回的任意字符串的长度。 返回一个字符串,其中包含了所请求数量的加密安全的随机字节。 生成和返回一个任意的字符串,包含了加密的随机字节。
random_int() int random_int ( int $min , int $max ) $min参数指定了返回值的下限,它必须等于或高于PHP_INT_MIN。$max参数指定了返回值的上限,它必须等于或低于PHP_INT_MAX。 加密的安全整数,这个数介于$min和$max之间。 生成和返回加密的任意整数。

作为样例,我们创建一个名为random_int.php的脚本,它会生成加密的任意整数。首先,生成一个范围在1和99之间的整数,然后生成一个范围在-100到0的整数。

  1. <?php
  2. var_dump(random_int(1, 99));
  3. var_dump(random_int(-100, 0));
  4. ?>

如果运行脚本的话,将会打印出两个整数。

int(98) int(-84)

生成的整数是随机的,如果相同的脚本再次运行的话,很可能会生成两个不同的整数。

接下来,创建另外一个样例脚本random_bytes.php,它会生成加密的任意字节,其长度为10。然后,我们使用bin2hex函数将任意的字节转换成ASCII字符串,其中包含了所返回字节的16进制字符串形式。复制如下的代码清单到脚本文件中:

  1. <?php
  2. $bytes = random_bytes(10);
  3. var_dump(bin2hex($bytes));
  4. ?>

运行脚本并生成加密的任意字节:

string(20) "ab9ad4234e7c6ceeb70d"

list()函数的修改

list()函数用来为一个变量列表像数组那样进行赋值。PHP 7.0和7.1为list()带来了一些变更。在PHP 7中,list()无法像之前的版本那样解包字符串,如果对字符串进行解包的话,将会返回NULL

在PHP 5.x中,使用list()解包一个字符串的时候,会将list()中的一个变量赋值为字符串中的值。我们使用PHP 5.x中的list()来阐述解包字符串。创建脚本list.php并复制如下的代码清单到脚本中。

  1. <?php
  2. $str = "aString";
  3. list($elem) = $str;
  4. var_dump($elem);

该脚本解包$str中的一个值到一个列表元素中。使用PHP 5.x运行该脚本,$elem的值会输出‘a’。如果你在PHP 7.0中再次运行该脚本的话,那么会输出NULL。另外一个无法在PHP 7中解包字符串的list()样例如下所示:

  1. <?php
  2. list($catalog) = "Oracle Magazine";
  3. var_dump($catalog);
  4. ?>

如果运行该脚本的话,与前面的脚本类似,我们会看到输出的值是NULL

实际上,在PHP 7.x中,如果list()要通过字符串进行赋值,那么必须要使用str_split函数:

  1. <?php
  2. $str = "aString";
  3. list($elem) = str_split($str);
  4. var_dump($elem);

如果运行上述样例的话,列表元素按照预期被设置成了‘a’。在PHP 7.0中,list()表达式不能完全为空。作为样例,运行如下list.php程序清单:

  1. <?php
  2. $info = array('Oracle Magazine', 'January-February 2018', 'Oracle Publishing');
  3. list(, , ) = $info;
  4. echo " \n";

这一次,会输出一条错误信息,表明“Cannot use empty list..”。列表中可以部分元素为空。在下面的代码清单中,list()中的一个元素为空:

  1. <?php
  2. $info = array('Oracle Magazine', 'January-February 2018', 'Oracle Publishing');
  3. list($name, $edition,) = $info;
  4. echo "$name latest edition is $edition.\n";
  5. ?>

如果运行该脚本的话,不会生成错误,并且会创建一个具有两个非空元素和一个空元素的列表。

Oracle Magazine latest edition is January-February 2018.

list()函数的另外一个修改就是按照变量定义的顺序为其进行赋值。在此之前,值是按照与定义相反的顺序进行赋值的。为了阐述之前的行为,使用PHP 5.x运行如下的list.php中的程序清单:

  1. <?php
  2. list($a[], $a[], $a[]) = ['A', 2, 3];
  3. var_dump($a);
  4. ?>

我们可以探测脚本的输出(参见图1),值是按照定义相反的顺序进行赋值的。

图1 值是按照相反的顺序赋值的

在PHP 7.0上运行相同的脚本,我们会发现list()会按照与定义相同的顺序为变量赋值:

array(3) { [0]=> string(1) "A" [1]=> int(2) [2]=> int(3) }

PHP 7.1.0支持在列表中指定key以表明赋值的数字顺序。作为样例,创建list.php脚本并包含如下的代码清单。注意,在本例中,所有的key都是数字:

  1. <?php
  2. list(0 => $journal, 1=> $publisher, 2 => $edition) = ['Oracle Magazine', 'Oracle Publishing', 'January February 2018'];
  3. echo "$journal, $publisher, $edition. \n";
  4. ?>

如果运行脚本的话,将会得到如下的列表值:

Oracle Magazine, Oracle Publishing, January February 2018.

赋值的数字顺序可以交叉,如下面的代码清单所示,索引0放在了索引1的后面。

  1. <?php
  2. list(1 => $journal, 0=> $publisher, 2=>$edition) = ['Oracle Magazine', 'Oracle
  3. Publishing', 'January February 2018'];
  4. echo "$journal, $publisher,$edition \n";
  5. ?>

如果运行脚本的话,输出的值表明变量是根据数字的key进行赋值的,而不是列表中key的顺序。

Oracle Publishing, Oracle Magazine,January February 2018

key的索引可以使用单括号或双括号括起来,如下所示:

  1. <?php
  2. list('1' => $journal, '0'=> $publisher, '2'=>$edition) = ['Oracle Magazine', 'Oracle
  3. Publishing', 'January February 2018'];
  4. echo "$journal, $publisher,$edition \n";
  5. ?>

上述脚本会生成与前面的脚本相同的输出。如果在list()中某个元素使用了key,那么它的所有元素都应该使用key。例如,创建一个列表,有些元素使用了key进行赋值,有些元素没有使用key:

  1. <?php
  2. list(0 => $journal, 1=> $publisher, 2) = ['Oracle Magazine', 'Oracle Publishing', 'January February 2018'];
  3. echo "$journal, $publisher. \n";
  4. ?>

如果运行该脚本的话,我们会看到一个错误信息,提示在赋值的时候,带有key和不带key的数组条目不能混合使用:

Cannot mix keyed and unkeyed array entries in assignments

字符串偏移支持负数

从PHP 7.1开始,像strpossubstr这样的字符串操作函数引入了对负数偏移量的支持,也就是从字符串的结尾处开始处理偏移。使用[]和{}的字符串索引也支持负数偏移量。例如,"ABC"[-2]将会返回字母'B'。现在,我们创建一个脚本str-negative-offset.php并复制如下的代码清单到脚本中:

  1. <?php
  2. echo "ABCDEF"[-1];
  3. echo "<br/>";
  4. echo strpos("aabbcc", "a", -6);
  5. echo "<br/>";
  6. echo strpos("abcdef", "c", -1);
  7. echo "<br/>";
  8. echo strpos("abcdef", "c", -5);
  9. echo "<br/>";
  10. echo substr("ABCDEF", -1);
  11. echo "<br/>";
  12. echo substr("ABCDEF", -2, 5);
  13. echo "<br/>";
  14. echo substr("ABCDEF", -7);
  15. echo "<br/>";
  16. echo substr("ABCDEF", -6);
  17. echo "<br/>";
  18. echo substr("ABCDEF", -5);
  19. echo "<br/>";
  20. echo substr("ABCDEF", 6);
  21. echo "<br/>";
  22. echo substr("abcdef", 1, -3);
  23. echo "<br/>";
  24. echo substr("abcdef", 3, -2);
  25. echo "<br/>";
  26. echo substr("abcdef", 4, -1);
  27. echo "<br/>";
  28. echo substr("abcdef", -5, -2);
  29. ?>

该脚本提供了多个在字符串函数和[]中使用负数偏移量的样例。如果运行脚本的话,它将会输出:

  1. F
  2. 0
  3. 2
  4. F
  5. EF
  6. ABCDEF
  7. ABCDEF
  8. BCDEF
  9. bc
  10. d
  11. e
  12. bcd

将回调转换成闭包的新函数

闭包用来以字符串变量的形式传递函数(用户自定义的函数以及除语言构造之外的内置函数)和方法。例如,函数hello()可以以参数的形式传递给另外一个函数,或者使用函数名字以字符串的形式从另外一个函数中返回,如'hello',当然这样做的前提是参数类型/返回类型为callable。我们创建一个样例脚本callable.php并声明函数hello(),该函数输出一个‘hello’消息。声明另外函数,其参数类型是callable。

  1. function callFunc(callable $callback) {
  2. $callback();
  3. }

callFunc(callable)函数能够以字符串的形式通过hello()的名字调用该函数:

callFunc("hello");

另外,内置的call_user_func ( callable $callback [, mixed $... ] )函数以callable作为其第一个参数,也可以用来根据名字调用hello()函数:

call_user_func('hello');

脚本callable.php如下所示:

  1. <?php
  2. function hello() {
  3. echo 'hello';
  4. }
  5. call_user_func('hello');
  6. function callFunc(callable $callback) {
  7. $callback();
  8. }
  9. echo '<br/>';
  10. callFunc("hello");

如果运行脚本的话,会根据所提供的名称调用hello()函数。

hello

hello

闭包是匿名函数的对象表示形式。那为什么要将回调转换成闭包呢?有多个原因,其中一个就是性能。回调类型相对比较慢,因为确定一个函数是否为回调需要一定的成本。

使用回调的另外一个缺点在于,只有public的函数可以用作回调。相反,将类中的函数转换成闭包并不需要该函数是public的,例如该函数可以声明为private。作为样例,我们创建一个脚本hello.php并声明一个类Hello,该类中包含返回一个回调函数的方法getCallback()

  1. public function getCallback() {
  2. return [$this, 'hello_callback_function'];
  3. }

回调函数声明为public。

public function hello_callback_function($name) { var_dump($name); }

创建该类的一个实例并调用回调函数。hello.php脚本如下所示:

  1. <?php
  2. class Hello {
  3. public function getCallback() {
  4. return [$this, 'hello_callback_function'];
  5. }
  6. public function hello_callback_function($name) { var_dump($name); }
  7. }
  8. $hello = new Hello();
  9. $callback = $hello-> getCallback();
  10. $callback('Deepak');

如果运行脚本的话,会得到如下的输出:

string(6) "Deepak"

接下来,使用Closure::fromCallable静态方法将私有的回调函数转换成一个闭包。

  1. <?php
  2. class Hello {
  3. public function getClosure() {
  4. return Closure::fromCallable([$this, 'hello_callback_function']);
  5. }
  6. private function hello_callback_function($name) { var_dump($name); }
  7. }
  8. $hello = new Hello();
  9. $closure = $hello-> getClosure();
  10. $closure('Deepak');

如果运行脚本的话,会得到相同的输出:

string(6) "Deepak"

转换成闭包的另外一个原因在于能够在早期探测到错误,不必推迟到运行期。考虑如上面所示的样例,但是这一次我们故意把函数名称拼错:

  1. public function getCallback() {
  2. return [$this, 'hello_callback_functio'];
  3. }

如果运行这个脚本的话,当回调函数在如下的语句实际执行的时候,将会抛出Call to undefined method Hello::hello_callback_functio()错误:

$callback('Deepak');

相反,如果我们将回调转换成闭包,错误Failed to create closure from callable: class 'Hello' does not have a method 'hello_callback_function'会在如下这行代码中就能探测出来:

return Closure::fromCallable([$this, 'hello_callback_functio']);

JSON_THROW_ON_ERROR标记

在PHP 7.3版本之前,对JSON函数json_encode()和json_decode()的错误处理功能都是非常少的,有如下的不足之处:

PHP 7.3在json_encode()json_decode()方法中添加了对JSON_THROW_ON_ERROR标记的支持。添加了新的异常子类JsonException,用来描述JSON解码/编码。如果为json_encode()json_decode()提供JSON_THROW_ON_ERROR标记并抛出了JsonException异常的话,那么全局的错误状态不会被修改。为了阐述新的JSON_THROW_ON_ERRORJsonException,我们创建一个json.php脚本,并尝试使用json_decode解码一个包含错误的数组:

  1. <?php
  2. try {
  3. $json = '{"a":1,"b":2,"c":3,"d":4,"e":5}';
  4. json_decode("{",false,1,JSON_THROW_ON_ERROR);
  5. }
  6. catch (\JsonException $exception) {
  7. echo $exception->getMessage(); // echoes "Syntax error"
  8. }
  9. ?>

如果运行脚本的话,我们会看到如下所示的JsonException

Maximum stack depth exceeded

作为使用JSON_THROW_ON_ERRORjson_encode()的样例,我们编码一个数组,该数组中包含一个值为 NAN 的元素,如下面的程序清单所示:

  1. <?php
  2. try {
  3. $arr = array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => NAN);
  4. echo json_encode($arr,JSON_THROW_ON_ERROR);
  5. }
  6. catch (\JsonException $exception) {
  7. echo $exception->getMessage();
  8. }
  9. ?>

当运行脚本的时候,会输出如下的信息:

Inf and NaN cannot be JSON encoded

从数组中获取第一个和最后一个key值的新函数

从数组中获取第一个和最后一个key值是很常见的操作,PHP 7.3专门新加了两个函数:

  1. $key = array_key_first($array);
  2. $key = array_key_last($array);

如下的代码清单给出了在相关数组甚至空数组中使用这两个函数的样例:

  1. <?php
  2. // 在数组中的应用
  3. $array = ['a' => 'A', 2 => 'B', 'c' => 'C'];
  4. $firstKey =array_key_first($array);
  5. $lastKey = array_key_last($array);
  6. echo assert($firstKey === 'a');
  7. echo "<br/>";
  8. echo $firstKey;
  9. echo "<br/>";
  10. echo $lastKey;
  11. echo "<br/>";
  12. // 在空数组中的应用
  13. $array = [];
  14. $firstKey = array_key_first($array);
  15. $lastKey = array_key_last($array);
  16. echo "<br/>";
  17. echo assert($firstKey === null);
  18. echo "<br/>";
  19. echo assert($lastKey === null);
  20. ?>

脚本的输入如下所示:

  1. 1
  2. a
  3. c
  4. 1
  5. 1

使用Compact函数报告未定义的变量

compact()函数在PHP 7.3中有一个新的特性,那就是报告未定义的变量。为了阐述该功能,运行如下的脚本,该脚本中包含了一些未定义的变量:

  1. <?php
  2. $array1=['a','b','c',];
  3. $var1="var 1";
  4. var_dump(compact($array1,$array2,'var1','var2'));
  5. ?>

脚本将会输出如下的信息:

  1. Notice: Undefined variable: array2 on line 9
  2. Notice: compact(): Undefined variable: a on line 9
  3. Notice: compact(): Undefined variable: b on line 9
  4. Notice: compact(): Undefined variable: c on line 9
  5. Notice: compact(): Undefined variable: var2 on line 9

函数调用中的拖尾逗号

PHP 7.3添加了在函数调用时使用拖尾逗号的支持。拖尾逗号在有些经常追加参数的场景中是很有用处的,比如可变参数的函数(array_mergecompactsprintf)。语言构造unset()isset()也支持拖尾逗号。如下的样例使用unset函数阐述了拖尾逗号:

  1. <?php
  2. function funcA($A,$B,$C)
  3. {
  4. unset($A,$B,$C,);
  5. echo $A;
  6. echo $B;
  7. echo $C;
  8. }
  9. $A = 'variable A';
  10. $B = 'variable B';
  11. $C = 'variable C';
  12. funcA($A,$B,$C,);
  13. ?>

运行脚本,将会产生如下的输出:

  1. Notice: Undefined variable: A
  2. Notice: Undefined variable: B
  3. Notice: Undefined variable: C

array_merge()函数是另外一个可以借助拖尾逗号简化追加值的样例。如下的脚本使用在对array_merge()的函数调用中使用了拖尾逗号:

  1. <?php
  2. $array1=[1,2,3];
  3. $array2=['A','B','C'];
  4. $array = array_merge(
  5. $array1,
  6. $array2,
  7. ['4', '5'],
  8. );
  9. ?>

方法调用和闭包也允许使用拖尾逗号。在类中,方法就是一个函数。闭包是表示匿名函数的一个对象。拖尾逗号只能用于函数调用,不能用于函数声明。自由位置的逗号、前导逗号和多个拖尾逗号在该语言中是禁止使用的。

数学函数bcscale

bcscale函数的语法是int bcscale ([ int $scale ]),它能够为所有后续的bc数学函数调用设置默认的小数位数。像bcadd()bcdiv()bcsqrt()这样的bc数学函数能够用于任意精度的数字计算。PHP 7.3添加了使用bcscale获取当前小数位数的支持。设置bcscale之后会返回旧的小数位数。作为样例,如下的脚本将默认的小数位数设置为3,随后输出了当前的小数位数:

  1. <?php
  2. bcscale(3);
  3. echo bcscale();
  4. ?>

上述脚本的输出是3。

新函数is_countable

PHP 7.3添加了新函数is_countable,如果函数参数为array类型或Countable实例的话,它会返回true。

bool is_countable(mixed $var)

例如,is_countable()能够用来判断给定的参数是不是数组。

echo is_countable(['A', 'B', 3]);

ArrayIterator是可数的,is_countable会输出TRUE,因为ArrayIterator实现了Countable接口。

echo is_countable(new ArrayIterator());

is_countable<small>可以与if()一起使用,确保某个参数时可数的,然后再运行后续的代码。在如下的代码片段中,我们测试了类A的实例是不是可数的:

  1. class A{}
  2. if (!is_countable(new A())) {
  3. echo "Not countable";
  4. }

上述代码片段的结果是FALSE,因为类A并没有实现Countable。如果is_countable的参数是一个数组的话,那么它将会返回TRUE,如下面的代码片段所示:

  1. $array=['A', 'B', 3];
  2. if (is_countable($array)) {
  3. var_dump(count($array));
  4. }

本节所有的代码片段均放到了is_countable.php脚本中。

  1. <?php
  2. class A{}
  3. echo is_countable(['A', 'B', 3]);
  4. echo "<br/>";
  5. echo is_countable(new ArrayIterator());
  6. echo "<br/>";
  7. if (!is_countable(new A())) {
  8. echo "Not countable";
  9. }
  10. echo "<br/>";
  11. $array=['A', 'B', 3];
  12. if (is_countable($array)) {
  13. var_dump(count($array));
  14. }
  15. ?>

运行该脚本,输出如下所示:

  1. 1
  2. 1
  3. Not countable
  4. int(3)

箭头函数

PHP 7.4引入了箭头函数,从而使匿名函数的语法更加简洁。箭头函数的形式如下所示:

fn(parameter_list) => expr

箭头函数具有最低的执行优先级,这意味着箭头 =>右边的表达式会在箭头函数之前执行,例如,箭头函数fn($x) => $x + $y等价于fn($x) => ($x + $y),而不是(fn($x) => $x) + $y。在外围作用域中声明的且被表达式中使用的变量是隐式按值捕获的。作为样例,考虑如下的脚本,它声明了一个箭头函数:

$fn1 = fn($msg) => $msg.' '.$name;

变量$name会自动从封闭范围捕获,上述的箭头函数等价于:

  1. $fn2 = function ($msg) use ($name) {
  2. return $msg.' '.$name;
  3. };

对$x的按值绑定等价于对箭头函数中每次出现$x均执行use($x)。箭头函数还声明了一个参数$msg。在下一个样例中,var_export调用箭头函数并提供一个参数,输出值为'Hello John':

  1. <?php
  2. $name = "John";
  3. $fn1 = fn($msg) => $msg.' '.$name;
  4. var_export($fn1("Hello"));//'Hello John'
  5. ?>

箭头函数可以进行嵌套,如下面的脚本所示。外层的脚本函数按值捕获变量$name,内层的箭头函数从外层函数捕获$name

  1. <?php
  2. $name = "John";
  3. $fn = fn($msg1) => fn($msg2) => $msg1.' '.$msg2.' '.$name;
  4. var_export($fn("Hello")("Hi"));
  5. ?>

当脚本运行的时候,输出入图2所示。

图2 箭头函数可以进行嵌套

因为箭头函数使用按值的变量绑定,修改箭头函数中变量的值不会影响外层作用域中的值。为了阐述这一点,如下脚本中的箭头函数递减了外层代码块中变量x的值并没有影响,它依然是1

  1. <?php
  2. $x = 1
  3. $fn = fn() => x--; // 没有影响
  4. $fn();
  5. var_export($x); //输出 1
  6. ?>

箭头函数支持任意的函数签名,可以包含参数和返回类型、默认值、可变参数以及按引用的变量传递和返回。如下的脚本阐述了箭头函数不同形式签名的使用。脚本中签名描述和输出通过注释//进行展示。

  1. <?php
  2. $name = "John";
  3. $x = 1;
  4. //包括参数类型、返回类型和默认值的箭头函数 and default value
  5. $fn = fn(string $msg='Hi'): string => $msg.' '.$name;
  6. //包括可变参数的箭头函数
  7. $fn2 = fn(...$x) => $x;
  8. //包括按引用参数传递的箭头函数
  9. $fn3=fn(&$x) => $x++;
  10. $fn3($x);
  11. echo $x; // 2
  12. var_export($fn("Hello"));//'Hello John'
  13. var_export($fn());//'Hi John'
  14. var_export($fn2(1,2,3)); //array ( 0 => 1, 1 => 2, 2 => 3, )
  15. ?>

箭头函数的对象上下文中可能会使用$this。如果与带有static前缀的箭头函数一起使用,那么就不能使用$this。为了阐述这一点,考虑如下的脚本,它在对象上下文和类上下文中使用了$this。如果不在对象上下文中使用的话,将会输出错误信息。

  1. <?php
  2. class A {
  3. public function fn1() {
  4. $fn = fn() => var_dump($this);
  5. $fn(); // object(A)#1 (0) { }
  6. $fn = static fn() => var_dump($this);
  7. $fn(); //Uncaught Error: Using $this when not in object context
  8. }
  9. }
  10. $a=new A();
  11. $a->fn1();

总结

在该系列关于PHP 7新特性的第四篇(也是倒数第二篇)文章中,我们讨论了关于PHP函数的新特性。

在本系列的下一篇,也就是最后一篇中,我们将会讨论关于数组、操作符、常量和异常处理方面的新特性。

关于作者

Deepak Vohra是一位Sun认证的Java程序员和Sun认证的Web组件开发人员。Deepak在WebLogic Developer's Journal、XML Journal、ONJava、java.net、IBM developerWorks、Java Developer’s Journal、Oracle Magazine和devx上都发表过Java和Java EE相关的技术文章。Deepak还出版过五本关于Docker的书,他是Docker导师。Deepak还发表了多篇关于PHP的文章,以及一本面向PHP和Java开发人员的Ruby on Rails图书。

查看英文原文:Article: PHP 7 – Functions Improvements

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注