当前位置:首页 > 文章列表 > 文章 > php教程 > PHP readonly 类怎样处理需要延迟初始化的属性

PHP readonly 类怎样处理需要延迟初始化的属性

来源:17golang原创 2026-11-14 05:09:46 0浏览 收藏

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任何会观察对象状态的操作都可能触发初始化
PHP readonly 属性从构造完成到第一次赋值的一次性状态流转说明图
图1:状态说明图,展示 readonly 属性从未初始化到第一次赋值的单向生命周期;这是静态解释图,不是运行截图。

让 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 两类策略。它们解决的是对象级问题:对象先以延迟状态存在,等属性被读取、修改,或其他操作需要观察对象状态时,再调用初始化器。

PHP 单属性懒加载与 lazy ghost、lazy proxy 的职责范围对比结构图
图2:决策结构图,比较单属性懒加载、lazy ghost 与 lazy proxy 的延迟范围;这是静态解释图,不是 PHP 运行结果。

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 约束只管成功的属性写入,不会替你设计缓存失败、并发访问或内部对象变更。发布前可以按下面的运行手册检查:

  1. 确认属性是有类型的,并且没有试图声明普通默认值;readonly static property 也不受支持。
  2. 确认延迟赋值发生在声明类允许的作用域内,不能从外部直接初始化。
  3. 确认加载方法成功后不会再次覆盖同一属性;如果需要刷新,改成返回新对象或使用非 readonly 的显式缓存策略。
  4. 如果属性保存的是对象,记住 readonly 锁住的是属性引用,不会阻止该对象内部状态改变。
  5. 如果使用 PHP 8.4 Lazy Objects,检查哪些操作会观察对象状态,避免在日志、序列化或调试输出中意外触发昂贵初始化。

一个实用的判断是:字段级需求用“未初始化属性 + 显式访问方法”,兼容旧属性语法才考虑 __get();对象级需求再用 newLazyGhost() 或 newLazyProxy()。这样 readonly 保持的是清晰的一次写入边界,延迟加载负责的是发生时间,两者不会互相掩盖。

相关问题

readonly 属性可以声明默认值吗?

不能声明普通默认值,因为那会让属性在对象建立时就已经是固定常量语义。需要延迟初始化时,保留有类型但无默认值的状态。

readonly 类是不是深度不可变?

不是。属性不能重新指向别的值,但属性里保存的对象仍可能修改自己的内部状态。

什么时候不该使用 __get()?

只要可以使用明确的 rows()、config() 等访问方法,就不必隐藏加载路径。魔术访问应留给必须兼容既有调用方式的接口。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
OpenTelemetry Collector batch processor 怎样控制 flush 频率OpenTelemetry Collector batch processor 怎样控制 flush 频率
上一篇
OpenTelemetry Collector batch processor 怎样控制 flush 频率
PHP CallbackFilterIterator 按文件属性过滤目录项的写法
下一篇
PHP CallbackFilterIterator 按文件属性过滤目录项的写法
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    1018次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1020次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1155次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1121次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    962次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码