首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >30年后!我们或许真的能在 PHP 中看到泛型了

30年后!我们或许真的能在 PHP 中看到泛型了

作者头像
Tinywan
发布2026-07-01 16:53:15
发布2026-07-01 16:53:15
690
举报
文章被收录于专栏:开源技术小栈开源技术小栈

概述

此 RFC 为 PHP 添加泛型类型语法。类、接口、trait、函数、方法、闭包和箭头函数都可以声明类型参数;这些参数支持边界(bound)、默认值和协变/逆变标记;在使用的位置和调用位置可以通过 turbofish::<...>)提供类型参数。

代码语言:javascript
复制
<?php

final readonly class Pair<+L, +R> {
    publicfunction __construct(
        public L $left,
        public R $right,
    ) {}

    publicfunction swap(): Pair<R, L> {
        returnnew Pair($this->right, $this->left);
    }
}

final readonly class Box<+T> {
    publicfunction __construct(
        public T $value,
    ) {}

    publicfunction map<U>(callable $fn): Box<U> {
        returnnew Box(($fn)($this->value));
    }

    publicfunction zip<O>(O $value): Box<Pair<T, O>> {
        returnnew Box(new Pair($this->value, $value));
    }
}

function identity<T>(T $value): T {
    return $value;
}

$greeting = new Box::<string>("hello, world");
$paired   = $greeting->zip::<int>(42);
$swapped  = $paired->value->swap();
$result   = identity::<Pair<int, string>>($swapped);

var_dump($result->left);  // int(42)
var_dump($result->right); // string(12) "hello, world"

本提案中的泛型是绑定擦除(bound-erased)的:在运行时,Box<int>Box<string> 是同一个类,每个类型参数都被替换为其声明的边界(无边界时为 mixed),引擎看到的是普通的 PHP 类型。强制检查分为三层:

  • 编译时:验证语法、127 个参数上限,以及类型参数的默认值是否满足其声明的边界。
  • 链接时:验证继承子句的参数数量(extends/implements/use)、边界符合性(包括转发参数的 bound-on-bound)、拒绝具有冲突绑定的菱形继承,以及参数化 LSP 传播到属性类型、属性钩子签名、trait 方法签名和非重写继承方法签名。
  • 运行时:在每个 turbofish 位置(调用、new、属性构造)验证参数数量和边界。后备属性存储、属性钩子签名和 trait 属性会强制使用替换后的类型。

新的 Reflection API 会暴露擦除前的形式,因此静态分析工具可以直接从引擎读取泛型信息,而非依赖 PHPDoc 约定。不使用泛型的代码会编译成与今天完全相同的字节码。

RFC 其余部分会讲解什么是泛型、PHP 为什么需要它们、塑造此设计的先例、完整提案、设计理由、实现,以及未来可叠加的运行时模型。

什么是泛型

泛型之于类型,就像函数之于值。

函数对值进行抽象:sum(a, b) 从两个输入计算结果,无论 a 和 b 是 1 和 2 还是 100 和 200,函数体都适用。泛型对类型进行抽象:一个泛型 Stack 存放某种类型 T 的元素,无论 T 是 int、string 还是 User,同一个类体都适用。

没有泛型时,只有两种不理想的选择:

  1. 为每种元素类型写一份相同的数据结构(IntStackStringStackUserStack)。实现几乎完全相同,修复 bug 时必须在每份拷贝中修改。
  2. 写一个元素类型为 mixed 的单一数据结构。它对所有人可用,但每次从中取出值时都丢失了类型信息,必须信任生产者或重新检查。

泛型类型会携带类型信息。声明 class Stack<T> 引入了一个类型参数 T,代表“元素类型,无论它是什么”。使用 Stack<int>Stack<User> 则为该参数提供类型实参(type argument)。类体只需写一次,就能适用于每种 T 的选择;静态分析器和语言本身可以跟踪参数与流经它的值之间的关系。

为什么人们使用泛型

人们已经在 PHP 中使用泛型了——只是写在注释里。

当今 PHP 生态中的每个主要框架、ORM、测试库和标准库项目,都在 PHPDoc 中携带泛型类型信息,由静态分析工具解析和验证。这些信息是真实的,关系是关键的,当 docblock 与实现脱节时,代码库会以微妙方式出错。RFC 详细列举了 Laravel、Symfony、PSL、Doctrine、Tempest、API Platform、PHPUnit 等项目的实际使用案例。

这些项目独立到达了相同结论:PHP 需要泛型。它们还实际采用了几乎相同的语法:@template T(可选 of X 表示边界)、使用位置的 <T, U>、继承转发标签(如 @extends@implements 等)。

这种收敛是真实的、生态级的,但它没有经过标准流程。没有 @template 的规范,没有治理,没有共享测试套件。依赖的是多个静态分析工具(Mago、PHPStan、Psalm、Phan 及 IDE 中的 PHPDoc 解析器)在过去十年相互观察并相互支持的结果。

这种收敛是脆弱的。本 RFC 将泛型语法带入语言核心,让静态分析工具直接从引擎和 Reflection 获取信息,而非依赖脆弱的 PHPDoc 约定。

提案要点

  • 声明位置:类、接口、trait、函数、方法、闭包、箭头函数。
    • 语法:class Box<T> {}function id<T>(T $x): T 等。
    • 支持边界(T : SomeType)、默认值(K = string)、协变/逆变(+T-T)。
  • 边界:上界(upper bound),默认为 mixed。支持复杂类型、递归引用(嵌套形式)、同列表内相互引用。
  • 默认值:必须满足边界;可选参数不能后跟必选参数。
  • 使用位置:任何类型表达式中,如属性、参数、返回类型、self<T>static<T> 等。arrayiterable 的泛型参数暂不支持(留给后续 RFC)。
  • 调用位置 Turbofishnew Box::<User>()filter::<User>($data) 等。运行时检查参数数量和边界。
  • 擦除语义:运行时类型参数被替换为边界(或 mixed)。参数化 LSP 在链接时进行签名替换。
  • Reflection API:新增方法暴露泛型元数据(参数、边界、默认值、协变标记等)。
  • 限制:单列表/实参列表最多 127 个参数;匿名类不支持泛型声明;类级类型参数不能用于静态上下文。

设计选择

为什么选择绑定擦除? 它在 PHP 当前类型检查模型、生产使用记录(Java、Kotlin 等语言的擦除泛型长期成功)、性能和向前兼容性(未来可叠加重ified 或单态化)之间取得了良好平衡。运行时开销极低,不使用泛型的代码零影响。

其他设计决策包括:使用 <...> 作为类型参数、使用 ::<...>(turbofish)调用、使用 : 表示边界、使用 +/- 表示协变/逆变、使用 = 表示默认值等。

RFC:https://wiki.php.net/rfc/bound_erased_generic_types

PR:https://github.com/php/php-src/pull/21969

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开源技术小栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 什么是泛型
  • 为什么人们使用泛型
  • 提案要点
  • 设计选择
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档