PHP readonly 类怎样处理需要延迟初始化的属性
来源:17golang原创
时间:2026-11-14 05:09:46 263浏览 收藏
PHP 的 readonly 并不要求每个属性都在构造函数里立刻加载。真正的限制是:有类型的只读属性只能在允许的作用域内完成一次初始化,之后不能再次赋值。把昂贵数据保留为“未初始化”,在对象第一次真正需要它时赋值,就能同时得到延迟加载和一次写入约束。
如果只是延迟一个字段,优先在类内部用访问方法完成加载;如果要延迟整个服务对象的构造,PHP 8.4 的 Lazy Objects 更合适。本文不把 readonly 说成深度不可变,也不把字段级懒加载和代理对象混为一谈。
官方文档:https://www.php.net/language.oop5.properties
先判断:要延迟的是属性还是整个对象
先看延迟范围,再选实现方式。一个报告对象可能已经有轻量的标识信息,但只有调用 rows() 时才需要从存储层读取大量记录;这属于字段级延迟。相反,如果连服务对象的构造、依赖注入或实体水化都不希望发生,就应该考虑 PHP 8.4 的 Lazy Objects。
| 场景 | 更合适的边界 | 需要留意 |
|---|---|---|
| 一个数组、配置或计算结果很昂贵 | readonly 属性 + 类内访问方法 | 第一次成功赋值后不能重算或覆盖 |
| 希望兼容魔术属性访问 | 未初始化属性 + __get() | 要明确哪些名称允许触发加载 |
| 整个服务或实体都应按需创建 | PHP 8.4 lazy ghost / lazy proxy | 任何会观察对象状态的操作都可能触发初始化 |

让 readonly 属性保持未初始化
readonly 属性必须有类型,不能写普通默认值。构造函数只负责保存轻量字段,昂贵字段先不读、不写;访问方法用 isset() 判断它是否已经初始化,第一次进入时完成赋值。之后每次调用只读取同一个结果。
source;
}
public function rows(): array
{
// isset 对未初始化的 typed property 返回 false,不会读取出一个伪造的空数组。
if (!isset($this->rows)) {
// 只有第一次成功走到这里时,readonly 属性才被赋值。
$this->rows = $this->loadRows();
}
return $this->rows;
}
private function loadRows(): array
{
// 示例只表达延迟边界;真实项目应在这里处理存储层异常。
return [
['id' => 1, 'state' => 'ready'],
['id' => 2, 'state' => 'queued'],
];
}
}
$report = new Report('daily-import');
// 构造函数没有加载 rows,第一次调用才触发 loadRows()。
$first = $report->rows();
// 第二次调用只读已经初始化的属性,不会再次加载或覆盖它。
$second = $report->rows();
这里的关键不是把属性声明成可空类型,而是让“未初始化”和“已初始化”成为两个明确状态。如果加载失败,赋值语句没有完成,属性仍然没有成功初始化;因此要不要允许下一次调用重试,应由业务代码决定。若失败结果也必须缓存,就应另设状态字段或把失败对象作为一次结果保存,而不是把异常吞掉。
用 __get() 做魔术访问时先收紧边界
PHP 的 readonly properties RFC 说明,初始化前显式 unset() 可以让属性保持未初始化,并配合魔术方法实现传统的懒加载模式。这个方案适合需要兼容 $object->property 访问的旧接口,但它更容易把拼写错误当成加载请求,因此必须做名称白名单。
displayName);
}
public function __get(string $name): mixed
{
// 只允许明确列出的属性,避免把未知字段静默当成远程查询。
if ($name !== 'displayName') {
throw new LogicException('未知的延迟属性:' . $name);
}
// 只读属性在声明类作用域内完成第一次直接赋值。
$this->displayName = $this->fetchDisplayName($this->userId);
return $this->displayName;
}
private function fetchDisplayName(string $userId): string
{
// 示例用固定值代替外部请求;生产代码应保留超时和异常处理。
return 'user-' . $userId;
}
}
$profile = new LazyProfile('42');
// 第一次读取触发 __get;成功后 displayName 不能再次赋值。
echo $profile->displayName;
工程上更推荐前一个显式方法版本:它的调用路径、返回类型和错误处理都更容易被 IDE、静态分析器和代码审查识别。只有在必须保留既有公共属性访问语法时,才使用 __get(),并为未知名称、加载失败和重复进入建立明确规则。
PHP 8.4 用 Lazy Objects 延迟整个对象
PHP 8.4 引入 Lazy Objects,提供 lazy ghost 和 lazy proxy 两类策略。它们解决的是对象级问题:对象先以延迟状态存在,等属性被读取、修改,或其他操作需要观察对象状态时,再调用初始化器。

lazy ghost 会在原对象上完成初始化,适合希望调用方最终拿到同一个对象身份的场景;lazy proxy 则把访问转发给实际对象,选择时要把代理对象与真实实例的身份差异考虑进去。
items);
}
}
$reflection = new ReflectionClass(Catalog::class);
$catalog = $reflection->newLazyGhost(
static function (Catalog $ghost): void {
// 初始化器只在对象首次被观察时运行,并在原对象上补齐状态。
$items = [
['sku' => 'A-1'],
['sku' => 'B-2'],
];
$ghost->__construct($items);
},
);
// 读取对象状态会触发初始化器,之后再按普通对象使用。
echo $catalog->count();
不要为了延迟一个数组就引入对象级代理。Lazy Objects 更适合服务、实体或依赖对象本身很重的场景;如果对象已经创建,只是其中一个字段昂贵,类内访问方法更直接。
失败时怎么处理:一次赋值不等于一次无条件重试
readonly 约束只管成功的属性写入,不会替你设计缓存失败、并发访问或内部对象变更。发布前可以按下面的运行手册检查:
- 确认属性是有类型的,并且没有试图声明普通默认值;readonly static property 也不受支持。
- 确认延迟赋值发生在声明类允许的作用域内,不能从外部直接初始化。
- 确认加载方法成功后不会再次覆盖同一属性;如果需要刷新,改成返回新对象或使用非 readonly 的显式缓存策略。
- 如果属性保存的是对象,记住 readonly 锁住的是属性引用,不会阻止该对象内部状态改变。
- 如果使用 PHP 8.4 Lazy Objects,检查哪些操作会观察对象状态,避免在日志、序列化或调试输出中意外触发昂贵初始化。
一个实用的判断是:字段级需求用“未初始化属性 + 显式访问方法”,兼容旧属性语法才考虑 __get();对象级需求再用 newLazyGhost() 或 newLazyProxy()。这样 readonly 保持的是清晰的一次写入边界,延迟加载负责的是发生时间,两者不会互相掩盖。
相关问题
readonly 属性可以声明默认值吗?
不能声明普通默认值,因为那会让属性在对象建立时就已经是固定常量语义。需要延迟初始化时,保留有类型但无默认值的状态。
readonly 类是不是深度不可变?
不是。属性不能重新指向别的值,但属性里保存的对象仍可能修改自己的内部状态。
什么时候不该使用 __get()?
只要可以使用明确的 rows()、config() 等访问方法,就不必隐藏加载路径。魔术访问应留给必须兼容既有调用方式的接口。
-
127 收藏
-
371 收藏
-
347 收藏
-
112 收藏
-
331 收藏
-
180 收藏
-
181 收藏
-
文章 · php教程 | 12分钟前 | pdo · php教程 · php session_write_close session.lazy_write session_set_save_handler SessionHandlerInterface324 收藏
-
219 收藏
-
104 收藏
-
229 收藏
-
226 收藏
-
218 收藏
-
493 收藏
-
273 收藏
-
238 收藏
-
494 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习