Bevy Reflect 自定义属性迁移指南:从 `CustomAttributes::with_attribute` 到 `CustomAttributesBuilder`
在 Bevy 的反射系统(bevy_reflect)中,自定义属性(CustomAttributes)是挂载到类型、字段和枚举变体上的元数据容器。本指南聚焦于一次重要的 API 破坏性变更:CustomAttributes::with_attribute 方法已被移除,取而代之的是独立的 CustomAttributesBuilder 构建器。读完后,你将能够把旧版本代码无损迁移到新 API,并理解这次变更背后的内存优化设计,以及反射属性从定义、derive 生成到运行时查询的完整链路。
变更概述:with_attribute 被构建器取代
迁移指南(_release-content/migration-guides/custom_attributes.md,对应 PR #24171)记录的旧写法是:
// 旧版本写法(已移除)
let custom_attributes = CustomAttributes::default()
.with_attribute("my attribute")
.with_attribute(123);
新写法统一通过 CustomAttributesBuilder 完成:
// 当前版本写法
let custom_attributes = CustomAttributesBuilder::new()
.attribute("my attribute")
.attribute(123)
.build();
在当前仓库源码中,with_attribute 已不存在于任何公开 API 中——全仓库检索仅能在这份迁移指南的旧示例里找到它的痕迹。替代方案实现在 attributes.rs 中:
// crates/bevy_reflect/src/attributes.rs
/// Builder for [`CustomAttributes`].
#[derive(Default)]
pub struct CustomAttributesBuilder {
attributes: TypeIdIndexMap<CustomAttribute>,
}
impl CustomAttributesBuilder {
/// Creates a new, empty builder.
pub fn new() -> Self { ... }
/// Adds a single attribute to the builder.
pub fn attribute<T: Reflect>(self, value: T) -> Self { ... }
/// Consumes the builder, returning the final [`CustomAttributes`].
pub fn build(self) -> CustomAttributes { ... }
}
该模块通过 lib.rs 中的 pub mod attributes; 公开,因此完整路径为 bevy_reflect::attributes::CustomAttributes 与 bevy_reflect::attributes::CustomAttributesBuilder。
为什么破坏性改动:CustomAttributes 的内存优化
迁移指南指出,这次改动是 "CustomAttributes 内部内存优化的副作用"。从当前源码结构可以清楚看到优化的具体形态。CustomAttributes 的定义为:
// crates/bevy_reflect/src/attributes.rs (L40-L54)
#[derive(Default, Clone)]
pub struct CustomAttributes {
attributes: Option<Arc<TypeIdIndexMap<CustomAttribute>>>,
}
impl CustomAttributes {
fn new(attributes: TypeIdIndexMap<CustomAttribute>) -> Self {
Self {
attributes: if attributes.is_empty() {
None
} else {
Some(Arc::new(attributes))
},
}
}
}
这里有两处关键设计:
- 空集合零分配。
attributes是Option<Arc<...>>:没有任何属性时存储None,不为空集合分配Arc和映射表。由于反射信息(StructInfo、FieldInfo、枚举VariantInfo等)几乎总是携带CustomAttributes字段,而大多数字段实际上没有自定义属性,这种"空时不占堆内存"的设计对大型 ECS 场景的内存占用影响是实质性的。 Arc共享引用。构建后的属性集合同一实例可以被多个地方共享(如字段信息的克隆),Clone派生只克隆Arc指针而非整个映射表。
正是由于存储从"内部直接持有可变集合"变成了"构建后冻结的 Arc 共享结构",&self -> Self 式的链式 with_attribute 方法无法在保持 Arc 共享的前提下实现原地增改,因此被重构为"可变构建期 + 不可变运行期"的经典构建器模式——这正是迁移指南所说的"副作用"。
构建器侧同样有一处与内存/编译开销相关的细节。attribute 方法通过一个擦除版本的内部方法落地写入:
// crates/bevy_reflect/src/attributes.rs (L219-L230)
pub fn attribute<T: Reflect>(self, value: T) -> Self {
self.attribute_erased(TypeId::of::<T>(), CustomAttribute::new(value))
}
// Erased version of `attribute` with inlining disabled. This reduces
// monomorphization costs, and avoids excessive inlining in cold generated
// code.
#[inline(never)]
fn attribute_erased(mut self, type_id: TypeId, value: CustomAttribute) -> Self {
self.attributes.insert(type_id, value);
self
}
源码注释明确说明:擦除版方法禁用了内联,以降低单态化(monomorphization)成本,避免在 derive 宏生成的冷代码中产生过量内联。#[derive(Reflect)] 会为每个类型的每个属性各生成一条 .attribute(...) 调用链,这一优化让生成代码的编译产物更紧凑。
语义不变:按 TypeId 存储,同一类型只保留一个属性
迁移只是构建方式的变化,CustomAttributes 的存储语义保持不变:属性按其 TypeId 索引,因此每种类型的属性最多只有一个。文档注释中写明:
Attributes are stored by their
TypeId. Because of this, there can only be one attribute per type.
(见 attributes.rs 的模块文档。)
由于构建器内部是 TypeIdIndexMap,重复添加同一类型的属性时后者覆盖前者。这一行为由单元测试 should_accept_last_attribute 固化:
// crates/bevy_reflect/src/attributes.rs 测试模块
#[derive(Reflect)]
struct Foo {
#[reflect(@false)]
#[reflect(@true)]
value: i32,
}
// ...
let field = info.field("value").unwrap();
assert!(field.get_attribute::<bool>().unwrap());
查询 API:运行时如何读取自定义属性
CustomAttributes 提供了完整的运行时查询接口(attributes.rs):
| 方法 | 签名 | 说明 |
|---|---|---|
| 按类型判断存在 | contains<T: Reflect>(&self) -> bool |
是否存在类型 T 的属性 |
按 TypeId 判断存在 |
contains_by_id(&self, id: TypeId) -> bool |
动态版本的 contains |
| 按类型读取 | get<T: Reflect>(&self) -> Option<&T> |
返回属性值的引用 |
按 TypeId 读取 |
get_by_id(&self, id: TypeId) -> Option<&dyn Reflect> |
返回 Reflect 对象引用 |
| 迭代全部 | iter(&self) -> impl Iterator<Item = (&TypeId, &dyn Reflect)> |
遍历所有属性 |
| 数量 | len(&self) -> usize |
属性个数 |
| 判空 | is_empty(&self) -> bool |
是否为空集合 |
这些能力通过 impl_custom_attribute_methods! 宏(attributes.rs)批量暴露到所有反射信息类型上。宏为宿主类型生成 custom_attributes()、get_attribute<T>()、get_attribute_by_id()、has_attribute<T>()、has_attribute_by_id() 五个方法。从源码调用点看,它被用于以下位置:
- 结构体信息:structs.rs、tuple_struct.rs
- 字段信息:fields.rs
- 枚举信息:enum_trait.rs
- 枚举变体(单元/元组/结构体三种变体):variants.rs
一个完整的运行时读取示例(摘自 attributes.rs 的文档测试):
# use bevy_reflect::{Reflect, Typed, TypeInfo};
use core::ops::RangeInclusive;
#[derive(Reflect)]
struct Slider {
#[reflect(@RangeInclusive::<f32>::new(0.0, 1.0))]
value: f32
}
let TypeInfo::Struct(info) = <Slider as Typed>::type_info() else {
panic!("expected struct info");
};
let range = info.field("value").unwrap().get_attribute::<RangeInclusive<f32>>().unwrap();
assert_eq!(0.0..=1.0, *range);
derive 宏侧:@ 语法生成的正是 Builder 调用链
#[derive(Reflect)] 中的 #[reflect(@...)] 注解就是自定义属性的主要来源。解析与代码生成逻辑位于 derive/src/custom_attributes.rs:
/// Parse `@` (custom attribute) attribute.
///
/// Examples:
/// - `#[reflect(@Foo))]`
/// - `#[reflect(@Bar::baz("qux"))]`
/// - `#[reflect(@0..256u8)]`
pub fn parse_custom_attribute(&mut self, input: ParseStream) -> syn::Result<()> {
input.parse::<Token![@]>()?;
self.push(input.parse()?)
}
其 to_tokens 方法生成的正是本次迁移涉及的新 API 调用链:
// crates/bevy_reflect/derive/src/custom_attributes.rs (L12-L23)
pub fn to_tokens(&self, bevy_reflect_path: &Path) -> TokenStream {
let attributes = self.attributes.iter().map(|value| {
quote! {
.attribute(#value)
}
});
quote! {
#bevy_reflect_path::attributes::CustomAttributesBuilder::new()
#(#attributes)*.build()
}
}
也就是说,derive 宏在编译期直接输出 CustomAttributesBuilder::new().attribute(...).build() 形式的代码。属性值表达式(如 @0.0..=1.0、@RangeInclusive::<f32>::new(0.0, 1.0)、@Tooltip::new("..."))原样展开为 .attribute(...) 的参数。这也解释了为何 attribute 方法必须接受任意 T: Reflect 值:derive 生成的代码在编译期并不知道每个属性的具体类型,而是依赖泛型单态化逐个实例化。
支持 @ 语法的位置覆盖面由 attributes.rs 的测试矩阵完整验证,包括:结构体容器、结构体字段、元组结构体容器与字段、枚举容器、枚举变体、枚举变体字段,以及单元结构体(unit struct)作为属性值的使用。
迁移操作步骤
对使用旧 API 的代码库,迁移是机械式的替换:
- 定位所有
CustomAttributes::default().with_attribute(...)调用(可全局搜索with_attribute与CustomAttributes::default)。 - 将
default()替换为CustomAttributesBuilder::new(); - 将每个
.with_attribute(x)改为.attribute(x); - 在链尾追加
.build(),得到最终的CustomAttributes值。
前后对照:
// 旧版本
let custom_attributes = CustomAttributes::default()
.with_attribute("my attribute")
.with_attribute(123);
// 当前版本
let custom_attributes = CustomAttributesBuilder::new()
.attribute("my attribute")
.attribute(123)
.build();
由于 CustomAttributesBuilder 同样派生了 Default,如果你的代码曾依赖 CustomAttributes::default() 产生空集合并直接传入构建器之外的场景,CustomAttributes::default() 依然可用(结构体本身仍派生 Default,空集合即 None 存储),只有链式追加属性的方法被移除。
若代码是通过 #[derive(Reflect)] 的 @ 语法声明属性的,则无需任何修改——derive 宏生成的代码已经指向新的 Builder 路径,旧 API 只影响手动构造 CustomAttributes 的代码。
验证方式
变更行为的回归测试集中在 attributes.rs 的测试模块中,可在仓库根目录运行:
cargo test -p bevy_reflect attributes::tests
关键用例包括:should_get_custom_attribute(按类型读取)、should_get_custom_attribute_dynamically(按 TypeId 动态读取并通过 reflect_partial_eq 比较)、should_iterate_custom_attribute(迭代语义)、should_debug_custom_attributes(Debug 输出格式)以及前述各类 should_derive_custom_attributes_on_* 用例(覆盖所有 derive 注解位置)。
小结
CustomAttributes::with_attribute已移除,统一使用bevy_reflect::attributes::CustomAttributesBuilder::new().attribute(...).build()构建。- 该变更源于内部存储改为
Option<Arc<TypeIdIndexMap<CustomAttribute>>>:空属性集不分配堆内存,非空集合以Arc共享,同时#[inline(never)]的擦除写入方法降低 derive 生成代码的单态化成本。 - 存储与查询语义不变:按
TypeId索引、同类型只留最后一个、get/contains/iter接口齐全;#[reflect(@...)]derive 语法不受影响。
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