首页
/ Bevy ECS 迁移指南:Access 的 reads_and_writes 更名为 reads —— 背景、迁移步骤与源码解析

Bevy ECS 迁移指南:Access 的 reads_and_writes 更名为 reads —— 背景、迁移步骤与源码解析

2026-09-05 21:13:54作者:袁立春Spencer

本文围绕 Bevy 官方迁移指南 access_fields_rename.md 展开,讲清 bevy_ecsAccess 类型字段与方法的一次更名(reads_and_writesreads)背后的设计动机、需要修改的具体 API 列表、可直接复制的迁移写法,并结合 crates/bevy_ecs/src/query/access.rscrates/bevy_dev_tools/src/schedule_data/serde.rs 的源码,说明这一更名在当前代码库中的实际落地形态,帮助你完成 ECS 查询访问相关代码的平滑升级。

为什么要把 reads_and_writes 改名为 reads

迁移指南给出的理由非常直接:旧的 Access 至少包含 reads_and_writeswrites 两个成员,而 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 会同时向 readswrites 插入,而 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_writesAccess::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_invertedUnboundedAccessError::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 则进一步把冲突结果压缩为 AccessConflictsAll / Individual(ComponentIdSet) 两种形态,用于生成调度冲突错误信息。可以看到,reads 中"包含 writes"的语义正是冲突判定的前提:如果 reads 不包含写集合,那么"系统 A 写 T、系统 B 写 T"这种冲突就会被漏判。这也是为什么项目方认为旧名 reads_and_writes 不仅冗余而且误导——它让人以为写访问需要单独声明,而实际上 writesreads 的严格子集。

仓库内的实际使用: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 与当前源码,升级时可以按以下清单逐项自查:

  1. 全局搜索 try_reads_and_writes:替换为 try_reads(或当前快照中的 reads().as_finite_set())。旧方法在当前代码库中标注了 #[deprecated(since = "0.20.0", ...)],编译器警告会直接指出待迁移的调用点。
  2. 全局搜索 read_and_writes_inverted:替换为 reads_inverted。该字段是 UnboundedAccessError 结构体的公共字段,通常只出现在错误匹配/构造处。
  3. 顺带检查 try_writes:虽然不在本次迁移指南的列表内,但它与 try_reads_and_writes 成对出现,同样被弃用,建议一并迁移为 writes().as_finite_set()(见 crates/bevy_ecs/src/query/access.rs)。
  4. 验证冲突行为未变化:更名只是语义澄清,is_compatible / get_conflicts 的判定逻辑(crates/bevy_ecs/src/query/access.rs)保持不变;若你的项目有自定义调度器或依赖 Access 的工具,运行现有测试即可确认行为一致。
  5. 关注相关迁移文档:本次更名与 InvertibleComponentIdSet 的引入FilteredAccess 数据暴露等变更属于同一时期对 Access 的系列重构,如需完整迁移背景,可查阅 _release-content/migration_guides.md 中的索引。

小结

Access::reads_and_writesAccess::reads 的更名(对应上游 PR #24778,迁移说明见 access_fields_rename.md)是一次典型的"消除误导性命名"重构:它把"写即含读"这一长期存在的隐含不变式(writes ⊆ reads)显式化到 API 命名中,并同步把 UnboundedAccessError 的字段改为更精确的 reads_inverted。迁移成本极低——公开 API 只有两处更名——但理解其背后的集合语义(有限集 vs. 排除集、archetypal 访问、冲突判定)能帮你正确使用 Access 的相关 API,并避免在自定义调度或工具链代码中踩到无界访问的坑。

登录后查看全文
热门项目推荐
相关项目推荐