YAOTU INSIGHTS

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

Bevy Reflect 自定义属性迁移指南:从 `CustomAttributes::with_attribute` 到 `CustomAttributesBuilder`
Bevy Reflect 自定义属性迁移指南从CustomAttributes::with_attribute到CustomAttributesBuilder【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy在 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: TypeIdIndexMapCustomAttribute, } impl CustomAttributesBuilder { /// Creates a new, empty builder. pub fn new() - Self { ... } /// Adds a single attribute to the builder. pub fn attributeT: 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: OptionArcTypeIdIndexMapCustomAttribute, } impl CustomAttributes { fn new(attributes: TypeIdIndexMapCustomAttribute) - Self { Self { attributes: if attributes.is_empty() { None } else { Some(Arc::new(attributes)) }, } } }这里有两处关键设计空集合零分配。attributes是OptionArc...没有任何属性时存储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 attributeT: 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 theirTypeId. 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方法签名说明按类型判断存在containsT: Reflect(self) - bool是否存在类型T的属性按TypeId判断存在contains_by_id(self, id: TypeId) - bool动态版本的contains按类型读取getT: Reflect(self) - OptionT返回属性值的引用按TypeId读取get_by_id(self, id: TypeId) - Optiondyn Reflect返回Reflect对象引用迭代全部iter(self) - impl IteratorItem (TypeId, dyn Reflect)遍历所有属性数量len(self) - usize属性个数判空is_empty(self) - bool是否为空集合这些能力通过impl_custom_attribute_methods!宏attributes.rs批量暴露到所有反射信息类型上。宏为宿主类型生成custom_attributes()、get_attributeT()、get_attribute_by_id()、has_attributeT()、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::RangeInclusivef32().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_attributesDebug输出格式以及前述各类should_derive_custom_attributes_on_*用例覆盖所有 derive 注解位置。小结CustomAttributes::with_attribute已移除统一使用bevy_reflect::attributes::CustomAttributesBuilder::new().attribute(...).build()构建。该变更源于内部存储改为OptionArcTypeIdIndexMapCustomAttribute空属性集不分配堆内存非空集合以Arc共享同时#[inline(never)]的擦除写入方法降低 derive 生成代码的单态化成本。存储与查询语义不变按TypeId索引、同类型只留最后一个、get/contains/iter接口齐全#[reflect(...)]derive 语法不受影响。【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考