Any特征在Rust开发的早期引入,以提供动态类型能力,主要用于无法获得编译时类型信息的错误处理和调试场景。它的设计类似于其他语言中的概念,如C++的typeid或Java的instanceof,但Rust的所有权模型施加了独特的限制。'static要求源于确保类型擦除引用永远不超过它们描述的数据的生命周期,防止在没有垃圾收集的语言中出现使用后释放错误。
如果没有**'static限制,作为Any擦除的类型可能包含对具有有限生命周期的栈本地数据的引用。如果Any特征对象超出了该栈帧,降级和解引用将访问已释放的内存。由于Any通过虚表和类型擦除进行操作,编译器无法在降级时验证生命周期;'static**限制作为一种保守保证,确保类型拥有其所有数据或仅持有静态引用,确保擦除边界的内存安全。
Any特征定义trait Any: 'static利用Rust的特征边界系统在编译时强制执行该限制。只有不包含非静态引用的类型才能实现Any,这保证了任何&dyn Any或**Box<dyn Any>**在整个程序持续时间内保持有效。这允许通过downcast_ref()和downcast_mut()进行安全的降级,因为底层数据有保证在作用域退出时不会失效。
我们正在为一个游戏引擎构建插件系统,其中脚本可以注册返回任意数据的事件处理程序。引擎需要在异构队列中存储这些返回值,以便由不同的子系统随后处理,这需要类型擦除以在单个集合中存储不同类型。然而,一些脚本绑定尝试返回对脚本执行上下文内临时本地变量的引用,这在脚本帧完成后会变成悬空引用。
解决方案1:带生命周期参数的自定义特征
一种方法是创建一个自定义特征PluginResult,具有与生命周期参数相关的类型,允许引擎通过特征对象跟踪生命周期。这承诺灵活性,允许借用数据,但需要在整个插件API表面进行复杂的生命周期注释。这种复杂性将迫使每个插件作者理解高级Rust生命周期机制,造成不可接受的陡峭学习曲线,并增加第三方代码中微妙生命周期错误的风险。
解决方案2:不安全的生命周期转化
另一个解决方案提议使用unsafe代码在存储数据时转化生命周期,基本上承诺引擎将在源作用域退出之前放弃所有引用。虽然这允许所需的API人机工程学,但将内存安全的负担完全放在引擎开发者身上。跟踪引用来源的任何错误都会导致可利用的使用后释放漏洞,违反了Rust的安全保证,并使代码库审计变得困难。
我们选择要求所有插件返回值实现带有**'static限制的Any**,强制脚本作者返回拥有的数据或Arc包装的共享状态。这个决定放弃了零复制引用的一些理论性能优势,以保证引擎的事件队列可以安全地存储和异步处理数据。结果是一个可靠的插件API,在公共接口中没有unsafe代码,尽管需要为以前依赖于临时借用的类型添加序列化层。
为什么Any需要'static而不仅仅是用于创建特征对象的引用的生命周期?
Any特征在编译时擦除类型信息以生成虚表,过程中丢失所有生命周期数据。当你创建&dyn Any时,编译器无法将原始生命周期'a编码到特征对象中,以便稍后降级机械可以验证。要求**'static是确保底层类型不包含悬空指针而无需运行时生命周期跟踪的唯一方法。如果Any接受较短的生命周期,虚表指针本身将不得不携带生命周期元数据,这将要求Rust**实现依赖类型或运行时借用检查,基本改变语言的零成本抽象模型。
当原始类型包含非静态引用时,Box<dyn Any>如何与'static限制交互?
像struct Wrapper<'a>(&'a str)这样的类型无法实现Any,因为它不满足**'static特征边界。因此,不能从Wrapper<'a>实例创建Box<dyn Any>。候选人常常错误地认为把值放入盒子中可以延长其生命周期;然而,Box仅拥有堆上的分配,而不拥有该分配内字段引用的数据。如果引用的数据是栈本地的,将外部结构移动到堆中并不会延长引用的生命周期,因此编译器正确地拒绝转换为Box<dyn Any>**。这防止了一个堆分配的盒子超出包含引用数据的栈帧的情况。
可以安全地使用unsafe代码和手动生命周期跟踪实现自定义Any特征,放宽'static要求吗?
虽然技术上可以使用unsafe来转化生命周期和自定义虚表,但这样的实现将是不安全的,因为Rust的特征系统和借用检查器无法验证降级位置的生命周期不变性。你需要实现一个并行的类型系统,在运行时跟踪生命周期,检查每次访问时原始作用域是否仍然存在。这种方法基本上重新实现了垃圾收集器或引用计数系统,失去了Rust的编译时保证。此外,任何unsafe实现将不安全地与标准库组件交互,这些组件期望Any的不变性,在与std::any::Any特征对象混合时导致未定义行为。