PHP 泛型之殇 泛型 RFC 提案被拒绝

📅 2026/6/29 20:55:51
PHP 泛型之殇 泛型 RFC 提案被拒绝
Symfony 核心维护者 Nicolas Grekas关于 PHP 泛型已有大量详尽的前期研究。结论十分明确PHP 引擎无法高效实现泛型挑战包括类型推导不能破坏开发者体验、性能不能增加运行成本等众多方面。唯一可行的方案是提前静态分析。梦想在 PHP 自身内核中内置一个用 C 编写的静态分析器固然美好但并不现实PHP 静态分析工具之所以用 PHP 开发自有其道理。这种方式允许快速迭代由精通该语言的人来推进。将这种经验迁移到引擎侧将会把那些真正让 PHP 静态分析成为现实的人排斥在外这对社区来说将是一个巨大的损失。提案中的擦除式泛型正是这条道路的延续而这条路已被证明是可行且有用的。得益于 IDEPHP 静态分析的用户基础远比 phpstan、psalm 等工具的活跃用户广泛得多。在 LLM 生成代码的时代PHP 拥有更强验证环节的必要性更加凸显。静态分析工具如今已成为开发流程中不可或缺的一部分。多年来社区一直在等待和憧憬泛型的一点一点推进甚至连泛型数组都曾因过于困难而被否决。等待的时间已经足够长足以得出一个结论擦除式泛型是唯一现实的进步方向。当然如果事实证明判断有误那也会是一件令人高兴的事。几年之后如果那时人们和 LLM 还在写 PHP 的话再看分晓。来自 Tempest 核心开发者、全职 PHP 开发者 Márk Magyar首先需要坦诚地说如果这个 RFC 未能通过他会感到极度失望。但先从最没有争议的地方说起——多年来他一直在 PHP 中使用泛型事实上大多数 PHP 开发者也是如此。如果曾经打开过任何一个 Laravel、Doctrine、Symfony、PHPUnit 或 PSL 类就一定会看到template注解。它在 GitHub 上出现在超过 20.2 万个文件中。这是生态系统中每一个严肃的集合、仓库和结果类型的自我描述方式。社区使用擦除式泛型——即静态分析器检查而引擎忽略的类型——已经将近十年。因此当有人把这个 RFC 说得像某种新奇的外来事物时会让人感到有些困惑。Bound-Erased Generic Types RFC 实际上唯一做的事情就是不再让开发者把这些泛型写在注释里假装它们不算数。这种注释开销是真实存在的。想想看一个泛型 Laravel 类今天长什么样。它同时用两种语言编写签名里的 PHP 类型以及紧随其上的 docblock 中的 PHPDoc 类型。解析器验证前者却默默信任后者。重构时重命名一个属性注释就悄悄过时了。ReflectionClass::getName()返回的是Collection而不是CollectionTKey, TModel。PHPStan、Psalm 和 Mago 对复杂情况的解读各有一些差异因为根本不存在一个它们可以参照的实际语言标准。这个 RFC 以零运行时成本解决了所有这些问题。然而在撰写本文时它的投票结果是 4 票赞成 / 12 票反对 / 3 票弃权。想谈谈这背后的原因因为这才是真正的悲剧所在而且比核心团队讨厌泛型更令人沮丧。事实并非如此。真正的原因比这更糟糕。但原生语法不应该撒谎反对这个 RFC 的最强论点并不愚蠢因此不会试图硬碰硬。Rowan Tommins 在邮件列表中说得最好但目前PHP 的原生语法不会撒谎——标记为 private 的属性确实是私有的返回类型标记为 int 的确实总是整型……这个提案将从根本上改变这一点——它将引入一种看起来像是标准强制类型系统一部分的语法但在很多情况下却完全不起作用。Derick Rethans 从经验出发表示赞同称用户在发现这些类型不会被强制检查时几乎无一例外地感到困惑。Tim Düsterhus 进一步指出静态分析器只能证明错误的存在无法证明错误的不存在——他说的没错不实际运行 PHP 程序就无法对其做完整的类型检查。面对这个论点思考了良久因为它听起来无懈可击。但是——总有一个但是——这是一个关于纯正性的论点而 PHP 的类型系统从来就不纯正。运行时已经不会检查数组的元素类型、iterable 的内容、callable 的参数或返回签名以及mixed内部任何类型。PHPStan 的作者、也是第一个在 PHP 中引入 docblock 泛型的人 Ondřej Mirtes在同一个邮件线程中指出在已发布的 PHP 版本中已经存在一些角落——写了原生类型语法却默默地永远不会被强制检查。所以原生语法绝对不能撒谎这条规则PHP 很久以前就已经打破了而大家依然愉快地继续写 PHP。Attribute 就是最干净的例子一个原本只存在于 docblock 中的概念获得了真正的语法但除了反射之外没有运行时效果而社区非常喜爱它们。用户会感到困惑这个担忧其实根本站不住脚因为十年时间足以让这个担忧成为现实但它并没有。Azjezz 的回应是值得被铭记的一段话其核心前提是可以用经验测试的而这个测试已经运行过了PHP 在 docblock 中使用泛型已经十年并被所有主流框架所采用……你所描述的那种失败模式在当前系统下本应是最容易发生的但它并没有发生。而这背后的原因简单到几乎乏味。那些在乎到会写CollectionUser的人正是那些会运行静态分析工具的人。使用泛型和不运行任何检查工具之间的重叠在实践中几乎为零。当然Gina Banyard 质疑乐观的90% 项目使用静态分析的说法是有道理的——JetBrains 的调查显示这个数字接近 44%——不打算否认这一点。但这 44% 恰恰就是那些会使用泛型的人群。没有人会在整个代码库中手写TKey, TModel注解然后什么工具都不运行。他们在等待一套行不通的运行时泛型那么如果反对意见实际上站不住脚为什么会有 12 张反对票因为大多数反对者仍然在等待具象化泛型——在运行时检查的类型——他们宁愿什么都没有也不愿现在接受擦除式泛型。在这一点上与大多数人意见不同不想要运行时检查的泛型。不是作为将来的目标也不是作为擦除式泛型暂时代替的真正版本。擦除式就是实际想要的那个模型因为它零成本并且和生态系统已有的工作方式一致。不过先放下个人偏好不谈因为即使从反对者自己的立场来看这种坚持也没有道理。PHP 尝试构建运行时泛型已经十年了却总在同一条路上撞上同一堵墙。Nikita Popov 早在 2020 年就做过原型并详细阐述了为什么单态化在动态引擎里行不通。PHP 基金会在 2024 年重新捡起这项工作结果当泛型遇到联合类型时立即遇到了超线性类型检查的问题。到 2025 年官方路线已经全面退缩到仅编译时泛型new RepositoryBlogPost()、联合类型和类型推导全部被明确标记为真的真的很难™、真的真的很慢™或者两者兼有。而当 Rob Landers 在大约一周内将具象化实现挂载到这个分支上时测试数据表明泛型密集的代码慢了 30% 到 50%最坏情况下接近 2 倍。这种开销会通过依赖图向下游传导——任何仅仅依赖某个使用了泛型的库的应用都要承担这个代价。这就是灌木丛中的那只鸟。而反对票正在放手手中的那只只为继续盯着它。他们真正要求的标——在考虑擦除式之前必须先证明具象化泛型不可能——是一个永远无法满足的标准。对于一台没有人有时间、资金或授权去重写的引擎无法证明一个否定命题。真正会使用它的人没有投票权到了某个时刻这不再像是一个技术裁决而更像是一个流程失败。讨论由静态分析领域的人主导他们绝大多数表示支持。投票者则主要是运行时引擎开发者。正如 Psalm 的作者 Matthew Brown 所说这两类人之间的重叠并不多。那些每天都会使用这个特性的人没有投票权那些永远不会写CollectionUser的人却有。Brown 不仅在 Reddit 上说过。在 internals 邮件列表的讨论中他告诉开发者们2004 年做出的决定不应该主导今天的决策运行时检查在当年没有更好方案时有意义但如今静态分析能比运行时发现更多错误更早、更快。这个人编写了整个生态系统所依赖的两个工具之一而他的名字却不在投票名单里。一个被开发者们期盼了十年的特性正在消亡因为会用的人不是做决定的人。就连劝说 Azjezz 推迟投票的 Larry Garfield 也是因为这个原因——用他的话说核心团队否决泛型对每一个关心 PHP 的人来说都是最糟糕的结果。他说得对。而这件事很可能还是会照常发生。也应当给反对派应有的评价因为并不认为这个 RFC 是完美无缺的。它并不是一个完全干净的擦除模型。它确实存在编译时和链接时的执行缺口而且担心它的发布可能会给未来的具象化设计带来更棘手的破坏性变化——这个担忧是合理的不是 FUD。但投赞成票的 Nicolas Grekas 指出了真正重要的事情docblock 泛型之所以可被采纳正是因为它们对引擎不可见而原生的T可以遵循同样的渐进路径——PHP 在返回类型、可空参数以及其他所有特性上已经走过了这条路。所以并不对投票失败感到惊讶。只是对这种局面感到厌倦。PHP 本可以拥有泛型。它每天都在运行泛型依托于每一个我们所依赖的框架。而它恰恰正在拒绝这些泛型只为等待一个它花了十年证明自己无法构建的版本。这才是悲剧。不是灌木丛中的鸟难以捕捉而是我们一再放手手中的那只只为站在那里盯着它看。说实话这真的很可悲。来自 RFC 作者 Azjezz作为 RFC 的作者询问 Azjezz 是否愿意发表意见。他表示没有时间撰写一篇措辞优美的博文但确实想参与并允许从他在 Reddit 上的一篇近期评论中引用内容。在那篇评论中他解释了为什么在投票很可能失败的情况下仍然开启了投票。Azjezz 解释说讨论的焦点已经从 RFC 本身关于运行时忽略泛型转向了另一种诉求——人们想要具象化运行时检查泛型。他解释道第二类情况值得坦诚面对。原则上不反对具象化泛型。如果它们在今天的 PHP 中可行也更愿意选择它们。没有将 RFC 转向具象化的原因是设计得更好并不是问题所在。Rob Landers 在大约一周内在这个分支上实现了具象化泛型测试数据表明泛型密集的代码慢了 30%–50%最坏情况下接近 2 倍。这种开销会通过依赖图不断累积。如果 Psl或 Symfony、PHPUnit、Laravel、Doctrine 等在具象化模型下发布了原生泛型每一个下游应用都要为此买单即使那些从未声明过自己的泛型的应用也无法幸免。把这个成本放到 CI 上10 分钟的构建会变成 15 到 20 分钟蔓延到整个生态系统。这样的数据人们同样不会投赞成票。关于为什么 Azjezz 在未进一步探索在 RFC 之上添加具象化泛型的可能性就提前开启投票他这样回答老实说不会把时间花在具象化泛型上因为在他看来它们不会发生。不是因为不想要它们——他确实想要。要使它们在 PHP 中达到可以正式发布的性能水平需要对引擎进行结构性改造而他既没有时间、自由也没有这个授权去做。他只是一个利用业余时间、在各项事务间隙中工作的个人。要求他重写 PHP 引擎以使具象化泛型可行并在这个过程中破坏现有功能这并不是一个现实的请求。而在没有这种重写的情况下发布具象化 RFC只会让现状走到 Rob 的分支已经到达的地方一个可以工作但性能表现令社区无法接受的实现。如果具象化泛型有一天在 PHP 中变得可行那一定是因为某个有时间、有资源、有引擎层面授权的人开辟了那条道路。一旦这种情况发生也会很乐意支持后续的 RFC。在那之前静态分析收敛性是否值得发布边界擦除式泛型才是选票上真正的问题。来自 Laravel 高级软件工程师 Nuno Maduro自 PHPStan 问世以来Nuno 几乎在其所有代码中通过 PHPStan 使用泛型。到了今天这感觉像是任何现代语言的必备特性。如果 PHP 想继续朝着这个方向前进它需要泛型。来自 JetBrains PHP 开发者倡导者 Brent Roose信不信由你但 Brent 对最近的 RFC 正在失败这件事并没有那么在意。他也希望它能通过但泛型在大局上并不会带来本质区别。那个大局要重要得多如果说有什么意义的话泛型 RFC 反而凸显了 PHP 在这个方面的失败。无论人们是否愿意接受PHP 不仅仅是解释器也不仅仅是语法。PHP 之所以有今天的地位不是因为语言本身有多优美而是因为它拥有丰富的生态系统。PHP 不仅仅是一门编程语言如果没有框架、包和工具构成的生态系统它很可能早已不复存在。与此同时大约 100 人组成的一个群体正在决定这门语言的未来严格来说大约有 2000 人有资格投票但大多数人已经不再参与了——这本身就很令人震惊。没有领导者或实体来设定愿景而这个群体内部严重分裂——例如他们会花费数周争论是否应该从官网上移除一个 X 链接。有人说缺乏统一的愿景和方向正是 PHP 的伟大之处但 Brent 认为这正在严重拖累 PHP。那些还没有使用 PHP 的公司为什么会选择一门设计权不属于任何人的语言在这个体系中唯一的受薪实体随时可能因为一小群人的反对而被阻止前进而这个群体几乎没有任何来自真正驱动 PHP 发展的最大生态系统——如 Laravel、Symfony、WordPress 或 Packagist——的代表。在 Brent 看来这就是泛型 RFC 以及在此之前许多 RFC 所揭示的失败。过去曾有人试图改变这个体系但徒劳无功。委员会似乎对现状感到满意并不希望流程发生改变。