PHP 8.0 新特性一览

自 2015 年 12 月 3 日 PHP 7.0 发布以来,PHP 已经有五年的时间没有更新过大版本号了。而就在最近几天,PHP 社区终于发布了下一个大版本——PHP 8.0。PHP 8.0 相比之前的版本有哪些更改呢?PHP 8.0 有哪些新特性呢?下面就带大家来看一看。

主要更改

PHP 8.0 包括了许多提升与改进,其中全新增加的特性有:

另外,PHP 8.0 中还有一些容易被大家忽略的细微更改

联合体(Union)类型

PHP 8.0 为用户带来了全新的联合体(Union)类型。传统上,参数的接收者是无法指定多个类型作为可选接收类型的。在下例中,如果接收者允许接收 floatint 类型,传统的代码应当书写如下:

class Number {
  /** @var int|float */
  private $number;

  /**
   * @param float|int $number
   */
  public function __construct($number) {
    $this->number = $number;
  }
}

new Number('NaN'); // Ok

然而,在新版本的 PHP 代码中,我们只需使用 | 操作符分隔不同变量类型:

class Number {
  public function __construct(
    private int|float $number
  ) {}
}

new Number('NaN'); // TypeError

只要不是单独出现,null 是可以放置于联合体定义中的。比如接收者允许接收空值时,上例可相应改为:int|float|null。另外有趣的一点是,包括 strpos() 在内的许多函数可能返回 false 值作为错误指示,因此联合体定义是允许出现 false 关键字的。

参见文档:类型定义(PHP 8.0)

具名参数

期待多年的具名参数(Named arguments)特性终于出现在 PHP 8.0 中了。废话不多说,我们一起来看看示例吧:

htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);

在 PHP 8.0 中,上例写作:

htmlspecialchars($string, double_encode: false);

这样一来,当我们要更改 htmlspecialchars() 函数的第四个参数时,我们在调用时直接指定参数的名称并赋值即可。真是喜大普奔。

参见文档:函数参数(PHP 8.0)

match 表达式

作为 PHP 8.0 中新引入的表达式,match 表达式有着许多与 switch 语句类似的特点。相比 switch 语句,match 表达式提供了以下三种特性:

  • match 是一种表达式,意味着其值可以被赋值至变量中或者作为返回值返回
  • match 的分支仅支持单行,且不需要 break;
  • match 使用严格比较

在进行一些简单的取值判断中,传统的代码我们会这样写:

switch (8.0) {
  case '8.0':
    $result = "Oh no!";
    break;
  case 8.0:
    $result = "This is what I expected";
    break;
}
echo $result;
//> Oh no!

而得益于 match 表达式,在 PHP 8.0 中我们将上述代码改写为:

echo match (8.0) {
  '8.0' => "Oh no!",
  8.0 => "This is what I expected",
};
//> This is what I expected

由于 match 是一个表达式,其末尾需要使用分号 ; 作结。

参见文档:match 表达式(PHP 8.0)

属性

社区已经为 PHP 中实现属性(Attributes)讨论很长一段时间了。如今我们可以使用 PHP 原生的语法标注元信息,而非使用 PHPDoc 版本的标注。

下述代码:

class PostsController
{
    /**
     * @Route("/api/posts/{id}", methods={"GET"})
     */
    public function get($id) { /* ... */ }
}

在 PHP 8.0 中已经修改为如下所示:

class PostsController
{
    #[Route("/api/posts/{id}", methods: ["GET"])]
    public function get($id) { /* ... */ }
}

构造函数属性提升

还记得在先前版本 PHP 中编写一个类的构造函数时类似于下面的痛苦吗:

class Point {
  public float $x;
  public float $y;
  public float $z;

  public function __construct(
    float $x = 0.0,
    float $y = 0.0,
    float $z = 0.0,
  ) {
    $this->x = $x;
    $this->y = $y;
    $this->z = $z;
  }
}

在 PHP 8.0 中,我们可以在构造函数的参数列表中直接声明类的成员变量,并将其赋值:

class Point {
  public function __construct(
    public float $x = 0.0,
    public float $y = 0.0,
    public float $z = 0.0,
  ) {}
}

太方便了,不是吗!

参见文档:构造函数属性提升(PHP 8.0)

null 值安全的操作符

如果我们要安全解析一个成员可能不存在(为 null)的类型时,传统方法我们会这样做:

$country =  null;

if ($session !== null) {
  $user = $session->user;

  if ($user !== null) {
    $address = $user->getAddress();
 
    if ($address !== null) {
      $country = $address->country;
    }
  }
}

然而在新的 PHP 8.0 版本中,通过新引入的 ?-> 操作符,上述代码可以很容易地改写为:

$country = $session?->user?->getAddress()?->country;

弱映射

弱映射(Weak Maps)拓展了自 PHP 7.4 中引入的弱引用的概念。弱引用的意思是允许一个对象在不增加其引用计数器的情况下被引用。而弱映射则允许创建从对象到任意值的映射,而不会阻止对用作键的对象进行垃圾回收。只要将对象添加到 WeakMap 中,GC 在触发条件时就可以将其占用内存回收。

每当一个变量被删除时,PHP 会检查是否有其他变量仍然引用该对象。如果没有,它就知道可以安全地删除这个对象了。

PHP 中的垃圾回收机制简述

它的一般用途是将数据与单个对象实例相关联,而不会强迫它们保持活动状态,从而有效地在长时间运行的进程中泄漏内存。例如,弱映射可以用来记忆计算结果。

即时编译

在 PHP 8.0 发布的这些特性中,最最重大的特性要属即时编译(Just-In-Time Compilation,JIT)莫属。我们都已经了解自 PHP 7.0 版本以来,随着 Zend VM 引擎的优化以及核心数据结构方面的更改,PHP 的执行性能得到了数倍提升。然而事实上由于托管代码的还是 Zend 虚拟机,PHP 的性能不太可能在这样的架构中再有大幅优化。

而 PHP 8.0 中 JIT 特性的引入,使得 PHP 代码第一次地能够以本地机器码的方式在计算机中得到执行。这得益于 JIT 将 Zend 虚拟机生成的指令视为中间代码,而非最终表述。我们惊喜地发现通过本地码执行的 PHP 代码相较 Zend 虚拟机执行的版本速度快了一倍以上(详见性能对比),而性能则是 PHP 5.4 的四倍。

一些细微更改

PHP 8.0 中还有一些在其他网站上容易被忽略的细微改进,我们将其列举如下;

在 PHP 8.0 版本进行数字与字符串类型的比较操作时,除非将包含数字的字符串参与比较,否则 PHP 会将数字先转换为字符串,然后进行字符串比较操作。比如下述代码:

0 == 'foobar'

在 PHP 7 中得到 true,而在 PHP 8 中得到 false 值。网上有人开玩笑地提到这相当于填补了 PHP 8.0 中的弱比较类型安全缺陷。

特别感谢

我们特别感谢惠新宸老师特别是在 JIT 部分,为 PHP 8.0 所做的贡献,大家可以读读他的《写在PHP8发布之前的话》。