Serde 元编程
第 2 章 Serde Data Model:29 种原语的设计哲学
2.1 为什么需要一套"中间语言"
上一章我们说过,Serde 用一个中间层击碎了 M×N 问题。这个中间层叫 Data Model。但"中间层"这个词太抽象了,不够具体——中间层到底长什么样?谁规定了它的边界?为什么不是 30 种原语或 20 种,恰好是 29 种?
本章要回答这些问题。在此之前,先建立一个更清晰的比喻。
Data Model 的角色很像翻译接力中的"通用中间语"。想象一个联合国会议,有 30 种语言(代表 30 种数据结构),会议要翻译成 5 种官方语言(代表 5 种格式)。如果每一对"源语言-目标语言"都配一个专职翻译,你需要 30 × 5 = 150 个翻译。这就是 M×N。
更聪明的做法是约定一种"中间语"——所有源语言先翻译成中间语,中间语再翻译到目标语言。这样你只需要 30 + 5 = 35 个翻译。但前提是:中间语必须足够丰富,能表达所有源语言的意思,也必须足够通用,让所有目标语言都能接收。
Serde 的 Data Model 就是这种中间语。它要同时满足两个矛盾的约束:
- 表达力:Rust 类型系统里所有"可序列化"的东西,都得能用 29 种原语表达。
- 格式无关:每一种原语都不能带某个具体格式的假设。比如不能规定"字符串必须加引号、做转义"(那是 JSON 文本的写法,Bincode 只写长度加原始字节),也不能规定"整数必须变长编码"(JSON 是文本)。
这两个约束把 Data Model 的选型空间压缩得极窄。Serde 选了 29 个原语,每一个都经过取舍。本章会逐个拆解。
2.2 29 种原语的全景图
先给一张表格,建立全局印象。之后我们逐类深入。
| 类别 | 原语 | 对应 Rust 类型举例 | Serializer 方法 |
|---|---|---|---|
| 布尔 | bool |
bool |
serialize_bool |
| 有符号整数 | i8 / i16 / i32 / i64 / i128 |
i8..=i128 |
serialize_i8/..i128 |
| 无符号整数 | u8 / u16 / u32 / u64 / u128 |
u8..=u128 |
serialize_u8/..u128 |
| 浮点 | f32 / f64 |
f32, f64 |
serialize_f32, serialize_f64 |
| 字符 | char |
char |
serialize_char |
| 字符串 | str |
&str, String |
serialize_str |
| 字节串 | bytes |
&[u8](通过 serde_bytes) |
serialize_bytes |
| 可选 | option |
Option<T> |
serialize_none, serialize_some |
| 单元 | unit |
() |
serialize_unit |
| 单元结构 | unit_struct |
struct Nothing; |
serialize_unit_struct |
| 单元变体 | unit_variant |
enum E { A } 的 A |
serialize_unit_variant |
| 新类型结构 | newtype_struct |
struct Millimeters(u8); |
serialize_newtype_struct |
| 新类型变体 | newtype_variant |
enum E { M(String) } 的 M |
serialize_newtype_variant |
| 变长序列 | seq |
Vec<T>, HashSet<T> |
serialize_seq |
| 定长元组 | tuple |
(A, B, C) |
serialize_tuple |
| 元组结构 | tuple_struct |
struct Pair(i32, i32); |
serialize_tuple_struct |
| 元组变体 | tuple_variant |
enum E { V(A, B) } 的 V |
serialize_tuple_variant |
| 键值映射 | map |
HashMap<K, V> |
serialize_map |
| 结构体 | struct |
struct User { ... } |
serialize_struct |
| 结构变体 | struct_variant |
enum E { V { a: A } } 的 V |
serialize_struct_variant |
数一下:5 种有符号整数 + 5 种无符号整数 + 2 种浮点 + bool + char + str + bytes + option + unit/unit_struct/unit_variant + newtype_struct/newtype_variant + seq/tuple/tuple_struct/tuple_variant + map/struct/struct_variant = 29 种。
设计意图:为什么
serialize_none和serialize_some是option这一个原语下的两个调用,而不是两个独立原语?因为它们在语义上表达的是"存在/不存在"这个同一个概念的两种取值。同样的逻辑也适用于后面会看到的seq状态机(用serialize_seq+ 多次serialize_element+end)——一个原语对应一种语义抽象,而不是一个方法对应一种抽象。
Serializer 的真实 trait 定义见 serde_core/src/ser/mod.rs:355(serde_core crate,位于 serde 仓库内;本章下文 serde_core/src/... 路径同此):
// serde_core/src/ser/mod.rs:355
pub trait Serializer: Sized {
type Ok;
type Error: Error;
type SerializeSeq: SerializeSeq<Ok = Self::Ok, Error = Self::Error>;
type SerializeTuple: SerializeTuple<Ok = Self::Ok, Error = Self::Error>;
// ... 另外 5 个 SerializeXxx 关联类型
fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error>;
fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error>;
// ... 一共 30 个 serialize_* 方法(option 占 2 个)
}
注意 7 个关联类型 (SerializeSeq, SerializeTuple, ...)。它们对应 Data Model 里所有"复合"原语——seq、tuple、tuple_struct、tuple_variant、map、struct、struct_variant。这 7 种原语的共同特点是:它们不是一次调用就能序列化完的,需要"先开始、中间多次添加、最后结束"的状态机模式。关联类型就是这台状态机的"状态变量"。第 3 章会详细讲状态机。
2.3 数值与基本类型(12+2)
最没有悬念的一组:12 种数值 + bool + char。它们之所以分这么细,有三个原因:
原因一:保留类型信息让格式优化。 Rust 的 u8 和 i64 在 JSON 文本里写出来没区别,但在 Bincode 里一个占 1 字节、一个占 8 字节。Serde 把 12 种数值都拆开,格式实现者可以针对每种类型写最紧凑的编码。
原因二:避免精度损失。 Rust 有 f32 和 f64 两种浮点。如果只给 serialize_float,实现者不知道原始精度,可能把 f32 序列化成"看起来精确"的 f64。精度信息必须保留。
原因三:i128/u128 是特殊的。 Rust 1.26 才稳定了 128 位整数,JSON、MessagePack 等历史悠久的格式没有原生 128 位类型。Serde 给 serialize_i128/serialize_u128 提供了默认实现(返回错误),让不支持 128 位的格式可以明确拒绝:
// serde_core/src/ser/mod.rs:531
fn serialize_i128(self, v: i128) -> Result<Self::Ok, Self::Error> {
let _ = v;
Err(Error::custom("i128 is not supported"))
}
char 为什么独立? 因为 Rust 的 char 是 4 字节的 Unicode 标量值,和 u32 虽然位宽相同但语义不同。文本格式(JSON)把 char 写成单字符字符串,二进制格式可能按 Unicode 代码点存。如果把 char 混进 serialize_u32,格式丢失了语义信息。
bytes 为什么不是 Vec<u8>? 这是一个让无数人困惑的设计。Rust 里 Vec<u8> 和 &[u8] 是最自然的"字节数组"表达,但 Serde 默认把它们当作普通的 Vec<T>/[T],不做任何特殊处理([T] 的实现见 serde_core/src/ser/impls.rs:175-184,Vec<T> 由同文件 seq_impl! 宏展开,函数体都是 serializer.collect_seq(self))——经 collect_seq 的默认实现走 serialize_seq,每个字节调一次 serialize_u8。
这是对的吗?在 JSON 里无所谓——serde_json 的 serialize_bytes 同样写成整数数组(serde_json/src/ser.rs:1804-1817 的 write_byte_array),两条路径输出完全相同。Bincode 1.x 和 Postcard 里输出也相同:seq 路径写长度再逐个写 1 字节的 u8,bytes 路径写长度再整块写入原始字节,差别只在逐元素调用的开销。真正拉开差距的是 MessagePack(以 rmp-serde 1.3.1 实测 1024 字节的 Vec<u8>):
- 走
seq:数组头 + 每个字节单独编码成整数——0~127 占 1 字节,128~255 要加0xcc前缀占 2 字节,最坏情况(全是 ≥128 的字节)编码成 2052 字节 - 走
bytes:bin头 + 原始字节整体存放,恒为 1028 字节
Serde 的解决方案是一个叫 serde_bytes 的辅助 crate,提供 Bytes/ByteBuf 包装类型和 #[serde(with = "serde_bytes")] 属性,让用户显式选择 bytes 路径。serde_bytes 的 README 把原因归结为 Rust 没有 specialization(特化,尚未稳定):没有它,Serde 只能把 &[u8] 当作普通切片、把 Vec<u8> 当作普通向量处理。
设计意图:
bytes和seq分开,是 Serde "格式无关"原则的一次轻微妥协。纯粹的格式无关做法是只有seq,让格式自己去判断元素类型是不是 u8。但现实中几乎所有二进制格式都对字节串有原生支持,Serde 把它提升为原语,让格式可以用一个方法就接住——性能优先压倒了极致的简洁。
2.4 Option:为什么不是一个 Tag?
Option<T> 是 Rust 最常见的类型之一。Serde 给它一个专门的原语 option,通过两个方法表达:
// serde_core/src/ser/mod.rs:788 & 821
fn serialize_none(self) -> Result<Self::Ok, Self::Error>;
fn serialize_some<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize;
为什么不按 Rust enum 的朴素思路,把 Option 当成一个普通 enum 处理?毕竟 Option<T> 就是 enum Option<T> { None, Some(T) },可以用 unit_variant(None)+ newtype_variant(Some(T))表达。
答案:Option 太常用了,值得特殊优化。 如果走 enum 路径,JSON 里 Some(42) 会编码成 {"Some":42} 这种丑陋的形式。而约定俗成地,所有 JSON 格式都希望 Some(42) 就是 42、None 就是 null。Serde 把 option 单独提出来,让每种格式决定怎么处理"存在性"这个语义,而不受普通 enum 规则的束缚。
不同格式的 option 处理:
| 格式 | None 编码 |
Some(42u64) 编码 |
|---|---|---|
| JSON | null |
42 |
| MessagePack | 0xc0 (nil) |
0x2a (整数 42) |
| Bincode 1.x | 0x00 (1 字节 tag) |
0x01 <42 的 8 字节> |
| Postcard | 0x00 |
0x01 <42 的 varint> |
每种格式都用它自己最紧凑的方式表达"存在"。JSON 甚至完全省略了"存在标签"——直接写值。如果没有独立的 option 原语,无法做到这种格式特异性优化。
设计意图:Option 的单独支持是 Serde "通用抽象 + 实用优化"平衡的典型例子。Data Model 在绝大多数地方追求简洁统一,但在极其常用的模式上愿意增加特殊原语。这种"80/20 取舍"贯穿 Serde 设计——
bytes、option、unit都是同样的原因。
2.5 Unit 家族:三种"没有数据"的形态
Data Model 里有三个看起来差不多的原语:
unit:对应 Rust 的()(空元组)unit_struct:对应struct Nothing;unit_variant:对应enum E { A, B }中的A
它们在运行时都不携带任何数据——没有字段、没有值,只有一个"类型身份"。为什么要分三个?
答案:名字信息不同。 看 Serializer trait:
// serde_core/src/ser/mod.rs:841, 861, 889
fn serialize_unit(self) -> Result<Self::Ok, Self::Error>;
fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error>;
fn serialize_unit_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
) -> Result<Self::Ok, Self::Error>;
serialize_unit无参数——它纯粹表示"啥都没有"。serialize_unit_struct带一个name参数——它是"一个叫 X 的啥都没有"。serialize_unit_variant带name(enum 名)、variant_index(变体下标)、variant(变体名)——它是"enum X 的第 i 个变体 Y"。
这些名字信息对某些格式很重要。 比如你有一个 enum Status { Active, Banned },序列化 Active 时:
- JSON 格式可以用
"Active"这个字符串(利用variant名字) - Bincode 格式可以用整数
0(利用variant_index,不必写变体名字符串) - MessagePack 可能选择
"Active"也可能选择0,由具体实现决定
如果 unit_variant 不把这些信息作为参数传入,格式无法做出选择。反过来,unit 不需要任何名字——() 序列化通常就是"什么都不写"(JSON 里是 null,Bincode 里是 0 字节)。
unit_struct 的特殊用法。 struct Millimeters; 这种单元结构体在业务代码里很少直接用,但它是 类型标签(type tag) 的绝佳载体。想象你有一个 struct Celsius(f64),你把它 derive 成 Serialize 后,JSON 会写 42.5——温度的上下文丢了。如果改成 struct Celsius { tag: CelsiusTag, value: f64 },其中 CelsiusTag 是单元结构体,格式就能拿到 "CelsiusTag" 这个名字。但要注意用不用是格式的选择:serde_json 的 serialize_unit_struct 直接转发到 serialize_unit(serde_json/src/ser.rs:209),写出来是 {"tag":null,"value":42.5};想保留名字得靠格式自己利用 name 参数。unit_struct 原语做的是让这种"只为名字存在"的类型有了一等支持。
设计意图:三个 unit 原语的差异不在运行时数据(都是零),而在编译期元信息(名字、索引)。Serde 把这些元信息通过参数传递给 Serializer,让格式可以选择是否利用它们。这呼应了贯穿 Serde 的一个模式:把 Rust 编译期能获取的所有信息,都完整传递给格式层,由格式层决定取舍。
2.6 Newtype 家族:对"包装"的一等支持
Newtype 模式是 Rust 最常见的设计模式之一——用一个单字段 struct 包装一个原始类型,获得类型安全和新语义:
struct UserId(u64);
struct Email(String);
struct Celsius(f64);
在类型系统里,UserId 和 u64 是不同类型,编译器不会让你把一个 u64 当作 UserId 传递。但在运行时,UserId 的内存布局通常和 u64 一样(只有加上 #[repr(transparent)] 才有保证)。
Serde 给 newtype 一个专门的原语:
// serde_core/src/ser/mod.rs:916
fn serialize_newtype_struct<T>(
self,
name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize;
为什么需要? 看两种可能的实现选择:
选择 A:把 newtype 当作单字段 tuple_struct。 UserId(42) 序列化成 [42](JSON)。正确但冗余——这个包装层没有语义价值。
选择 B:把 newtype 当作透明包装。 UserId(42) 序列化成 42。内容一致,但类型名丢失。
Serde 的选择:交给格式决定。 serialize_newtype_struct 把 name("UserId")和 value(内部的 42)都传给格式,格式决定是用 A 还是 B:
serde_json默认选 B——JSON 里UserId(42)就是42(透明)postcard也选 B- 如果有需要保留类型名的场景,格式可以选 A(或自定义结构)
这个方法没有默认实现(必须由格式实现),但官方文档注释用一句散文把推荐写法定成了透明包装(serde_core/src/ser/mod.rs:898-900):
Serializers are encouraged to treat newtype structs as insignificant wrappers around the data they contain. A reasonable implementation would be to forward to
value.serialize(self).
注意这段文档没有给出 Serializer 侧的示例代码——紧随其后的 doctest(serde_core/src/ser/mod.rs:902-915)演示的是调用方:一个 impl Serialize for Millimeters,在里面调 serializer.serialize_newtype_struct("Millimeters", &self.0)。换句话说,"丢弃 name、直接转发"只是官方建议的默认行为,不是源码里能抄的一段实现。真要在自己的格式里落实,写出来是这样(不是源码原文,是按上面那句建议写的实现示意):
// 不是源码原文:按 ser/mod.rs:898-900 的文字建议写出的实现示意
fn serialize_newtype_struct<T>(
self,
_name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(self) // 直接转发,丢弃 name
}
serde_json 正是这么做的(serde_json/src/ser.rs:225-230,函数体只有 value.serialize(self) 一行)。
newtype_variant 是 enum 版本的同样思路:
// serde_core/src/ser/mod.rs:950
fn serialize_newtype_variant<T>(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize;
对应 enum E { M(String) } 的 M 变体——带一个值的 enum 变体。相比 unit_variant,多了一个 value 参数。
设计意图:newtype 的独立原语是 Serde 对 Rust 习惯用法的一次"特殊优待"。如果 Serde 诞生在一个不使用 newtype 模式的语言里(比如 Python),这个原语可能不会出现。Data Model 不是"数据理论上的最小集合",而是"Rust 生态实际最需要的集合"——这是工程而非数学。
2.7 Sequence 家族:4 种序列
Data Model 有 4 种"元素列表"原语:
seq:变长,Vec<T>、HashSet<T>(官方文档称其为 heterogeneous,Data Model 并不要求元素同类型)tuple:定长,元素类型可以不同,(A, B, C)tuple_struct:定长带名字,struct Pair(i32, i32)tuple_variant:enum 变体带元组数据,enum E { V(A, B) }
它们的 Serializer 方法签名:
// serde_core/src/ser/mod.rs:1006
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error>;
// serde_core/src/ser/mod.rs:1062
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error>;
// serde_core/src/ser/mod.rs:1089
fn serialize_tuple_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error>;
// serde_core/src/ser/mod.rs:1134
fn serialize_tuple_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error>;
注意细节:
seq 的 len 是 Option<usize>。 为什么?因为迭代器链式调用时,最终元素数量可能不知道:iter.filter(...).map(...).collect::<Vec<_>>() 可以提前知道长度,但 iter.filter(...) 单独用时不行。seq 允许 None,让流式序列化成为可能。代价是某些格式(Bincode 需要先写长度)无法处理 None——直接报错,调用方得先把所有元素收集起来。
元组的元数上限是 16。 Serialize 对元组的实现是宏批量展开出来的,不是无限泛型:serde_core/src/ser/impls.rs:397 单独写了 impl<T> Serialize for (T,)(一元组),serde_core/src/ser/impls.rs:404-420 的 tuple_impls! { 2 => ... 16 => ... } 把二元到十六元一次展开完,文档属性也在 serde_core/src/ser/impls.rs:392-396 明写 "This trait is implemented for tuples up to 16 items long."。反序列化一侧同样只展开到十六元(serde_core/src/de/impls.rs:1481 起的 tuple_impls!)。超过 16 元的元组没有 Serialize 实现——这是 Rust 缺变长泛型时代的标准做法,不是 Data Model 的语义上限。
tuple/tuple_struct/tuple_variant 的 len 是 usize。 因为 Rust 的元组类型在编译期就确定了长度,(A, B, C) 永远是 3 个元素。格式可以省略长度字段:JSON 里 [1,2,3] 和 [1,2] 字节不同,但 Bincode 里如果双方都知道长度是 3,就可以不写长度,直接写三个值——节省 8 字节。
四者的"名字参数"差异反映了用法差异:
seq:完全匿名,因为Vec<T>的"身份"不重要tuple:也匿名,因为(A, B, C)是结构性类型,没有名字tuple_struct:有name,因为struct Pair(i32, i32)有类型名tuple_variant:有 enum 名、变体索引、变体名——enum 身份是关键信息
这些名字让格式可以选择编码策略:JSON 可能把 E::V(1, 2) 写成 {"V":[1,2]}(用变体名);Bincode 会写 <variant_index><a><b>——变体名一个字节都不进字节流。
状态机:为什么 seq 返回 SerializeSeq 而不是直接写元素?
serialize_seq 返回一个 Self::SerializeSeq 对象,然后调用方在这个对象上调用 serialize_element 多次、最后 end():
// 典型用法
let mut seq = serializer.serialize_seq(Some(vec.len()))?;
for item in &vec {
seq.serialize_element(item)?;
}
seq.end()
为什么要这个状态机?因为某些格式的"开始"和"结束"需要特殊处理:
- JSON 需要写
[,然后元素之间加,,最后写] - Bincode 需要在开始时先写长度头(len 为 None 时直接报错)
- MessagePack 需要在开始时写"数组头"带长度
如果 serialize_seq 一次性接收所有元素(&[T]),就没法处理迭代器场景;如果它每次调用都独立(serialize_element),又没法管理"开头/结尾"状态。状态机模式是这两个约束的平衡点。
设计意图:状态机模式(begin → add × N → end)是 Serde Data Model 的一个核心模式,不只 seq,后面的 map、struct、struct_variant 都用它。这个模式让 Serde 能支持从 O(1) 内存的流式序列化到 O(n) 内存的一次性序列化的完整谱系。
2.8 Map 家族:3 种键值映射
最后三个原语:
map:动态键值映射,HashMap<K, V>、BTreeMap<K, V>struct:键在编译期已知的结构体,struct User { id: u64, name: String }struct_variant:enum 变体里的结构体,enum E { V { a: A } }
它们的方法签名:
// serde_core/src/ser/mod.rs:1188
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error>;
// serde_core/src/ser/mod.rs:1220
fn serialize_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeStruct, Self::Error>;
// serde_core/src/ser/mod.rs:1264
fn serialize_struct_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error>;
map vs struct 的关键区别: key 是什么时候决定的?
map的 key 在运行时才知道——HashMap<String, i32>的实际 key 集合取决于插入了什么struct的 key 在编译期就知道——User { id, name }的字段永远是id和name
这个区别决定了:
serialize_map接收Option<usize>(和 seq 一样,流式友好),但每次serialize_entry需要接收一个运行时 keyserialize_struct接收usize(长度编译期已知),每次serialize_field的 key 是&'static str(编译期字符串)
Bincode 如何利用这个区别? Bincode 对 struct 完全不写字段名——反正编译期双方都知道是 User { id, name },按顺序写 id 的值再写 name 的值就够了。而 map 必须写每个 key-value 对的 key,不然接收方不知道这是什么。
对一个有 10 个字段的结构体,map 编码要把 10 个字段名逐个写进字节流,struct 编码在 Bincode 这类格式里把这部分全部省掉——省下的正是所有字段名的字节数加上各自的长度前缀。这是 Data Model 分层的实际经济价值。
struct_variant:如果 enum 的某个变体是 struct 形态:
enum Event {
Click { x: i32, y: i32 }, // struct_variant
KeyPress(char), // newtype_variant
Close, // unit_variant
}
Click { x: 10, y: 20 } 会调用 serialize_struct_variant("Event", 0, "Click", 2),然后分别 serialize_field("x", &10)、serialize_field("y", &20)、end()。
2.9 完整的"调用形状图"
到这里,29 种原语都介绍完了。用一张综合的流程图看 Serialize 的整体结构:
flowchart TD
Start[开始序列化一个值] --> Type{Rust 类型是什么?}
Type -->|bool/数字/char| Prim[serialize_bool/i*/u*/f*/char<br>一次调用]
Type -->|&str, String| Str[serialize_str<br>一次调用]
Type -->|"&[u8] (via serde_bytes)"| Bytes[serialize_bytes<br>一次调用]
Type -->|Option| Opt{Some or None?}
Opt -->|None| None[serialize_none]
Opt -->|Some| Some[serialize_some value]
Type -->|"()"| Unit[serialize_unit]
Type -->|struct Nothing| US[serialize_unit_struct]
Type -->|enum 的单元变体| UV[serialize_unit_variant]
Type -->|struct X T| NS[serialize_newtype_struct]
Type -->|enum 的元组变体 只 1 个字段| NV[serialize_newtype_variant]
Type -->|Vec/HashSet 等变长序列| Seq[serialize_seq<br>然后多次 serialize_element<br>最后 end]
Type -->|A,B,C 元组| Tup[serialize_tuple<br>然后多次 serialize_element<br>最后 end]
Type -->|struct Pair int,int| TS[serialize_tuple_struct<br>多次 field + end]
Type -->|enum 的元组变体| TV[serialize_tuple_variant<br>多次 field + end]
Type -->|HashMap/BTreeMap| Map[serialize_map<br>多次 key/value + end]
Type -->|struct User field:T| Struct[serialize_struct<br>多次 serialize_field + end]
Type -->|enum 的结构变体| SV[serialize_struct_variant<br>多次 serialize_field + end]
style Prim fill:#e0ffe0
style Str fill:#e0ffe0
style Bytes fill:#e0ffe0
style None fill:#e0ffe0
style Some fill:#e0ffe0
style Unit fill:#e0ffe0
style US fill:#e0ffe0
style UV fill:#e0ffe0
style NS fill:#e0ffe0
style NV fill:#e0ffe0
style Seq fill:#fff0e0
style Tup fill:#fff0e0
style TS fill:#fff0e0
style TV fill:#fff0e0
style Map fill:#fff0e0
style Struct fill:#fff0e0
style SV fill:#fff0e0
绿色节点是一次调用完成的原语;橙色节点是需要状态机(begin → N × add → end)的原语。这个二分法贯穿整个 Data Model。
2.10 Data Model 的镜像面:Deserializer 与 Visitor
到目前为止我们一直从"写出去"的方向讲 Data Model——Serializer 的 30 个 serialize_* 方法如何把 Rust 值拆解到 29 种原语里。但 Data Model 是双向的:同样这 29 种原语也定义了反序列化的词汇表。反向这条链的 trait 在 serde_core/src/de/mod.rs:945:
pub trait Deserializer<'de>: Sized {
type Error: Error;
/// 告诉 Deserializer:我不知道是什么类型,请你决定。
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where V: Visitor<'de>;
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where V: Visitor<'de>;
fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where V: Visitor<'de>;
// ... 加上 deserialize_string / byte_buf / identifier / ignored_any 等提示方法,
// 一共 31 个 deserialize_*(4 种 enum 变体合并进 deserialize_enum)
fn is_human_readable(&self) -> bool { true } // serde_core/src/de/mod.rs:1253
}
乍看结构和 Serializer 是对称的——同样一堆 deserialize_bool / deserialize_i8 / deserialize_str。但两个方向的"谁驱动谁"完全不同,这是初学者最常栽的跟头:
- 序列化方向:是类型驱动格式。
Vec<u8>::serialize里 Rust 代码主动选择调用serialize_seq(经collect_seq)。格式只是被动执行。 - 反序列化方向:是类型提示格式、但格式决定Visitor。
Vec<u8>::deserialize调用deserializer.deserialize_seq(visitor)(VecVisitor,serde_core/src/de/impls.rs:1143、:1175)——"seq" 只是提示,真正决定 Visitor 哪个方法被回调的,是格式里实际存的是什么。如果用户要反序列化i64、JSON 里存的是42,serde_json 回调的是visit_u64而不是visit_i64(非负整数一律按 u64 解析,serde_json/src/de.rs:118-129),由 Visitor 里写的逻辑决定接不接受这种"宽松对齐"。反过来,格式也可以不给 Visitor 表态的机会:输入是字符串"42"时,serde_json 在deserialize_i64里直接报 invalid type,根本不回调visit_str(serde_json/src/de.rs:319-343)。
所以 Data Model 在反序列化方向落到 Visitor 这个第二 trait 上(serde_core/src/de/mod.rs:1317):
pub trait Visitor<'de>: Sized {
type Value;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result;
fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> { ... }
fn visit_i8<E: Error>(self, v: i8) -> Result<Self::Value, E> { ... }
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> { ... }
// ... 一共 27 个 visit_* 回调
fn visit_borrowed_str<E: Error>(self, v: &'de str) -> Result<Self::Value, E> { ... }
fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> { ... }
}
Visitor 的一个额外信号:visit_borrowed_str/visit_borrowed_bytes 带 'de lifetime——这是 Serde 著名的 zero-copy 反序列化的钩子。&'de str 直接指向原始输入缓冲,不拷贝一字节。格式只有在原始数据本身就是 UTF-8 连续字节(如 JSON string 没有转义)且它的生命周期至少和 'de 一样长时,才调这个方法;否则退回到 visit_str(&str)——生命周期短于 'de,Visitor 想借走就得拷贝。
这个双 trait 设计的工程意义:把"格式怎么读"和"类型想接什么"彻底解耦。一个 Deserialize<'de> for MyType 的实现者只需要写一次 Visitor、描述"我能接受哪些原语如何转成我",就能匹配所有格式。反之一个 Deserializer 格式实现者只管"我的数据里下一个值是什么原语",不用管目标类型有多少种。
2.10.1 asymmetric:两个默认实现藏在这里
从书写量看 Serializer 和 Deserializer 几乎对称,但有两处不对称容易被忽略,都是 serde_core 的真实源码实现:
第一处——deserialize_i128 / deserialize_u128 有默认实现(serde_core/src/de/mod.rs:991-997,deserialize_u128 同款在 serde_core/src/de/mod.rs:1022-1028):
fn deserialize_i128<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where V: Visitor<'de>,
{
let _ = visitor;
Err(Error::custom("i128 is not supported"))
}
这是公开的 deserialize_* 方法里唯二没有强制实现的两个(另有一个 #[doc(hidden)] 的 __deserialize_content_v1 带默认实现,注明 "Not public API",serde_core/src/de/mod.rs:1257-1265)。Serializer 侧的 serialize_i128 是同样的默认——也直接报错(§2.3),两侧对称。工程考量:128 位整数在相当多格式里根本没有原生表示(JSON 数字在多数实现里按 IEEE 754 双精度处理,整数只精确到 53 位;MessagePack 的 int 限 64 位),默认实现让格式实现者不必关心它,用到再说。
第二处——Visitor 几乎所有方法都有默认错误实现,只有 expecting(serde_core/src/de/mod.rs:1343)强制实现。这个设计让你可以"只接受一种原语":
impl<'de> Visitor<'de> for MyBoolVisitor {
type Value = bool;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a boolean")
}
fn visit_bool<E: Error>(self, v: bool) -> Result<bool, E> { Ok(v) }
// 其他 26 个 visit_* 保留默认——要么转发到更宽的类型(如 visit_i8 → visit_i64),要么返回 invalid_type 错误
}
碰到错误类型时默认实现返回的是结构化错误 Error::invalid_type(Unexpected::Xxx, &self)——它会调 self.expecting() 拼错误消息。这就是你见到 "invalid type: integer 42, expected a boolean" 这种信息的来源,expecting 必须实现就是因为它被这套默认链反复调用。
2.11 deserialize_any 与自描述格式的边界
所有 Deserializer 方法里最特殊的是 deserialize_any——它的意思是 "我不知道下一个值是什么类型,格式你来告诉我"。官方源码注释(serde_core/src/de/mod.rs:950-958)直截了当:
When implementing
Deserialize, you should avoid relying onDeserializer::deserialize_anyunless you need to be told by the Deserializer what type is in the input. Know that relying onDeserializer::deserialize_anymeans your data type will be able to deserialize from self-describing formats only, ruling out Postcard and many others.
这把一个容易搞错的边界讲得很清楚:Serde 的格式分两类——
自描述格式(self-describing):每个值前面带类型标签。JSON(字面 tokens)、YAML(缩进 + 类型字面值)、MessagePack(前缀字节指示类型)、CBOR、RON。这类格式能实现 deserialize_any——解析器先看 token、再决定 visit_xxx。
非自描述格式(non-self-describing):类型信息不在字节流里,依赖 schema 指导读取。Bincode、Postcard。这类格式的 deserialize_any 通常直接返回错误——因为解析器看到一串字节,没有 schema 就不知道这是个 u32 还是个 string。
实际影响:serde_json::Value、toml::Value 这类"任意结构"容器的反序列化内部靠 deserialize_any——这就是为什么你能 serde_json::from_str::<serde_json::Value>(s) 但不能 postcard::from_bytes::<postcard::Value>(bytes)(Postcard 根本没有 Value 类型)。设计你自己的 Deserialize 实现时,避免调 deserialize_any——一用就把 Bincode、Postcard 这类非自描述格式排除在外。明确调 deserialize_i64 / deserialize_str / deserialize_struct,该 flavor 的信息至少让 Postcard、Bincode 这类非自描述格式知道该读多少字节。
2.11.1 is_human_readable——一个 bool 撑起的格式分野
Deserializer trait 末尾还挂着一个不起眼的方法(serde_core/src/de/mod.rs:1253-1255):
// serde_core/src/de/mod.rs:1253-1255
fn is_human_readable(&self) -> bool {
true
}
默认 true(偏向 JSON/YAML 这种人能读的格式)。Bincode、Postcard 会 override 成 false。
这个 bool 有两个大用途:
1. 紧凑 vs 友好的二选一——像 std::net::IpAddr、SocketAddr 这类类型在人类可读格式下序列化成字符串("127.0.0.1"),在紧凑格式下序列化成紧凑字节元组([127, 0, 0, 1])。(Duration 不在此列:它两种格式下都走 serialize_struct("Duration", 2),写 secs、nanos 两个字段,serde_core/src/ser/impls.rs:684-695。)同一个 Serialize 实现通过 if serializer.is_human_readable() { ... } else { ... } 分支走不同路径。
2. 格式演进的不可回头承诺——源码注释(serde_core/src/de/mod.rs:1246-1251,serde_core/src/ser/mod.rs:1452-1457 有一份逐字相同的)明确:"modifying this method to change a format from human-readable to compact or vice versa should be regarded as a breaking change"。一旦某个格式声明了自己是 readable 或 compact,之后改变选择就会让已有数据反序列化失败。所以新格式实现这个方法时要提前想清楚。
到这一节为止,我们完成了 Data Model 在两个方向的完整覆盖:Serializer/Deserializer 两棵 trait 对称站立,Visitor 在反序列化方向承担把 29 种原语翻译回 Rust 类型的职责,deserialize_any 和 is_human_readable 是两根连接具体格式能力与类型期望的承诺线。下一章回到序列化方向,完整拆解 Serializer trait,包括它的 7 个状态机子 trait——这是 Data Model 从"静态表格"变成"动态调用序列"的地方。
2.12 Data Model 不包括什么
理解一个抽象的边界,和理解它的内容同样重要。Serde Data Model 明确不支持的东西:
1. 引用和借用关系。 你不能序列化 &'a T 里的"借用信息"——序列化的是被指向的值,引用本身消失。这意味着图结构(有共享节点)无法直接表达,必须手动设计 ID 映射。
2. 裸指针。 *const T、*mut T 没有语义,不能自动序列化。
3. 函数和闭包。 序列化可执行代码是一个独立的巨大问题(远程执行、安全性),不在 Serde 范围内。想序列化回调?用函数名字符串 + 查找表。
4. 类型元数据。 Serde 不能序列化"这是什么类型"本身(只能序列化某个类型的实例)。如果你需要 RTTI,用 typetag 这个第三方 crate。
5. 循环引用。 Rc<RefCell<Node>> 里的循环会导致 serialize 无限递归。Serde 不检测循环——这是用户的责任。注意 serde_json 那道有名的 128 层深度限制帮不上忙:remaining_depth: 128 是 Deserializer 的字段(serde_json/src/de.rs:34 声明、serde_json/src/de.rs:63 初始化),递减与超限判定在 serde_json/src/de.rs:1374-1378,只保护"解析深嵌套输入"这一侧;serde_json/src/ser.rs 全文没有任何深度计数(grep remaining_depth 0 处)。序列化一个成环的 Rc 得到的是栈溢出,不是 Err。
6. 保留 Rust 类型标签。 Vec<u8> 和 Box<[u8]> 序列化结果一样,反序列化时 Data Model 无法区分——字节流里只有"这是一个 seq",没有"这是哪种容器"。
设计意图:Serde 选择不做"万能"序列化器。它聚焦于"树形结构数据"这个最主流的场景,把图结构、RTTI、循环等边缘问题推给第三方库或用户手动处理。这种"做减法"的克制是 Serde 能保持简洁快速的关键。
2.13 Data Model 与具体格式的对应关系
为了把 Data Model 的抽象具象化,看几个真实原语在 4 种主流格式里的编码对比(以下字节均用 serde_json 1.0.149、rmp-serde 1.3.1、bincode 1.3.3 默认配置、postcard 1.1.3 实测)。
例子:User { id: 42u64, name: "alice" }
JSON:
{"id":42,"name":"alice"}
MessagePack(十六进制,rmp_serde::to_vec_named 的输出;默认的 to_vec 会把 struct 编成数组):
82 a2 69 64 2a a4 6e 61 6d 65 a5 61 6c 69 63 65
82= map with 2 entriesa2 69 64= str of length 2, "id"2a= integer 42a4 6e 61 6d 65= str of length 4, "name"a5 61 6c 69 63 65= str of length 5, "alice"
Bincode:
2a 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 61 6c 69 63 65
2a 00 00 00 00 00 00 00= u64 42(小端)05 00 00 00 00 00 00 00= u64 长度 561 6c 69 63 65= "alice" 的 UTF-8 字节
注意 Bincode 完全没有字段名——因为 struct 原语允许格式省略字段名。
Postcard(类似 Bincode 但用 varint):
2a 05 61 6c 69 63 65
2a= varint 42(1 字节)05= varint 5(字符串长度)61 6c 69 63 65= "alice"
同一份数据,文本格式占 24 字节,紧凑二进制占 7 字节——差 3.4 倍。这就是 Data Model 层面允许格式做特异化优化的价值:Serde 不预设"应该怎么编码",它只传递结构信息,让每种格式做自己最擅长的事。
2.13.1 实测:is_human_readable 的真实用户——10 处 IP/网络类型
§2.11.1 说 is_human_readable 这个 bool 撑起了格式分野,下面到源码里实测它的真实使用场景——
grep is_human_readable serde_core/src/{ser,de}/impls.rs 共 10 处使用(ser/impls.rs 6 处 + de/impls.rs 4 处)——全部集中在 std::net 的地址类型上:
网络地址类型(std::net)——
| Rust 类型 | 序列化分支(human_readable=true) | 二进制分支 |
|---|---|---|
IpAddr |
dispatch 到 Ipv4Addr / Ipv6Addr |
dispatch 到对应二进制路径 |
Ipv4Addr |
"192.0.2.1" 字符串(最长 15 字符) |
octets() 的 [u8; 4],走 tuple |
Ipv6Addr |
"2001:db8::1" 字符串(最长 39 字符) |
octets() 的 [u8; 16],走 tuple |
SocketAddr |
dispatch 到 V4/V6 socket | 二进制 |
SocketAddrV4 |
"192.0.2.1:65000" 字符串(最长 21 字符) |
ip + port 各自二进制 |
SocketAddrV6 |
字符串带 zone | 二进制 |
实测 serde_core/src/ser/impls.rs 里的真实代码——
// serde_core/src/ser/impls.rs:806
impl Serialize for net::Ipv4Addr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if serializer.is_human_readable() {
const MAX_LEN: usize = 15;
debug_assert_eq!(MAX_LEN, "101.102.103.104".len());
let mut buf = [b'.'; MAX_LEN];
let mut written = format_u8(self.octets()[0], &mut buf);
for oct in &self.octets()[1..] {
// Skip over delimiters that we initialized buf with
written += format_u8(*oct, &mut buf[written + 1..]) + 1;
}
// Safety: We've only written ASCII bytes to the buffer, so it is valid UTF-8
let buf = unsafe { str::from_utf8_unchecked(&buf[..written]) };
serializer.serialize_str(buf)
} else {
self.octets().serialize(serializer) // [u8; 4] → 4 元素 tuple
}
}
}
两条值得记住的事实:
is_human_readable在 std 类型的实现里只有 10 处使用,全是网络地址类型的双格式分支。Serde 没有把这个 bool 撒到每个类型上,只在可读字符串与紧凑字节差别明显的地址类型上分出两条路径。debug_assert_eq!(MAX_LEN, "101.102.103.104".len()):Ipv4Addr的序列化代码把 15 字符上限硬编码成栈缓冲区大小,再用 debug 断言核对,避免了一次堆分配。
is_human_readable 默认实现是 true(serde_core/src/ser/mod.rs:1459、serde_core/src/de/mod.rs:1253):不显式实现的 Serializer/Deserializer 都被当作 human readable,所以 bincode、postcard 等二进制格式必须显式 override 为 false 才能让 IP 地址走二进制路径;JSON / YAML / TOML 直接复用默认值。
2.14 本章小结
Data Model 是 Serde 的"中间语"——一套格式无关、类型安全、编译期零开销的数据表示。29 种原语不是随意选的,它们对应:
- 数值与文本:14 种基本类型(12 数值 + bool + char),加上 str、bytes 共 16 种
- 可选性:option 一个原语,两次调用
- 空值身份:unit、unit_struct、unit_variant 三种"没有数据"的语义
- 透明包装:newtype_struct、newtype_variant 两种一字段形态
- 有序复合:seq(变长)、tuple(定长)、tuple_struct、tuple_variant 四种序列
- 键值复合:map(运行时 key)、struct(编译期 key)、struct_variant 三种映射
四个关键设计原则贯穿 Data Model:
- 类型信息不丢失:12 种数值分别对应,名字通过参数传递,格式可选择性利用。
- 状态机表达复合结构:begin → add → end 模式让流式序列化和一次性序列化共存。
- 格式无关但优化友好:原语不预设编码,但传递足够的"提示信息"让格式各显神通。
- 双向对称但驱动反转:Serializer 由类型主动驱动调用链,Deserializer 由格式决定回调哪个 Visitor 方法;
deserialize_any明确标出"只适用于自描述格式"的边界,is_human_readable让同一类型能在紧凑/友好两种编码之间做选择。
动手实验
- 观察不同格式的编码差异。写一个简单 struct:
分别用#[derive(Serialize)] struct User { id: u64, name: String }serde_json::to_string、bincode::serialize(bincode 1.x 的 API,依赖写bincode = "1")、rmp_serde::to_vec序列化User { id: 42, name: "alice".into() }。打印字节数和内容。看看哪种格式最紧凑、哪种最可读。 - 尝试 bytes 优化。把
Vec<u8>类型的字段直接 derive 和用#[serde(with = "serde_bytes")]分别编码成 MessagePack,对比字节数(字节值全小于 128 时两者一样大,混入 ≥128 的字节后差距才出现,见 §2.3)。 - 思考题:如果 Data Model 没有 option 原语,必须用 enum 路径表达
Option<T>,那么Option<Option<T>>的 JSON 表示会是什么样?为什么这会让 null 和 missing 字段的区分成为一个问题?
延伸阅读
- Serde Data Model 官方文档:本章概念的官方版本。
- serde_bytes 仓库:理解 bytes vs seq 的工程权衡。
- Postcard 格式规范:嵌入式场景下的 Serde 格式,可作为"极简格式"学习样本。
- typetag:补上 Serde Data Model 的 trait object 空白。