Bevy ECS 迁移指南:Access 的 reads_and_writes 更名为 reads —— 背景、迁移步骤与源码解析
本文围绕 Bevy 官方迁移指南 access_fields_rename.md 展开,讲清 bevy_ecs 中 Access 类型字段与方法的一次更名(reads_and_writes → reads)背后的设计动机、需要修改的具体 API 列表、可直接复制的迁移写法,并结合 crates/bevy_ecs/src/query/access.rs 与 crates/bevy_dev_tools/src/schedule_data/serde.rs 的源码,说明这一更名在当前代码库中的实际落地形态,帮助你完成 ECS 查询访问相关代码的平滑升级。
为什么要把 reads_and_writes 改名为 reads
迁移指南给出的理由非常直接:旧的 Access 至少包含 reads_and_writes 和 writes 两个成员,而 reads_and_writes 这个命名具有误导性——它暗示"该集合还需要额外声明写权限",而事实上写访问本身就隐含读访问(能写一个组件,自然能读它)。因此项目方将其直接更名为 reads,语义变为"所有被读取的组件",其中天然包含所有被写入的组件。
这一点在当前源码的文档注释中得到了印证。crates/bevy_ecs/src/query/access.rs 中的 Access 结构体定义如下(第 226–237 行):
#[derive(Eq, PartialEq, Default, Debug)]
pub struct Access {
/// All accessed components.
///
/// Note: this includes those in [`Self::writes`], since a mutable access also allows read-only
/// access.
reads: InvertibleComponentIdSet,
/// All exclusively-accessed components.
writes: InvertibleComponentIdSet,
// Components that are not accessed, but whose presence in an archetype affect query results.
archetypal: ComponentIdSet,
}
可以看到:
reads:所有被访问的组件集合,明确注明包含了writes中的组件("mutable access also allows read-only access");writes:独占(可变)访问的组件集合;archetypal:不参与冲突判断、但会影响查询结果的组件集合,源码注释说明目前仅用于Has<T>与Allow<T>过滤器。
这一不变式(writes ⊆ reads)贯穿整个实现,例如 add_write 会同时向 reads 和 writes 插入,而 remove_read 会连同 writes 一起移除——因为"连读都不允许,更不可能允许写"。
需要改名的公开成员
迁移指南列出了两项公开 API 的重命名:
| 旧名称 | 新名称 |
|---|---|
Access::try_reads_and_writes |
Access::try_reads |
UnboundedAccessError::read_and_writes_inverted |
UnboundedAccessError::reads_inverted |
1. Access::try_reads_and_writes → Access::try_reads
该方法用于返回"被读(含写)访问"的组件集合;当访问集合是无界的(unbounded,即"除了一小组例外,访问所有组件")时无法给出有限集合,于是返回错误。
查看当前仓库中 crates/bevy_ecs/src/query/access.rs(第 471–477 行),旧方法以弃用形式保留,供尚未迁移的代码继续编译:
/// Returns the set of components with read access,
/// or an error if the access is unbounded.
///
/// This includes components with write access, since write access also allows you to read the
/// component.
#[deprecated(since = "0.20.0", note = "use `reads_and_writes().as_finite_set()")]
pub fn try_reads_and_writes(&self) -> Result<&ComponentIdSet, UnboundedAccessError> {
self.reads.as_finite_set().ok_or(UnboundedAccessError {
writes_inverted: self.writes.is_unbounded(),
reads_inverted: self.reads.is_unbounded(),
})
}
而当前代码库推荐的新入口是 Access::reads(第 483–485 行),它返回 &InvertibleComponentIdSet,需要有限集合时调用其 as_finite_set():
/// Returns the set of components with read or write access.
///
/// This includes components with write access, since write access also allows you to read the
/// component.
pub fn reads(&self) -> &InvertibleComponentIdSet {
&self.reads
}
因此迁移时的对应关系为:
// 迁移前
let set = access.try_reads_and_writes()?;
// 迁移后(按迁移指南)
let set = access.try_reads()?;
// 当前代码库中等价的新写法
let set = access.reads().as_finite_set().ok_or(err)?;
同样的模式也适用于写访问:try_writes 已同样标注 #[deprecated(since = "0.20.0", note = "use writes().as_finite_set()")],见 crates/bevy_ecs/src/query/access.rs(第 489–495 行),替换为 access.writes().as_finite_set()。
适用前提:以上弃用标注与替代 API 以当前仓库快照(crates/bevy_ecs/src/query/access.rs)为准。如果你跟随的是迁移指南发布时的版本,请直接使用
try_reads;在更新的快照中,try_reads进一步演化为reads()+InvertibleComponentIdSet的形态。
2. UnboundedAccessError::read_and_writes_inverted → UnboundedAccessError::reads_inverted
UnboundedAccessError 是上面这些 try_* 方法的错误类型。当前实现见 crates/bevy_ecs/src/query/access.rs(第 556–567 行):
/// Error returned when attempting to iterate over items included in an [`Access`]
/// if the access excludes items rather than including them.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
#[error("Access is unbounded")]
pub struct UnboundedAccessError {
/// [`Access`] is defined in terms of _excluding_ exclusive
/// access.
pub writes_inverted: bool,
/// [`Access`] is defined in terms of _excluding_ shared and
/// exclusive access.
pub reads_inverted: bool,
}
要点说明:
- 错误由两个布尔字段组成:
writes_inverted表示写集合以"排除法"定义;reads_inverted(即更名后的字段)表示读集合以"排除法"定义。 - 之所以需要区分,是因为
Access内部基于 InvertibleComponentIdSet 实现(第 11–18 行),它可以是Included(有限集)或Excluded(有限集)(即全集补集)。典型触发场景:对一个查询先调用read_all()表示"访问所有组件"(如EntityRef、&World场景),再移除个别组件——此时集合变为"排除法"定义,as_finite_set()返回None,从而产生该错误。 - 如果你在代码里对错误做了结构体模式匹配,例如:
// 迁移前
Err(UnboundedAccessError { read_and_writes_inverted: true, .. }) => { /* ... */ }
// 迁移后
Err(UnboundedAccessError { reads_inverted: true, .. }) => { /* ... */ }
只需把字段名 read_and_writes_inverted 替换为 reads_inverted 即可,语义完全一致:都表示读集合处于"反转/排除"状态。
迁移后的完整使用方式:try_iter_access 示例
除了直接取集合,Access 还提供了更贴近"遍历每个组件的访问级别"的 API try_iter_access(第 502–553 行),它把读取集合按写权限归类,并额外输出 archetypal 访问,同样在无界时返回 UnboundedAccessError。源码自带可复制的示例(第 510–531 行):
# use bevy_ecs::query::{Access, ComponentAccessKind};
# use bevy_ecs::component::ComponentId;
let mut access = Access::default();
access.add_read(ComponentId::new(1));
access.add_write(ComponentId::new(2));
access.add_archetypal(ComponentId::new(3));
let result = access
.try_iter_access()
.map(Iterator::collect::<Vec<_>>);
assert_eq!(
result,
Ok(vec![
ComponentAccessKind::Shared(ComponentId::new(1)),
ComponentAccessKind::Exclusive(ComponentId::new(2)),
ComponentAccessKind::Archetypal(ComponentId::new(3)),
]),
);
其中 ComponentAccessKind(第 570–578 行)有三个变体:Archetypal(如 Has<Foo>)、Shared(如 &Foo)、Exclusive(如 &mut Foo)。从源码结构看,这一枚举是 try_iter_access 在"读取集合中区分独占/共享"时的输出载体,也是 UnboundedAccessError 文档注释中两个 inverted 字段语义的直接依据。
更名如何服务于冲突检测
理解这次更名,离不开 Access 的核心用途:它是 Bevy ECS 在系统初始化与调度时保证内存安全(soundness)的基础数据结构(见 crates/bevy_ecs/src/query/access.rs 结构体上方的文档注释)。其冲突判定规则由 is_compatible 给出(第 426–434 行):
pub fn is_compatible(&self, other: &Access) -> bool {
// We have a conflict if we write and they read or write, or if they
// write and we read or write.
self.writes.is_disjoint(&other.reads) && other.writes.is_disjoint(&self.reads)
}
即"我写的与它读的/写的交集为空,且它写的与我读的/写的交集为空"才兼容。get_conflicts 则进一步把冲突结果压缩为 AccessConflicts 的 All / Individual(ComponentIdSet) 两种形态,用于生成调度冲突错误信息。可以看到,reads 中"包含 writes"的语义正是冲突判定的前提:如果 reads 不包含写集合,那么"系统 A 写 T、系统 B 写 T"这种冲突就会被漏判。这也是为什么项目方认为旧名 reads_and_writes 不仅冗余而且误导——它让人以为写访问需要单独声明,而实际上 writes 是 reads 的严格子集。
仓库内的实际使用:bevy_dev_tools 的调度数据序列化
该更名并非孤立的 API 调整,仓库内的下游模块已经统一使用新语义。以 crates/bevy_dev_tools/src/schedule_data/serde.rs 为例(第 103–147 行),它把 bevy_ecs::query::Access 序列化为可持久化的 AccessData,供调度数据导出使用:
pub struct AccessData {
/// All accessed components, or forbidden components if
/// `Self::reads_inverted` is set.
pub reads: Vec<usize>,
/// All exclusively-accessed components, or components that may not be
/// exclusively accessed if `Self::writes_inverted` is set.
pub writes: Vec<usize>,
/// Is `true` if this component can read all components *except* those
/// present in `Self::reads`.
pub reads_inverted: bool,
/// Is `true` if this component can write to all components *except* those
/// present in `Self::writes`.
pub writes_inverted: bool,
/// Components that are not accessed, but whose presence in an archetype affect query results.
pub archetypal: Vec<usize>,
}
其构造函数直接调用了新 API value.reads().as_finite_set() 与 value.writes().as_finite_set()(第 128–129 行),并在第 123 行留下了关键注释:
// NOTE: `try_reads` returns error if `reads_inverted=true`,
// thus `AccessData` always has `reads_inverted=false`
这段注释恰好同时佐证了两件事:更名后的 reads 系列 API 在工具链中的调用惯例,以及 UnboundedAccessError::reads_inverted 字段的确切含义(读集合处于排除法状态)。
迁移检查清单
按 access_fields_rename.md 与当前源码,升级时可以按以下清单逐项自查:
- 全局搜索
try_reads_and_writes:替换为try_reads(或当前快照中的reads().as_finite_set())。旧方法在当前代码库中标注了#[deprecated(since = "0.20.0", ...)],编译器警告会直接指出待迁移的调用点。 - 全局搜索
read_and_writes_inverted:替换为reads_inverted。该字段是UnboundedAccessError结构体的公共字段,通常只出现在错误匹配/构造处。 - 顺带检查
try_writes:虽然不在本次迁移指南的列表内,但它与try_reads_and_writes成对出现,同样被弃用,建议一并迁移为writes().as_finite_set()(见 crates/bevy_ecs/src/query/access.rs)。 - 验证冲突行为未变化:更名只是语义澄清,
is_compatible/get_conflicts的判定逻辑(crates/bevy_ecs/src/query/access.rs)保持不变;若你的项目有自定义调度器或依赖Access的工具,运行现有测试即可确认行为一致。 - 关注相关迁移文档:本次更名与 InvertibleComponentIdSet 的引入、
FilteredAccess数据暴露等变更属于同一时期对Access的系列重构,如需完整迁移背景,可查阅 _release-content/migration_guides.md 中的索引。
小结
Access::reads_and_writes → Access::reads 的更名(对应上游 PR #24778,迁移说明见 access_fields_rename.md)是一次典型的"消除误导性命名"重构:它把"写即含读"这一长期存在的隐含不变式(writes ⊆ reads)显式化到 API 命名中,并同步把 UnboundedAccessError 的字段改为更精确的 reads_inverted。迁移成本极低——公开 API 只有两处更名——但理解其背后的集合语义(有限集 vs. 排除集、archetypal 访问、冲突判定)能帮你正确使用 Access 的相关 API,并避免在自定义调度或工具链代码中踩到无界访问的坑。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00