PHP8.1和8.2的类型系统有什么改进
发布时间:2026/10/1 4:03:14来源:尧图网络
前言PHP 8.0 把联合类型union type带进了语言从那之后类型系统进入了快速迭代期。8.1 和 8.2 两个版本又补上了一大批能力交集类型、独立类型的null/false/true、枚举、readonly、never、析取范式类型、readonly类。但这里有一个很容易搞混的事实这两个版本各自引入了什么边界并不总是凭直觉能分清的。比如readonly修饰属性是 8.1 引入的而readonly修饰类要到 8.2 才有never返回类型和纯交集类型AB都是 8.1DNF 类型(AB)|C和独立类型true要到 8.2。写代码时版本标错就会得到Parse error或者Cannot use ... as ...这类让人摸不着头脑的报错。本文按版本分两张表把改进讲清楚再用一段完整的示例把两边的能力串起来最后列出这些新语法在实战中真正会踩的坑。一、PHP 8.1 的类型系统改进特性语法示例说明枚举enum Suit: string { case Hearts H; }有 backing type 的叫 backed enum否则是 pure enumreadonly属性public readonly int $id初始化一次后不可再写never返回类型function fail(): never表示函数永远不正常返回纯交集类型function f(AB $x)多个类型必须同时满足一等可调用语法strlen(...)生成一个Closure不是调用初始化器中的newfunction __construct(private Logger $l new NullLogger())参数默认值里可以newarray_is_list()array_is_list([1, 2])判断数组是不是列表纤维 Fibernew Fiber(function () { ... })可中断/恢复的协程基础设施交集类型只能用于类、接口和self/parent/static不能包含标量类型。写intstring是编译期错误。而且要特别注意8.1 不支持可空的交集类型你不能写?AB这个语法有歧义?A和B谁先结合说不清。需要要么是 AB要么是 null时只能在 8.2 里用 DNF 写成(AB)|null。never是一个只出不进的类型。它只能作为返回类型不能作为参数类型或属性类型带有never返回类型的函数不允许出现return语句哪怕return;也不行必须通过抛异常、调用exit、或者进入无限循环来结束。如果函数意外返回了运行时会抛TypeError。?php declare(strict_types1); // 最低版本PHP 8.1 function fail(string $message): never { throw new RuntimeException($message); } // 编译器知道这行后面的代码不可达静态分析工具也能据此推断 fail(配置缺失);readonly属性有一个硬性限制必须声明类型。因为readonly的语义依赖只能被赋值一次的类型可预测性public readonly $x;无类型是编译期错误。它也不能有默认值——属性声明处的默认值会和初始化一次的语义冲突。二、PHP 8.2 的类型系统改进特性语法示例说明独立类型nullfunction f(): null只能返回 null看起来无用但组合场景有意义独立类型falsefunction f(): false结合联合类型表达失败独立类型truefunction f(): true同上DNF 类型function f((AB)\C $x)readonly类readonly class Point类中所有属性自动readonlyRandom扩展Randomizer::getRandomizer()面向对象的随机数接口#[\SensitiveParameter]用于标记敏感参数加密异常堆栈里的参数值DNF 是Disjunctive Normal Form析取范式的缩写。名字听着复杂规则其实只有两条外层只能是联合|括号里只能是交集交集必须写在括号里。所以(AB)|C合法A(B|C)不合法。括号不允许无意义地嵌套((AB))|C不行也不允许单独一个类型加括号(A)|B不行。?php declare(strict_types1); // 最低版本PHP 8.2 interface HasId { public function id(): int; } interface HasName { public function name(): string; } function render(HasIdHasName $entity): string { return $entity-id() . : . $entity-name(); } // DNF要么是一个同时有 id 和 name 的对象要么就直接是一个字符串 function describe((HasIdHasName)|string $value): string { return is_string($value) ? $value : render($value); }独立类型的实用价值主要在联合类型里。单独写: false只是永远返回 false没什么意义但string|false非常有用——它就是strpos()、file_get_contents()这类函数的真实返回形态。8.0 已经允许false出现在联合类型里8.2 进一步允许null和true独立出现包括单独使用。三、两类隐式转换的变化除了新增语法8.1 还收紧了类型转换这是升级时最容易无声无息出问题的地方。写法变化结果(int) 1.5一直都是 1显式转换不受影响函数声明int $x收到1.58.1 起弃用发Deprecated: Implicit conversion from float 1.5 to int loses precision函数声明int $x收到1.0允许不丢精度不报内置函数收到null参数非空8.1 起弃用Passing null to parameter用户函数收到null参数非空一直报TypeError严格模式外也报关键区别用户自定义函数从 PHP 7.0 起就拒绝 null 了而内置函数一直默默接受直到 8.1 才改成发弃用警告。这就是为什么升级到 8.1 后日志里会突然冒出几万条Passing null to parameter——它们不是新问题只是终于被说出来了。四、代码实战把两代特性串起来下面这个例子完整地用到了 8.1 和 8.2 的主要类型能力枚举、readonly属性、readonly类、never、交集类型、DNF 类型。它可以直接保存运行最低要求 PHP 8.2。?php declare(strict_types1); // 最低要求PHP 8.2用到了 DNF 类型和 readonly 类 // 枚举、readonly 属性、never、纯交集类型来自 PHP 8.1 /** 结算状态backed enum值可以被 json_encode 直接序列化 */ enum PaymentState: string { case Pending pending; case Paid paid; case Refunded refunded; /** 枚举里可以有方法 */ public function isFinal(): bool { return $this ! self::Pending; } } interface HasAmount { public function amount(): int; // 单位分 } interface HasState { public function state(): PaymentState; } /** readonly 类PHP 8.2整个类的属性都自动是 readonly 的 */ readonly class Payment implements HasAmount, HasState { public function __construct( public int $id, public int $cents, public PaymentState $state, public string $currency CNY, ) {} public function amount(): int { return $this-cents; } public function state(): PaymentState { return $this-state; } } /** * 交集类型PHP 8.1参数必须同时满足两个接口 */ function totalCents(HasAmountHasState $payment): int { // 已退款的不计入总额 return $payment-state() PaymentState::Refunded ? 0 : $payment-amount(); } /** * DNF 类型PHP 8.2交集外面套联合 */ function renderLabel((HasAmountHasState)|string $input): string { if (is_string($input)) { return $input; // 直接给字符串就原样返回 } return sprintf( #%d %s %s, $input-state()-value, number_format($input-amount() / 100, 2), $input-state()-name ); } /** * neverPHP 8.1用于一定不会正常返回的分支 */ function abort(string $reason): never { throw new InvalidArgumentException($reason); } /** 依据状态取展示颜色 */ function colorOf(PaymentState $state): string { return match ($state) { PaymentState::Pending gray, PaymentState::Paid green, PaymentState::Refunded orange, }; } // ---------- 运行 ---------- $payments [ new Payment(1, 19900, PaymentState::Paid), new Payment(2, 5000, PaymentState::Refunded), new Payment(3, 8800, PaymentState::Pending, USD), ]; $total 0; foreach ($payments as $payment) { $total totalCents($payment); printf( %s %s\n, renderLabel($payment), colorOf($payment-state()) ); } echo str_repeat(-, 40), PHP_EOL; printf(合计: %s 元\n, number_format($total / 100, 2)); // 枚举的常用操作 $state PaymentState::tryFrom(paid); printf( tryFrom(paid): %s, isFinal: %s\n, $state?-name ?? null, $state?-isFinal() ? true : false ); // 枚举的所有取值 $values array_map( static fn(PaymentState $s): string $s-value, PaymentState::cases() ); printf(全部状态: %s\n, implode(,, $values)); // DNF 参数也接受字符串 echo renderLabel(手动备注), PHP_EOL; // json_encode 对 backed enum 直接输出值 echo json_encode([state PaymentState::Paid], JSON_UNESCAPED_UNICODE), PHP_EOL;预期输出#1 paid 199.00 Paid green #2 refunded 50.00 Refunded orange #3 pending 88.00 Pending gray ---------------------------------------- 合计: 287.00 元 tryFrom(paid): Paid, isFinal: true 全部状态: pending,paid,refunded 手动备注 {state:paid}这段代码里几个值得留意的细节renderLabel()的参数是 DNF 类型(HasAmountHasState)|string进入函数后用is_string()做判断$input就被收窄成了交集类型。这也是交集类型最实用的地方函数体里只依赖两个接口共同承诺的方法amount()、state()不依赖具体类任何同时实现这两个接口的类都能传进来。match表达式PHP 8.0不带default时如果所有分支都没匹配上会抛UnhandledMatchError。这里枚举的三种情况已经穷尽所以安全。PaymentState::tryFrom()失败返回null所以用了?-空安全操作符PHP 8.0。常见坑点1. 在 8.1 上写 DNF 类型❌ 错误写法// 在 PHP 8.1 上 function f((AB)|null $x) {}✅ 正确写法(AB)|null是 8.2 才支持的 DNF 语法在 8.1 上会直接Parse error。8.1 里的替代方案是拆成两个参数或者让参数不加类型、在函数体里做instanceof判断。DNF 类型 8.2 引入纯交集类型 8.1 引入别把版本写反。2. 写?AB期望可空交集❌ 错误写法function f(?CountableArrayAccess $x) {}✅ 正确写法这个语法在 8.1 和 8.2 都不合法因为?和的结合顺序有歧义。要么拆开要么用 DNF// PHP 8.2 function f((CountableArrayAccess)|null $x) {}3. 用never却写了return❌ 错误写法function logAndDie(string $msg): never { error_log($msg); return; // 编译期错误never 函数不能有 return }✅ 正确写法function logAndDie(string $msg): never { error_log($msg); exit(1); // 用 exit / 抛异常 / 无限循环结束 }never的语义是绝不允许正常返回任何形式的return都违反这个约定PHP 会在编译期直接拒绝。4. 用readonly但不写类型❌ 错误写法class Point { public readonly $x; // 编译期错误readonly 属性必须有类型 }✅ 正确写法class Point { public function __construct( public readonly float $x, public readonly float $y, ) {} }5. 在__clone()里改readonly属性❌ 错误写法$p2 clone $p; $p2-x 10.0; // 错误Cannot modify readonly property✅ 正确写法不要对readonly对象做克隆后修改直接用构造器建新实例$p2 new Point(10.0, $p-y);PHP 8.3 才允许在__clone()内部重新初始化readonly属性8.1/8.2 上一律不行。6. 给枚举加属性或构造函数参数❌ 错误写法enum Status: string { public string $label ; // 错误枚举不能有属性 case Active active; }✅ 正确写法枚举不能声明属性但可以声明常量和实现方法需要每个 case 附带额外数据时用方法加match计算enum Status: string { case Active active; case Banned banned; public function label(): string { return match ($this) { self::Active 正常, self::Banned 已封禁, }; } }7. 两个枚举 case 用相同 backing 值❌ 错误写法enum Code: int { case A 1; case B 1; // Fatal error: Duplicate value in enum }✅ 正确写法backed enum 的值必须唯一这是编译期检查。另外提醒一句backed enum 的 case 名字也必须唯一而且它不能有__construct。8. 用纯枚举当 JSON 值❌ 错误写法enum Level { case Low; case High; } echo json_encode(Level::Low); // 纯枚举没有默认序列化方式✅ 正确写法给枚举加 backing type或者实现JsonSerializableenum Level: string implements JsonSerializable { case Low low; case High high; public function jsonSerialize(): string { return $this-value; } }总结特性引入版本关键限制枚举 enum8.1不能有属性backed enum 值唯一readonly属性8.1必须有类型不能有默认值never返回类型8.1只能是返回类型不能出现return纯交集类型AB8.1只限类/接口不能带?一等可调用语法f(...)8.1生成Closure不是调用结果初始化器中的new8.1每次求值都会新建对象DNF 类型 (AB)\C8.2独立类型null/false/true8.2单独用意义有限主要在联合里readonly类8.2所有属性自动 readonly不可与#[\AllowDynamicProperties]共存把这两个版本放在一起看能发现一条清晰的演进线索PHP 的类型系统正在从运行时的约定变成编译期就能验证的契约。8.0 给了联合类型8.1 给了交集和枚举8.2 给了两者的组合能力DNF。真正的收益不在于语法漂亮而在于静态分析工具能更准确地推断类型——类型写得越具体var注释就写得越少重构时的安全感就越强。用之前先确认目标运行环境的最低版本把版本的边界记住比记住具体语法更重要。
网站建设高端定制企业官网