如果有必要,如何在Rust中添加多态trait对象的反序列化?

19
我正在尝试解决序列化和反序列化Box<SomeTrait>的问题。我知道在封闭类型层次结构的情况下,推荐的方法是使用枚举,并且它们的序列化没有问题,但在我的情况下,使用枚举是不合适的解决方案。
起初,我尝试使用Serde,因为它是Rust序列化机制的事实标准。 Serde能够序列化Box<X>,但在X是特质的情况下无法序列化。Serialize特质不能为特质对象实现,因为它具有通用方法。可以通过使用erased-serde来解决此特定问题,以便可以对Box<SomeTrait>进行序列化。
主要问题是反序列化。要反序列化多态类型,您需要在序列化数据中有某些类型标记。首先应该反序列化此标记,然后使用它来动态获取将返回Box<SomeTrait>的函数。

std::any::TypeId可以用作标记类型,但主要问题是如何动态获取反序列化函数。我不考虑为每个多态类型注册一个函数的选项,因为需要在应用程序初始化期间手动调用。

我知道两种可能的方法:

  1. 像C#这样具有运行时反射功能的语言可以使用它来获取反序列化方法。
  2. 在C++中,cereal库使用静态对象的技巧,在库初始化时将反序列化器注册到静态映射中。

但是,Rust中没有这些选项。如果有的话,如何添加多态对象反序列化呢?


1
但在我的情况下,使用枚举是不合适的解决方案。我们能知道为什么吗?听起来好像使用枚举整个问题都会消失。请注意,在erased_serde中的类型擦除发生在Deserializer上,而不是正在被反序列化的对象上。 - E net4
1
“我们能知道为什么吗?”我试图尽可能减少依赖关系。我在一些非常小而基础的模块中有一个特质,以及很多依赖于它的代码。我想要将每个实现此特质的模块分开。此外,使用这种方法还有另一个缺点。 - Dmitry Gordon
1
抱歉,我不小心在评论未完成时发布了它。此外,使用枚举的另一个缺点是:如果所有枚举子类型中都有相同的方法,则在枚举上调用它时,您必须添加“match”结构,并为每种类型添加类似的情况。 “erased_serde中的类型擦除发生在Deserializer上,而不是正在反序列化的对象上。” 是的,我知道。我在问题中提到,erased_serder只解决序列化问题,而不解决反序列化问题。 - Dmitry Gordon
您可能会对 Rust 包 enum_dispatch 感兴趣,它可以从另一个方向解决您的程序问题。它提供了一个派生宏,可以自动生成样板代码 match 表达式,自动实现枚举类型的 traits。 - Qwertycrackers
3个回答

11

这已经被 dtolnay 实现

这个概念非常巧妙,可以在 README 中找到解释:

它是如何工作的?

我们使用 inventory crate 来生成您的 trait 的 impls 注册表,该注册表基于 ctor crate 构建以连接插入注册表的初始化函数。第一个 Box<dyn Trait> 反序列化将执行迭代注册表并构建标记到反序列化函数的映射的工作。随后的反序列化在该映射中查找正确的反序列化函数。erased-serde crate 也参与其中,以一种不破坏对象安全性的方式完成所有操作。

总之,每个声明为 [de]serializable 的 trait 实现都在编译时注册,并且在对 trait 对象进行 [de]serialization 时在运行时解析。


这个错误是因为无法对泛型特性进行反序列化处理。你可以尝试使用#[typetag::serialize]来生成仅支持序列化的代码。你试过这个方法了吗? - undefined
@curious 很抱歉,我已经有一段时间没有使用那个功能了,而且我完全没有时间去查看它。如果你敢尝试的话,你应该在社区聊天中询问,或者在这里开一个新的问题。 - undefined

-1

你的所有库都可以提供一个由std::sync::Once保护的注册例程,将某个标识符注册到公共的static mut中,但显然你的程序必须调用它们所有。

我不知道TypeId在使用不同依赖项重新编译时是否会产生一致的值。


-1
可以创建一个库来实现这个功能。为了创建这样的库,我们需要在使用该库之前从TypeId到类型名称创建一个双向映射,并将其用于带有类型标记的序列化/反序列化。可以有一个函数来注册不属于您包的类型,并提供一个宏注释,自动为在您的包中声明的类型执行此操作。
如果有一种方法可以在宏中访问类型ID,那么这将是一种在编译时而不是运行时仪器化TypeId和类型名称之间映射的好方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接