如何将反序列化为trait,而不是具体类型?

11

我正在尝试进行结构体序列化,其中字节最终将通过管道发送,重建后并对其调用方法。

我创建了一个适当的 trait 供这些结构体实现,并使用 serde 和 serde-cbor 进行序列化:

extern crate serde_cbor;
#[macro_use]
extern crate serde_derive;
extern crate serde;

use serde_cbor::ser::*;
use serde_cbor::de::*;

trait Contract {
    fn do_something(&self);
}

#[derive(Debug, Serialize, Deserialize)]
struct Foo {
    x: u32,
    y: u32,
}

#[derive(Debug, Serialize, Deserialize)]
struct Bar {
    data: Vec<Foo>,
}

#[derive(Debug, Serialize, Deserialize)]
struct Baz {
    data: Vec<Foo>,
    tag: String,
}

impl Contract for Bar {
    fn do_something(&self) {
        println!("I'm a Bar and this is my data {:?}", self.data);
    }
}

impl Contract for Baz {
    fn do_something(&self) {
        println!("I'm Baz {} and this is my data {:?}", self.tag, self.data);
    }
}

fn main() {
    let data = Bar { data: vec![Foo { x: 1, y: 2 }, Foo { x: 3, y: 4 }, Foo { x: 7, y: 8 }] };
    data.do_something();

    let value = to_vec(&data).unwrap();
    let res: Result<Contract, _> = from_reader(&value[..]);
    let res = res.unwrap();
    println!("{:?}", res);
    res.do_something();
}
当我尝试使用trait作为类型来重构字节(假设我不知道正在发送哪个基础对象),编译器会抱怨该trait没有实现Sized trait:

当我尝试使用trait作为类型来重构字节(假设我不知道正在发送哪个基础对象),编译器会抱怨该trait没有实现Sized trait:

error[E0277]: the trait bound `Contract: std::marker::Sized` is not satisfied
  --> src/main.rs:52:15
   |
52 |     let res: Result<Contract, _> = from_reader(&value[..]);
   |              ^^^^^^^^^^^^^^^^^^^ the trait `std::marker::Sized` is not implemented for `Contract`
   |
   = note: `Contract` does not have a constant size known at compile-time
   = note: required by `std::result::Result`
我猜这是因为编译器不知道结构体应该有多大并且不知道如何对齐字节。如果我更改反序列化对象的那一行来指定实际的结构体类型,它就可以工作:
let res: Result<Bar, _> = from_reader(&value[..]);
有没有更好的模式来实现这种序列化+多态行为?

2
我认为你做不到。除非你知道结构体的具体类型,否则无法恢复该结构体;除非你拥有指向其虚函数表的指针,否则无法调用其方法——而你无法确定其虚函数表,除非你可以访问其具体类型。你能序列化一个虚函数表吗? - trent
看起来是这样,但我希望有人能指出我所忽略的东西。我有一个非惯用的解决方案,但会增加代码的耦合性...所以我正在寻找更好的解决方案。 - Dash83
3
你确定你需要多态性而不仅仅是一个枚举吗?你的代码是否需要与用户提供的类型一起工作? - oli_obk
我……你知道的……但是……不行。你是正确的,@ker。当使用与数据相关联的枚举时,“非习惯用法”的解决方案变得更加自然。我一直试图将枚举用作标准C枚举,但我可以改变我的设计来使用枚举。如果您将您的建议发布为答案,我会接受它。 - Dash83
1
将反序列化为一个实现,该实现还为所有其他“Contract”实现实现了“Into”怎么样? - w.brian
@w.brian,听起来很有道理,但最终我选择了将我的设计改为使用实现合同的类型枚举和相应类型的关联结构体,这样序列化无缺陷,然后我对枚举进行模式匹配并操作相关数据。从一开始就采用这种设计可能更好,但有时候你必须先走错路。 - Dash83
3个回答

10

看起来你陷入了我从C++转到Rust时所陷入的同样的陷阱。试图使用多态性来模拟类型的一组固定变体。Rust的枚举(类似于Haskell的枚举,相当于Ada的变体记录类型)与其他语言中的经典枚举不同,因为枚举变体可以有自己的字段。

我建议你将代码更改为

#[derive(Debug, Serialize, Deserialize)]
enum Contract {
    Bar { data: Vec<Foo> },
    Baz { data: Vec<Foo>, tag: String },
}

#[derive(Debug, Serialize, Deserialize)]
struct Foo {
    x: u32,
    y: u32,
}

impl Contract {
    fn do_something(&self) {
        match *self {
            Contract::Bar { ref data } => println!("I'm a Bar and this is my data {:?}", data),
            Contract::Baz { ref data, ref tag } => {
                println!("I'm Baz {} and this is my data {:?}", tag, data)
            }
        }
    }
}

使用Bar和Baz结构作为枚举的相关数据,但在设计方面基本上保持了这种风格。谢谢! - Dash83
1
如果从具有类型参数的特质中存在任意类型集合,该怎么办? - Shisoft
@Shisoft 不太确定我理解了。为什么不开一个新问题呢? - oli_obk

9
你可以使用typetag来解决这个问题。将#[typetag::serde](或者像这里展示的::deserialize)添加到特质和每个实现中:
use serde::Deserialize;

#[typetag::deserialize(tag = "driver")]
trait Contract {
    fn do_something(&self);
}

#[derive(Debug, Deserialize, PartialEq)]
struct File {
    path: String,
}

#[typetag::deserialize(name = "file")]
impl Contract for File {
    fn do_something(&self) {
        eprintln!("I'm a File {}", self.path);
    }
}

#[derive(Debug, Deserialize, PartialEq)]
struct Http {
    port: u16,
    endpoint: String,
}

#[typetag::deserialize(name = "http")]
impl Contract for Http {
    fn do_something(&self) {
        eprintln!("I'm an Http {}:{}", self.endpoint, self.port);
    }
}

fn main() {
    let f = r#"
{
  "driver": "file",
  "path": "/var/log/foo"
}
"#;

    let h = r#"
{
  "driver": "http",
  "port": 8080,
  "endpoint": "/api/bar"
}
"#;

    let f: Box<dyn Contract> = serde_json::from_str(f).unwrap();
    f.do_something();

    let h: Box<dyn Contract> = serde_json::from_str(h).unwrap();
    h.do_something();
}

[dependencies]
serde_json = "1.0.57"
serde = { version = "1.0.114", features = ["derive"] }
typetag = "0.1.5"

参见:


谢谢你的回答,Shep!这是受到我刚刚在另一个问题中收到的答案的启发吗?https://dev59.com/m7Xna4cB1Zd3GeqPQ82R#63272188 - Dash83
1
@Dash83,实际上这是由于如何使用serde反序列化成分散的分层配置?引起的,我想将其关闭为此问题的重复项和如果有可能,如何在Rust中添加多态特征对象的反序列化?(我很遗憾这两个问题都存在)。然而,这两个问题都没有展示如何使用typetag的答案。 - Shepmaster

3

oli_obk的回答 的基础上,您可以使用Serde的枚举表示 来区分类型。

在这里,我使用内部标记表示来将这两个相似的对象反序列化为适当的变量:

{
  "driver": "file",
  "path": "/var/log/foo"
}

{
  "driver": "http",
  "port": 8080,
  "endpoint": "/api/bar"
}

use serde; // 1.0.82
use serde_derive::*; // 1.0.82
use serde_json; // 1.0.33

#[derive(Debug, Deserialize, PartialEq)]
#[serde(tag = "driver")]
enum Driver {
    #[serde(rename = "file")]
    File { path: String },
    #[serde(rename = "http")]
    Http { port: u16, endpoint: String }
}

fn main() {
    let f = r#"   
{
  "driver": "file",
  "path": "/var/log/foo"
}
"#;

    let h = r#"
{
  "driver": "http",
  "port": 8080,
  "endpoint": "/api/bar"
}
"#;

    let f: Driver = serde_json::from_str(f).unwrap();
    assert_eq!(f, Driver::File { path: "/var/log/foo".into() });

    let h: Driver = serde_json::from_str(h).unwrap();
    assert_eq!(h, Driver::Http { port: 8080, endpoint: "/api/bar".into() });
}

您不必将所有内容压缩到一个枚举中,您也可以创建单独的类型:

#[derive(Debug, Deserialize, PartialEq)]
#[serde(tag = "driver")]
enum Driver {
    #[serde(rename = "file")]
    File(File),
    #[serde(rename = "http")]
    Http(Http),
}

#[derive(Debug, Deserialize, PartialEq)]
struct File {
    path: String,
}

#[derive(Debug, Deserialize, PartialEq)]
struct Http {
    port: u16,
    endpoint: String,
}

你如何“解包”第二个示例中的结构体?目前的输出将是File(File{path:"foo"})。最干净的实现方式是获取File{path:"foo"}而不是File(File{path:"foo"}) - Marcin

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