PHP 8.2 的 readonly 类仅冻结属性直接赋值,不保证深层不可变、不封禁方法调用、不拦截反射或反序列化,也不提供 equals/hash 等契约,需手动补全;继承时子类必须显式声明 readonly 才合法。

PHP 8.2 的 readonly 类不是“不可变对象”的完整实现,而是一个语言级的、轻量但强约束的只读状态保障机制——它只冻结属性赋值,不自动处理嵌套可变结构、不封禁方法调用、也不保证深层不可变。
只读类 ≠ 不可变对象:关键差异在哪
很多人误以为加了 readonly 就等于得到了一个真正不可变的对象。实际并非如此:
-
readonly仅阻止对类属性的直接写入($obj->prop = ...),但不阻止你调用方法修改内部状态(比如$obj->incrementCounter()) - 如果属性本身是数组、对象或资源,
readonly不递归冻结其内容 ——public array $data;中的$data['key'] = 'new'依然合法 - 没有自动的
__clone防护、不禁止unserialize()、也不拦截反射赋值(ReflectionProperty::setValue()在开发环境下仍可能绕过) - 它不提供
equals()、hash()或深拷贝等不可变对象常用契约,这些需手动补全
只读类继承时必须显式声明 readonly
子类继承只读父类时,PHP 不会自动推导“我也是只读的”——你必须自己写 readonly class Child extends Parent,否则报错:
readonly class User { public function __construct(public string $name) {} }
class Admin extends User { } // Fatal error: Cannot extend readonly class
这个限制不是为了麻烦,而是为了明确表达设计意图。常见踩坑点包括:
立即学习“PHP免费学习笔记(深入)”;
- 忘记加
readonly关键字,导致继承失败且错误信息直白但容易忽略上下文 - 试图让非只读子类覆盖父类只读属性(如
public $name;→public string $name;),这在语法上就不允许 - 在子类构造函数中未调用
parent::__construct(),导致父类属性未初始化,触发Uninitialized readonly property错误
何时该用只读类,何时该回归传统不可变模式
readonly 类适合快速建模“一次初始化、绝不修改”的简单数据载体;但遇到以下场景,就得搭配其他手段:
- 需要深不可变:属性含对象?手动把子对象也做成
readonly类,或用ImmutableArrayObject等封装容器 - 需要值语义比较:重写
__equals()(PHP 8.4+ 支持)或手写字段逐一对比逻辑,否则==仍是引用比较 - 要防御序列化/反序列化篡改:声明
__serialize()和__unserialize(),或加final防止被继承后注入恶意逻辑 - 需要运行时校验(如邮箱格式、金额正数):构造函数里做,
readonly不帮你做,也不报错——它只管“能不能赋值”,不管“该不该赋这个值”
最易被忽略的一点:只读类的性能开销几乎为零,但它的安全边界非常窄——它只守住了“属性赋值”这一道门,门后是什么,得你自己清点。别把它当银弹,也别因它没封死所有路就弃用;用对位置,它就是最省心的数据容器。



















