首页
/ Bevy Reflect 自定义属性迁移指南:从 `CustomAttributes::with_attribute` 到 `CustomAttributesBuilder`

Bevy Reflect 自定义属性迁移指南:从 `CustomAttributes::with_attribute` 到 `CustomAttributesBuilder`

2026-09-05 09:33:21作者:霍妲思

在 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::CustomAttributesbevy_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))
            },
        }
    }
}

这里有两处关键设计:

  1. 空集合零分配attributesOption<Arc<...>>:没有任何属性时存储 None,不为空集合分配 Arc 和映射表。由于反射信息(StructInfoFieldInfo、枚举 VariantInfo 等)几乎总是携带 CustomAttributes 字段,而大多数字段实际上没有自定义属性,这种"空时不占堆内存"的设计对大型 ECS 场景的内存占用影响是实质性的。
  2. 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() 五个方法。从源码调用点看,它被用于以下位置:

一个完整的运行时读取示例(摘自 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 的代码库,迁移是机械式的替换:

  1. 定位所有 CustomAttributes::default().with_attribute(...) 调用(可全局搜索 with_attributeCustomAttributes::default)。
  2. default() 替换为 CustomAttributesBuilder::new()
  3. 将每个 .with_attribute(x) 改为 .attribute(x)
  4. 在链尾追加 .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_attributesDebug 输出格式)以及前述各类 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 语法不受影响。
登录后查看全文
热门项目推荐
相关项目推荐