2. 类型声明的核心价值:不只是“写清楚”这么简单
很多人在看代码规范时,总觉得类型声明是“给 IDE 看的”“方便同事读代码的”,这种理解不算错,但远远不够。类型声明真正带来的好处,是让 PHP 引擎在运行阶段省掉一大类隐式判断。
PHP 是动态类型语言,变量到底是什么类型,引擎在运行时才去确认。这个过程不是免费的。每执行一次函数调用,Zend 引擎都要检查传入参数的类型、执行可能的隐式转换、再根据转换结果决定后续操作路径。这些检查分散在大量 opcode 里,积少成多,性能损耗是真实存在的。
举个最直观的例子,PHP 7.4 开始支持参数类型声明里的 ?int、?string 这种可空类型,也支持返回类型声明。加上这些声明之后,Zend 引擎可以在进入函数体之前就完成一次快速类型校验,校验不通过直接抛 TypeError,不需要再进入函数内部走各种转换兜底逻辑。虽然这个检查本身也有成本,但相比动态判断带来的分支和潜在转换,收益在热点函数上非常明显。
我自己在项目里做过一次实际压测。一个电商订单模块的金额计算服务,核心方法被调用的频率很高,每秒上千次。这个方法有三个参数:订单金额、折扣比例、用户等级。改动前三个参数都是无类型声明状态,函数内部靠 (float) 强转和若干 is_numeric() 判断兜底。加上类型声明之后,函数内部所有强转代码直接删掉,参数在入口处就保证是 float 和 int,接口响应时间平均下降了大概 8%。单个请求看不出差别,但流量上来之后这个百分比就很有意义了。
这个案例里我用的就是 PHP 7.4 的类型语法特性,比如参数类型、返回类型、严格的 declare(strict_types=1)。这一套组合拳打下来,代码更简洁,运行时判断更少,性能自然上去了。而且这些改动对业务逻辑零侵入,只是把“本来就应该明确的约束”显式表达出来。
2.1 为什么类型声明能减少运行时开销
要理解性能提升的本质,先得知道 PHP 引擎处理一次函数调用时到底发生了什么。
没有类型声明的时候,函数收到一个 $price,引擎并不知道它是字符串、浮点数还是整数。后续代码里如果执行 $total = $price * $quantity,Zend 引擎得先判断 $price 的类型,再判断 $quantity 的类型,如果类型不一致,还要把其中一个转成另一个再运算。这个判断和转换过程在 C 层面是几行指令,但放在 PHP 层看,是每一次调用都要重复执行的开销。
加了类型声明之后,情况完全不同。参数被声明为 float,引擎在进入函数前就把传进来的值做一次强制转换或校验。如果调用方传入 "19.9" 这种字符串,PHP 会尝试转成 float;如果传入一个数组,直接抛 TypeError。这个校验发生在函数体执行之前,而且一旦通过,函数内部就可以假设这个参数永远是 float,Zend 引擎在编译函数体时也能做更多优化。
还有一个很多人没注意到的点:返回类型声明同样能帮助调用方做优化。一个函数声明了返回值是 int,外部调用它的时候,引擎知道拿到的一定是整数,后续再做算术运算、字符串拼接时,可以减少一层类型判断。这在长调用链里是能产生复利效应的。
用个生活化的类比。你去银行办事,没有预约的话,柜台工作人员得先问你要办什么业务、查你的身份信息、确认你的权限,然后才能给你办。但如果你提前在 App 上约好了业务类型、上传了证件,柜台拿到单子直接就能办。类型声明就是那个“预约单”,让引擎不用每次都在运行时重新询问参数到底是什么。
2.2 严格模式与强制模式:怎么选才对性能最有利
PHP 类型声明有两种使用模式,默认是强制模式(coercive mode),加了 declare(strict_types=1) 之后是严格模式(strict mode)。这个选择会直接影响性能表现,也影响代码的安全性。
强制模式下,PHP 会对不匹配的类型做自动转换。比如参数声明为 int,你传一个字符串 "42",PHP 会把它转成整数 42,然后进入函数体。这种模式的好处是兼容性好,旧代码不容易炸,但缺点是引擎多了一次隐式转换,而且类型混乱的问题只是被掩盖了,没被根治。
严格模式下,参数类型必须精确匹配。传字符串 "42" 给一个 int 类型的参数,直接抛 TypeError,不会做任何转换。性能上严格模式更好,因为引擎不用再去尝试转换,路径更短。从代码质量角度看,严格模式也更好,因为你强制要求调用方传入正确类型,而不是靠引擎兜底。
我在项目里的习惯是:新代码一律在文件顶部加 declare(strict_types=1),旧代码在重构时逐步加上去。为什么不全量一下子改?因为严格模式下发类型不匹配会直接报错,如果调用方代码没有同步清理,线上会秒挂。所以一定要分批推进,先
