Nullable 类型声明的两种写法及推荐风格?

解读

国内一线/二线公司在面试时,常把“Nullable 写法”作为考察 PHP7 以后类型系统是否熟练的“门槛题”。它看似简单,却能迅速区分候选人是否停留在 PHP5 时代。面试官真正想听的是:

  1. 两种语法都能脱口而出;
  2. 知道 PHP 8.1+ 推荐的“联合类型”写法;
  3. 能解释背后的 nullable 语义与底层实现(null 掩码位);
  4. 最后给出团队可落地的编码规范(PSR-12 风格)。
    答不到第 3、4 点,基本会被判定为“只用过,没研究过”。

知识点

  1. 旧语法(PHP 7.0 引入):?Type
    • 等价于 Type|null 的语法糖
    • 只能放在类型前,不能混写
  2. 新语法(PHP 8.0 起支持的联合类型):Type|null
    • 通用联合类型,可扩展为 int|float|null
    • ?Type 在底层生成的类型掩码完全一致,性能无差异
  3. 默认值与可空性区别:
    • function foo(?int $v = null) 合法
    • function foo(int $v = null) 非法(默认值为 null 但类型不可空)
  4. 属性、返回值、参数同时支持上述两种写法
  5. PSR-12 未强制 nullable 风格,但国内大厂普遍在 8.1+ 项目里用联合类型,以便后续平滑升级到 PHP 9 可能废弃 ? 前缀的传言

答案

两种写法:

  1. 问号前缀:?string $name
  2. 联合类型:string|null $name

推荐风格(PHP 8.1 及以上新项目):
统一使用联合类型 string|null,理由:

  • 与未来可能的纯联合类型语法保持一致,降低升级成本;
  • 可读性更高,一眼看出“到底联合了谁”;
  • 方便在代码审查工具(PHPStan level 9)中做更细粒度的类型收窄提示。

老项目(PHP 7.x 维护分支):保持 ?Type 即可,避免大规模重构。

拓展思考

  1. 底层实现:两种写法最终都生成同一份 ZEND_TYPE_NULLABLE 掩码,OPcache 优化后字节码完全一致,性能无差异;面试时可反问“是否需要性能基准测试”,展示深度。
  2. 泛型与 Nullable:如果公司用 Psalm/PHPStan,泛型模板里写 @param T|null?T 更易被静态分析识别,减少误报。
  3. 向前兼容策略:在公共库中建议加 #[\AllowDynamicProperties]string|null 联合类型并存,既保证老用户不报错,又能让新用户享受严格类型。
  4. 面试加分句:“我们团队在 CI 里加了 php-cs-fixer 规则 nullable_type_declaration_for_default_null_value:false,强制所有新代码用联合类型,存量代码通过脚本一次性转换,合并请求里只 Review 业务逻辑,不再争论 nullable 风格。”