PHP 7.4虽然还没有正式发布,但是RC版本已经出来了,社区里讨论得很热烈。类型属性、箭头函数、预加载、数组展开运算符这些新特性都很让人期待。
作为一个用了很多年PHP的开发者,我对PHP 7.4的新特性也很关注,最近研究了一下RC版本和源码,有了一些理解和思考。今天这篇文章就来深入剖析一下PHP 7.4的新特性,从语法到底层原理到性能影响,一步步带你深入理解。
先说明一下,PHP 7.4目前还在开发中,正式版预计2019年底发布,本文基于当前的RC版本和RFC,正式版可能会有一些变化,具体以正式版为准。
一、PHP 7.4的整体变化
先说说PHP 7.4的整体定位。PHP 7.0引入了Zend Engine 3,性能有了巨大提升,比PHP 5.6快了一倍甚至更多,同时引入了标量类型声明、返回类型声明、匿名类等新特性。PHP 7.1、7.2、7.3都是在7.0的基础上不断完善,增加了可空类型、void返回类型、类常量可见性、对象类型等,性能也在持续优化。
PHP 7.4是PHP 7.x系列的倒数第二个版本,下一个是PHP 7.5,然后就是PHP 8.0。它的变化很大,不仅增加了很多开发者期待已久的新特性,比如类型属性、箭头函数、预加载,而且在性能上也有不小提升,尤其是预加载特性能让PHP的性能再上一个台阶。
同时PHP 7.4也在为PHP 8.0做准备,很多PHP 8.0的特性会先在7.4里预览或者打下基础,比如JIT虽然是PHP 8.0的特性,但是7.4的一些优化是在为JIT做准备。
总的来说,PHP 7.4是一个很重要的版本,它的新特性能让我们写代码更方便更优雅,也能让PHP的性能更好,同时推动PHP朝着更现代化、更类型安全的方向发展。
接下来一个个来看新特性。
二、新特性一:类型属性(Typed Properties)
类型属性是PHP 7.4最重要的新特性之一,也是很多开发者期待已久的。
在PHP 7.4之前,类的属性不能声明类型,只能在注释里写@var来说明类型,但那只是注释,不是真正的类型声明,PHP不会做类型检查,你可以给它赋任意类型的值。
PHP 7.4引入了类型属性,你可以给类的属性声明类型,PHP会在运行时做类型检查,如果赋了错误类型的值会抛出TypeError。
举个例子:
class User {
public int $id;
public string $name;
public ?string $email;
public array $hobbies = [];
}
$user = new User();
$user->id = 123; // 正确
$user->name = "张三"; // 正确
$user->id = "abc"; // 报错,TypeError类型属性支持所有PHP类型,包括标量类型(int、float、string、bool)、复合类型(array、object、callable、iterable)、类和接口,以及可空类型(?Type)和self、parent等。
但是也有一些限制:不能用void类型(void只能用于函数返回类型);不能用callable类型(因为callable和数组、字符串有重叠,会有问题);类型属性在访问之前必须有值,不然会报错(除非声明为可空类型);不支持引用赋值(会绕过类型检查)。
底层原理
类型属性的底层实现是在Zend Engine里给类的属性增加了类型信息。属性声明的时候解析类型,存到zendpropertyinfo结构里。给属性赋值的时候调用zendcheckproperty_type函数做类型检查,如果类型不匹配就抛出TypeError。
同时类型属性也支持类型转换,比如给int类型的属性赋数字字符串"123",PHP会自动转换成int 123,不会报错。这和PHP的标量类型声明一致,默认是弱类型检查。如果开启了严格模式(declare(strict_types=1)),就不会自动转换,类型不匹配会报错。
性能影响
类型属性对性能的影响很小,因为类型检查只在赋值的时候做一次,而且检查逻辑很简单。同时因为有了类型信息,PHP的opcode cache和未来的JIT可以做更多优化,比如根据属性类型优化属性访问和内存布局。长期来看类型属性对性能是有好处的。
类型属性是PHP朝着更类型安全方向发展的重要一步,能让代码更健壮,减少bug,也能让IDE和静态分析工具更好地理解代码。强烈建议在PHP 7.4之后尽量给属性加上类型声明。
三、新特性二:箭头函数(Arrow Functions)
箭头函数也是PHP 7.4很重要的新特性,很多开发者期待已久。
在PHP 7.4之前,PHP的匿名函数写起来比较麻烦,要写function关键字,还要用use关键字捕获外部变量。比如:
$factor = 10;
$nums = [1, 2, 3];
$result = array_map(function($n) use ($factor) {
return $n * $factor;
}, $nums);PHP 7.4引入了箭头函数,写起来很简洁,而且会自动捕获外部变量,不需要use关键字:
$factor = 10;
$nums = [1, 2, 3];
$result = array_map(fn($n) => $n * $factor, $nums);箭头函数的语法是fn(参数列表) => 表达式。它的特点是:函数体只能有一个表达式,表达式的结果就是返回值,不需要写return;自动按值捕获外部变量,不需要use关键字;支持类型声明和默认参数;可以用在任何匿名函数能用的地方。
限制是:只能有一个表达式,不能有多个语句;不能返回引用;不能是生成器函数(不能用yield);自动捕获是按值的,不是按引用,所以不能在箭头函数里修改外部变量。
底层原理
箭头函数的底层实现和普通匿名函数一样,都是闭包,只是语法更简洁,而且自动捕获外部变量。PHP在编译箭头函数的时候会把它转换成普通闭包,自动把外部用到的变量通过use按值捕获。所以本质上箭头函数就是语法糖,底层和普通闭包一样,没有额外的性能开销。
为什么只支持一个表达式
很多人会问为什么箭头函数只支持一个表达式,不能像JavaScript那样支持语句块。这是因为PHP的箭头函数设计目标是简单的单行回调函数,这种场景在PHP里非常常见,比如arraymap、arrayfilter等。如果支持多个语句,就会涉及变量捕获是按值还是按引用、return怎么写等复杂问题,设计会变复杂。而且普通匿名函数已经能支持多个语句了,所以箭头函数专注于简单的单行场景,保持简单简洁。
箭头函数是一个非常实用的特性,能让代码更简洁更优雅,尤其是写回调函数的时候能少写很多样板代码。
四、新特性三:有限返回类型协变和参数类型逆变
这是PHP 7.4在类型系统上的另一个重要改进。
在PHP 7.4之前,PHP的类型继承是不变的(invariant),子类的方法参数类型和返回类型必须和父类完全一样,不能有变化。即使子类的类型是父类类型的子类也不行。
PHP 7.4引入了有限的返回类型协变和参数类型逆变。也就是说子类的方法返回类型可以是父类方法返回类型的子类(协变),参数类型可以是父类方法参数类型的父类(逆变)。这更符合面向对象的里氏替换原则。
举个协变的例子:
class Animal {}
class Dog extends Animal {}
interface AnimalShelter {
public function adopt(string $name): Animal;
}
class DogShelter implements AnimalShelter {
public function adopt(string $name): Dog { // 返回类型Dog是Animal的子类,协变
return new Dog();
}
}逆变的例子:
interface Consumer {
public function eat(Dog $dog);
}
class AnimalConsumer implements Consumer {
public function eat(Animal $animal) { // 参数类型Animal是Dog的父类,逆变
// ...
}
}目前的协变和逆变是有限的:只支持类和接口类型,不支持标量类型;只支持非可空到可空的变化;只支持单个参数的逆变。这些限制可能会在未来版本中逐步放开。
底层原理
底层实现是在检查子类方法和父类方法是否兼容的时候,放松了类型检查的规则。之前是严格要求完全一样,现在允许返回类型协变、参数类型逆变。同时这也在为未来的泛型支持打基础,因为协变和逆变是泛型的重要组成部分。
这个特性能让PHP的面向对象更符合里氏替换原则,代码更灵活更类型安全。
五、新特性四:空合并赋值运算符(Null Coalescing Assignment Operator)
这是一个很小但很实用的语法糖。
PHP 7.0引入了空合并运算符??,能方便地判断变量是否存在且不为null:
$username = $_GET['username'] ?? '默认值';但是有时候我们需要给变量赋值,如果它还没有值或者是null:
$array['key'] = $array['key'] ?? '默认值';PHP 7.4引入了空合并赋值运算符??=,写起来更简洁:
$array['key'] ??= '默认值';意思是如果$array['key']不存在或者是null,就把'默认值'赋给它,否则保持原来的值。
底层原理
底层就是语法糖,编译的时候把$a ??= $b转换成$a = $a ?? $b,然后编译成对应的opcode。和空合并运算符一样是短路的,只有左边的变量不存在或者是null的时候才会计算右边的表达式。
这个特性虽然小,但是很实用,能让代码更简洁,尤其是处理默认值的时候。
六、新特性五:数组展开运算符(Spread Operator in Array)
PHP 5.6引入了参数的展开运算符...(可变参数和参数解包),但是之前只能用在函数参数里,不能用在数组里。PHP 7.4引入了数组展开运算符,可以在数组里用...把一个数组的元素展开到当前数组里。
比如:
$array1 = [1, 2, 3];
$array2 = [...$array1, 4, 5, 6]; // [1, 2, 3, 4, 5, 6]可以在数组的任何位置用展开运算符,也可以用多个展开运算符。
限制是:只能展开数组和Traversable对象;不会展开字符串键(只处理数字键的元素,字符串键的元素会被忽略或者报错);是按值展开不是按引用。
底层原理
底层是在编译数组字面量的时候,如果遇到展开运算符,就生成对应的opcode,运行时把要展开的数组元素一个个复制到新数组里。比调用array_merge函数更快,因为没有函数调用开销,而且能更好地预分配内存。
根据基准测试,数组展开运算符比arraymerge快大约10%到20%。建议在PHP 7.4之后尽量用数组展开运算符代替arraymerge。
七、新特性六:预加载(Preloading)
预加载是PHP 7.4最重要的性能相关新特性,也是很多关注PHP性能的开发者期待已久的。
我们知道PHP是解释型语言,每次请求的时候PHP都会加载、解析、编译PHP文件,然后执行编译后的opcode。虽然有OPcache能缓存编译后的opcode,避免每次重新编译,但是即使有OPcache,每次请求还是需要把这些opcode加载到当前请求的内存空间里,还是有一些开销。对于框架和大型应用,有很多文件,每次请求都要加载很多文件的opcode,开销不小。
预加载就是为了解决这个问题。它能让我们在PHP服务启动的时候,就把一些PHP文件加载、解析、编译,然后把编译后的opcode和类、函数等永久存在内存里,所有请求都能直接使用这些预加载的类和函数,不需要每次请求都重新加载。这样能大大提高PHP的性能。
预加载的使用很简单,写一个PHP脚本,用opcachecompilefile或者require加载要预加载的文件,然后在php.ini里配置opcache.preload指向这个脚本。PHP服务启动的时候就会执行这个脚本,把里面的文件预加载到内存里。
比如预加载脚本preload.php:
<?php
opcache_compile_file('/path/to/framework/Core.php');
opcache_compile_file('/path/to/framework/Router.php');
// ... 其他文件php.ini配置:
opcache.enable=1
opcache.enable_cli=1
opcache.preload=/path/to/preload.php
opcache.preload_user=www-data预加载的特点是:永久加载,直到PHP服务重启;支持类、函数、常量;能预加载整个框架的核心文件;需要PHP-FPM或者类似的常驻进程SAPI(CLI模式预加载没有意义,因为每次执行都是新进程)。
限制和注意事项:预加载的文件不能被修改,修改后需要重启PHP服务才能生效;预加载会占用内存,不要预加载太多不需要的文件;预加载的文件不能有条件性的定义;需要开启OPcache。
底层原理
预加载是OPcache的扩展功能。OPcache本来就会把编译后的opcode缓存到共享内存里,预加载就是在服务启动的时候主动把一些文件编译存到OPcache的共享内存里,并且标记为永久的不会被淘汰。所有请求都能直接使用这些预加载的opcode,不需要重新编译,也不需要重新加载到当前请求的内存空间里。
而且预加载不仅缓存opcode,还把类、函数等提前加载到内存里,请求的时候不需要再加载这些类和函数,直接就能用。这比单纯的opcode cache更进一步,性能提升更大。
性能影响
预加载对性能的提升很明显,尤其是对于框架和大型应用。根据基准测试和实际项目测试,预加载能让PHP应用的性能提升大约10%到30%甚至更多,取决于应用的大小和文件数量。应用越大文件越多,预加载的提升越明显。同时预加载还能减少每次请求的内存使用,因为预加载的类和函数是所有请求共享的。
预加载是PHP 7.4最重要的性能特性,强烈建议在生产环境开启,把框架的核心文件和应用的常用文件预加载,能获得明显的性能提升。当然要合理配置,不要预加载太多不需要的文件,修改预加载的文件后要重启PHP服务。
八、其他新特性
除了上面几个重要的新特性,PHP 7.4还有一些其他的新特性和改进。
1. 数值文字分隔符(Numeric Literal Separator)
可以在数值字面量里用下划线作为分隔符,提高大数字的可读性:
$price = 1_000_000; // 相当于1000000
$pi = 3.141_592_653; // 相当于3.141592653下划线只是为了可读性,PHP解析的时候会忽略。
2. 弱引用(Weak References)
引入了WeakReference类,弱引用不会阻止对象被垃圾回收。适合缓存等场景,当内存不够的时候对象能被回收,不会因为缓存一直占用内存。
3. 从方法中抛出异常
允许在toString方法里抛出异常。之前的版本里toString方法不能抛出异常,因为__toString是在把对象转换成字符串的时候调用的,抛出异常会导致问题。PHP 7.4改进了这个限制。
4. 密码哈希扩展
把密码哈希功能做成了独立的扩展,增加了对Argon2i和Argon2id密码哈希算法的支持,这些算法比传统的bcrypt更安全。
5. 性能优化和bug修复
PHP 7.4也做了很多性能优化和bug修复,比如优化数组的内存布局、优化字符串处理、优化函数调用性能等,整体性能比PHP 7.3又有一定提升。同时弃用了一些旧的不推荐的特性,比如没有括号的include/require、curly braces访问数组和字符串偏移量等,为PHP 8.0做准备。
九、升级建议和注意事项
最后给一些升级到PHP 7.4的建议。
第一,升级之前一定要先看弃用和不兼容的变更。比如一些旧特性被弃用了,一些函数的行为变了,一些扩展被移除了。确保你的代码不会因为这些变更而出问题。比如PHP 7.4弃用了没有括号的include/require,弃用了curly braces访问数组和字符串偏移量(比如$str{0}),这些在PHP 7.4里会抛出弃用警告,未来版本会被移除,要提前修改。
第二,先在测试环境测试,不要直接在生产环境升级。确保代码在PHP 7.4下能正常运行,没有bug,没有性能问题,然后再逐步在生产环境升级。最好先升级到PHP 7.3再升级到7.4,不要跨版本升级。
第三,利用新特性改进代码。升级到PHP 7.4之后,可以给属性加上类型声明,用箭头函数代替简单的匿名函数,用数组展开运算符代替array_merge,用空合并赋值运算符简化默认值处理。这些能让代码更简洁更健壮更类型安全。同时在生产环境开启预加载,把框架核心文件和常用文件预加载,能获得明显的性能提升。
第四,注意预加载的配置。预加载脚本要正确,预加载的文件要是常用的核心文件,不要预加载太多不需要的文件浪费内存。修改预加载的文件后要重启PHP服务才能生效。预加载需要开启OPcache,并且需要PHP-FPM或者类似的常驻进程SAPI。
第五,关注PHP 8.0的动向。PHP 7.4是PHP 7.x系列的倒数第二个版本,下一个就是PHP 8.0,会有很多重大变化比如JIT、更完整的类型系统等。升级到7.4的时候也可以关注PHP 8.0的动向,提前做好准备。
十、写在最后
PHP 7.4是一个很重要的版本,它不仅增加了很多开发者期待已久的新特性,比如类型属性、箭头函数、预加载、数组展开运算符,也在性能上有不小提升,尤其是预加载能让PHP的性能再上一个台阶。同时PHP 7.4也在为PHP 8.0做准备,很多PHP 8.0的特性会先在7.4里打下基础。
作为一个用了很多年PHP的开发者,我很高兴看到PHP在不断进步不断现代化。从PHP 7.0的性能飞跃,到7.1、7.2、7.3的不断完善,再到7.4的类型属性和预加载,PHP正在变得越来越强大越来越现代化,也越来越受开发者欢迎。
希望这篇文章能帮大家更好地理解PHP 7.4的新特性和底层原理,也希望大家能尽快升级到PHP 7.4,体验新特性,享受性能提升。
最后用一句话结束:PHP一直在进步,从未停止。期待PHP 8.0给我们带来更多惊喜。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录