serde你在干什么 - serialize
serde 源码解析 | Serialization篇
关于serialize的过程,文章 understanding serde 讲得非常详细、非常好,用的例子还是真实的源码。推荐。
这篇文章最大的价值,是解释清楚了什么是所谓的serde data model。
它本身并不是一种中间的数据结构,而是一种纯粹的概念上的分类。
通常,对于序列化和反序列化,一个自然的想法,是建立一个中间抽象层,把Rust中的数据结构映射成中间的抽象层,然后再基于这个统一的抽象层,比如一个大的enum,然后对enum中的项目,则可以采用不同的翻译方式,转化成json、yaml等格式。
有了这个想法,再看 Serialize 和 Serializer trait的名字,望文生义都觉得没什么不妥。Serialize负责将Rust的数据结构转换到中间层,Serializer再负责把中间层翻译成具体的json、yaml。而中间层就是serde data model。这个模式其实很常见,连rust的编译都没逃过,想想Rustc、LLVM、MIR,是不是呼应上了?都呼应上了。
但是事情并不是这样的。
带着好奇,不妨来看代码。接下来用的,是在准备篇中介绍的简化版 serde_json。
故事开始
序列化的开始是在bin/inpsect.rs调用了serde_example::to_string
1 | |
to_string 构建了一个crate::ser::Serializer类型的实例serialzer,包含一个字段output,类型为String,可以猜到,接下来的序列化,就是serialzer的一场”看图作文”练习,根据内存中的数据,按照对应的格式,写到self.output中。
这个动作开始于value.serialize(&mut serializer)?;,魔法的开始之地,其中value是传入的Command实例。Command的serialze方法,由derive自动生成,所以接下来要看derive_ser.rs
1 | |
从serde::Serialize的角度
之前我们猜,serde::Serialize负责将Rust的原生类型整合为一个中间类型。现在就可以看到实际发生的,和猜想的有什么不同。serialize方法一进,劈头盖脸就是一个match,结构一目了然,四个枚举项,就问你们谁要序列化?
1 | |
比如Command::Set需要序列化,主体结构也非常明晰,给需要写作文的serializer安排四个步骤:
serialize_struct_variant,现在要序列化一个结构型的枚举项,告诉你几个关键信息好吧,枚举类型名称叫"Command",当前枚举项排行第一,名字叫"Set”,共有两个字段。好了,写去吧。写好了?过程中没什么意外发生吧,我检查检查,em……,Ok,在此基础上给你第二个任务 (这里的返回类型是SerializeStructVariant,之后再谈);serialize_field,接着序列化结构中的字段,字段名为”key”,实际内容也给你,写去吧。写好了?我检查检查,em……,Ok,在此基础上给你第三个任务;serialize_field,还是序列化结构中的字段,字段名为”value”,实际内容也给你,写去吧。写好了?我检查检查,em……,Ok,在此基础上给你第四个任务;end,结束了,没什么数据给你了,你最后收拾收拾,给小作文润色润色吧。
1 | |
我们看到,serde::Serialize没有想我们预想中的那样,在内存中新开辟空间来存放”中间类型”数据,而是把中间类型体现在对serializer的使用上,Serialize看出来,当前数据属于struct_variant,所以直接调用serialize中对应的方法。同理,对Command::Rm,调用的是newtype_variant,对Command::Quit调用的是unit_variant。
1 | |
所以,serde data model是serde::Serialize和serde::ser::Serializer共同承认的一套概念。serde::ser::Serializer用方法的形式,明确自己对某个data model的处理方式。而serde::Serialize则负责观察数据,将其分类,告知、提示serde::ser::Serializer该怎么做,即调用它的方法。这个过程中,数据只有一份。所以,如果对设计模式熟悉,这可以看成是访问者模式的一种应用。
从serde::ser::Serializer的角度
到这里,我们该看serialzier具体是怎么写作文的了。
第一个任务,serialize_struct_variant。根据json,Command::Set{ key: "A", val: "42" }表现为{"Set":{"key":"A","value":"42"}},所以忽略名称"Command" 以及枚举项的位置。作文起手,单走一个{,接一手字符串&str“Set”的序列化小作文。关于字符串的serde::Serialize已经在Serde crate内部实现,其实也很容易猜到,实现调用给定serializer的serialze_str方法,为字符传类型添加引号。到这里,self.output的值为{"Set",最后再接一个":{"形成 {"Set":{
1 | |
如前所说,serialize_struct_variant返回的应该是一个SerializeStructVariant类型,这其实是一个trait,ser.rs为当前的serializer实现了该trait,所以返回的是Ok(self)
和SerializeStructVariant类似的,有下面这些,定义在Serde中ser模组:
| Trait | Description |
|---|---|
| SerializeMap | Returned from Serializer::serialize_map. |
| SerializeSeq | Returned from Serializer::serialize_seq. |
| SerializeStruct | Returned from Serializer::serialize_struct. |
| SerializeStructVariant | Returned from Serializer::serialize_struct_variant. |
| SerializeTuple | Returned from Serializer::serialize_tuple. |
| SerializeTupleStruct | Returned from Serializer::serialize_tuple_struct. |
| SerializeTupleVariant | Returned from Serializer::serialize_tuple_variant. |
他们共同的特点,返回自一些序列化集合类型时所用的方法。目的是支持有状态的序列化,比如你可以在返回的结构里记录接下来是否是处理集合的第一项,以此来控制序列化中分隔符的使用。
再聚焦到我们使用到的SerializeStructVariant上,必须的两个方法是serialize_field,end,这两个方法被serde::Serialize调用过,用于安排作文写作的2、3、4步。
在serialize_field中,实际上并没有记录状态,因为当前简化版只支持紧密的json格式,所以可以通过看前一个字符是不是{来判断,是否在处理集合的第一项内容,进而决定要不要加,分割。
end就更简单了,小作文用}}收尾搞定。
1 | |
至此,serialzer.output已经变成我们想要的样子。{"Set":{"key":"A","value":"42"}}
尾声
起点,有一个,Rust的数据据结构。终点,有很多,json、yaml、toml、etc。
面对这个一对多的关系,通常都会想给Rust的数据结构做一个外层包装,然后各个下游的格式处理器,只和有限的数据结构做对接。这样逻辑上没有问题,但是在做包装的过程中,可能产生很多的内存分配、销毁、数据复制。
仔细想想,我们包装的目的,实际上就是对数据做分类,所以直接把这些类别用函数的形式固定,避免在内存中再造数据结构的计算开销。serde data model把Rust的数据类型分为29种,对应到Serializer::serialze_*()系列方法,搭配访问者模式,保证数据只在一个地方,然后用Serialize去驱动,帮助serializer完成对数据的序列化。
通过Serialize过程的观察,我们可以得到这样一条脉络,因为关注性能,所以要保证尽量减少新数据的产生,所以用访问者模式,由serde::Serialize带领serializer对原始数据参观一圈,一边走,一遍告诉serializer,这是A,这是B,然后小作家、小画家serializer就据此构建自己的作品。参观完,serialzer的作品也大功告成。
好了,Serialize就讲到这里。下期Deserialize再见。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!