当前位置:首页 > 文章列表 > 文章 > php教程 > PHP readonly 属性克隆对象时的状态复制边界

PHP readonly 属性克隆对象时的状态复制边界

来源:17golang原创 2026-10-10 10:59:02 0浏览 收藏

结论先说:PHP 的 clone 会创建新的外层对象,但对象属性中的对象引用默认仍按浅复制处理。readonly 只限制属性槽位被重新赋值,并不会自动把它指向的可变对象深复制。因此,原对象和克隆对象可能各自拥有一个只读槽位,却共同指向同一个可变对象。

从 PHP 8.3 开始,可以在 __clone() 执行期间把已初始化的 readonly 属性重新初始化一次。这正是隔离嵌套可变对象的窗口;离开这个窗口、对同一属性写第二次,或者在 PHP 8.1/8.2 上执行同样代码,都会触发 Error。

本文依据 PHP 官方手册与 Readonly amendments RFC 核对版本行为:

# PHP 属性与 readonly 官方手册
https://www.php.net/manual/en/language.oop5.properties.php

# readonly 克隆重新初始化规则的官方 RFC
https://wiki.php.net/rfc/readonly_amendments

先把复制边界说清:外层对象复制,不等于对象图深复制

假设订单快照里有一个 readonly 的 DateTime。执行 clone $snapshot 后,快照本身已经是另一个实例,但两个快照的 capturedAt 起初仍指向同一个 DateTime 对象。readonly 阻止的是 $snapshot->capturedAt = ... 这种重新赋值,不会阻止 $snapshot->capturedAt->modify(...) 对嵌套对象内部状态的修改。

capturedAt->modify('+1 day');

// 两个属性仍指向同一对象,所以原对象也会看到日期变化。
var_dump($original->capturedAt === $copy->capturedAt); // true

这类问题最容易被“外层对象编号已经不同”掩盖。判断是否真正隔离,不能只比较 $original !== $copy,还要比较需要隔离的嵌套对象。

PHP readonly 属性在浅复制和 __clone 深复制时的嵌套对象引用关系
图1:readonly 属性在浅复制与 __clone() 深复制下的引用关系说明图,不是截图或运行证据。

PHP 8.3 的门禁:只在 __clone() 执行期重新初始化一次

PHP 8.3 放开的不是“readonly 可以随便改”,而是一个非常窄的生命周期门禁:对象已经由引擎复制完成、正在执行该对象的 __clone(),并且目标 readonly 属性在这次克隆中还没有被重新初始化。满足这些条件时,可以为它换一个新值。

最小的深复制写法如下。它保留原对象不变,只让克隆对象的属性改为指向新的 DateTime 实例。

capturedAt = clone $this->capturedAt;
    }
}

$original = new Snapshot(new DateTime('2026-10-10 10:00:00'));
$copy = clone $original;

// 克隆对象修改自己的嵌套对象,不再影响原对象。
$copy->capturedAt->modify('+1 day');

var_dump($original !== $copy);                         // true
var_dump($original->capturedAt !== $copy->capturedAt); // true

如果最低运行版本仍是 PHP 8.2,不能仅靠条件判断绕过运行时错误;应继续在工厂或 wither 方法里调用构造器创建新对象,或者把最低版本提升到 8.3 后再启用这条路径。

PHP 8.3 和 PHP 8.4 readonly 属性克隆重新初始化边界
图2:PHP 8.3+ readonly 克隆重新初始化的执行期与错误边界说明图,不是截图或运行证据。

不是每个 readonly 属性都要深复制

标量、数组与对象要分开判断。字符串、整数等标量随对象复制后没有共享可变实例的问题。PHP 数组本身采用写时复制,通常也不需要在 __clone() 里为了隔离而重复赋值。真正需要重点检查的是对象和资源。

  • 值类型或不可变对象:例如字符串、整数、DateTimeImmutable,通常可以保持默认复制。
  • 可变对象:例如 DateTime、可变集合、可写配置对象,需要判断克隆后是否必须独立。
  • 有意共享的服务对象:记录器、连接池或无状态服务可能就应共享,盲目深复制反而破坏语义。
  • 对象组成的数组:数组外壳会分离,但数组元素仍可能是共享对象,需要逐个按业务语义处理。

所以规则不是“看到 readonly 就 clone”,而是先画出对象所有权:这个子对象由谁拥有,克隆后是否允许共同变化,复制成本是否可接受。readonly 解决的是属性写入约束,不等于完整的不可变对象模型。

一次写入是硬边界,unset 也算重新初始化流程的一部分

在 __clone() 中,同一个 readonly 属性只能重新初始化一次。先赋回原值再赋新值并不是两阶段更新,而是第二次写入,后一次会报错。RFC 还允许在克隆执行期先 unset(),之后再初始化;这主要服务于惰性属性或魔术访问器,不应成为普通深复制的默认写法。

capturedAt = $this->capturedAt;

    // 第二次写入会抛出 Error,不能把 readonly 当临时变量使用。
    $this->capturedAt = clone $this->capturedAt;
}

PHP 8.4 还明确禁止在 __clone() 中通过引用间接修改 readonly 属性,例如获取 &$this->capturedAt 的引用。迁移旧代码时,应使用清晰的直接赋值,不要依赖引用技巧。

把验证做成双向测试,而不是只看一次输出

好的回归测试至少验证三件事:外层对象不同、需要隔离的嵌套对象不同、任意一侧改变都不会串到另一侧。仅验证初始值相等是不够的,因为浅复制与深复制在刚完成时看起来完全一样。

capturedAt !== $copy->capturedAt);

// 再修改克隆侧,确认原对象仍保持原值。
$copy->capturedAt->modify('+1 day');
assert($original->capturedAt->format('Y-m-d') === '2026-10-10');
assert($copy->capturedAt->format('Y-m-d') === '2026-10-11');

如果类里有多个可变子对象,逐个列入测试。新增 readonly 属性时,也应把“共享还是隔离”作为代码评审项,否则未来一次看似安全的属性扩展就可能重新引入共享状态。

兼容性落地清单

  1. 在 composer.json 和部署镜像中确认最低 PHP 版本;使用重新初始化能力时最低必须是 PHP 8.3。
  2. 枚举 readonly 属性里的对象,标注“不可变、需要隔离、有意共享”三种所有权。
  3. 只在 __clone() 中直接替换需要隔离的属性,每个属性只写一次。
  4. 避免通过引用修改 readonly 属性,尤其是目标环境包含 PHP 8.4 及更高版本时。
  5. 测试对象身份和双向变更,不用单次序列化输出代替引用隔离验证。
  6. 对 PHP 8.1/8.2 保持构造器重建路径,不要让同一套包在低版本运行时才暴露致命错误。

常见问题

readonly 属性里的对象完全不能改吗?

不是。readonly 阻止属性被重新赋值,但对象内部仍可能被修改,这就是“内部可变性”。要完全不可变,应优先保存不可变对象,或在 API 层同时约束其修改方法。

PHP 8.2 中的 __clone() 能重新赋值 readonly 属性吗?

不能。已初始化 readonly 属性在 PHP 8.2 中仍不能于克隆时重新初始化。需要用构造器创建新实例,或把最低运行版本升级到 PHP 8.3。

为什么 clone 后 readonly 的 DateTime 仍会互相影响?

因为默认 clone 是浅复制,两个 readonly 属性槽位都保存了同一个 DateTime 引用。readonly 没有自动深复制语义。

可以在 __clone() 里连续修改同一个 readonly 属性吗?

不可以。每个属性在一次克隆执行中只能重新初始化一次,第二次写入会抛出 Error。

DateTimeImmutable 也需要在 __clone() 中复制吗?

通常不需要。它的修改方法返回新实例,不会原地改变旧实例。除非业务明确要求不同对象身份,否则共享不可变实例更简单。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
json/v2 解码 null 到指针字段的兼容处理json/v2 解码 null 到指针字段的兼容处理
上一篇
json/v2 解码 null 到指针字段的兼容处理
encoding/json/v2 用 StringifyNumbers 兼容旧数字字段
下一篇
encoding/json/v2 用 StringifyNumbers 兼容旧数字字段
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    402次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    478次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    487次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    433次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    260次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码