Appearance
第 3 章 Serializer trait:序列化端的 O(M) 抽象
3.1 从原语列表到可调用的代码
第 2 章画了一张地图——29 种原语像化学元素周期表一样罗列。但那张地图还不是代码,只是概念。本章要回答:Serde 如何把这张地图翻译成一个 Rust 开发者可以实际调用的 trait?
答案是 Serializer trait。它是整个 Serde 生态"格式侧"的唯一入口——任何格式(JSON、Bincode、MessagePack……)要接入 Serde,就必须实现这个 trait。一旦实现了,这个格式就自动支持所有已经实现 Serialize 的数据结构。这就是第 1 章说的 "M+N 里的那个 N"——N 种格式,N 个 Serializer 实现,O(N) 工作量。
本章的目标有三个:
- 拆解
Serializertrait 的结构:为什么有 30 个方法、7 个关联类型、为什么self是 move 语义。 - 理解
SerializeSeq/SerializeMap/... 这 7 个子 trait:状态机的关联类型如何组织。 - 用一个玩具格式走通全流程:从接收调用到产出字节的完整路径。
读完本章,你会拥有"实现一个 Serializer 的能力"——虽然还不是生产级,但足以理解 serde_json 的 Serializer 代码。
3.2 trait 的骨架:Ok、Error 与关联类型
先看 Serializer 的"头部"——trait 定义和所有关联类型:
rust
// serde/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>;
type SerializeTupleStruct: SerializeTupleStruct<Ok = Self::Ok, Error = Self::Error>;
type SerializeTupleVariant: SerializeTupleVariant<Ok = Self::Ok, Error = Self::Error>;
type SerializeMap: SerializeMap<Ok = Self::Ok, Error = Self::Error>;
type SerializeStruct: SerializeStruct<Ok = Self::Ok, Error = Self::Error>;
type SerializeStructVariant: SerializeStructVariant<Ok = Self::Ok, Error = Self::Error>;
// ... 30 个 serialize_* 方法
}光是这 9 个关联类型就值得拆解一番。它们不是装饰——每一个都承担具体工程职责。
Sized 约束。trait 定义里的 : Sized 意味着 Serializer 必须是一个确定大小的类型。这看似无足轻重,但它排除了一种常见用法:不能直接写 &dyn Serializer。因为 dyn Trait 是不确定大小的,Serde 禁止了这条路。为什么?因为 Serializer 的 serialize_* 方法有泛型参数(例如 serialize_some<T: Serialize>),而泛型参数无法通过虚表动态分发。Serde 放弃了 trait object 能力,换来了静态分发的零开销。
设计意图:
Sized是一个 180 度的决策。取舍是"灵活性"换"性能"——你不能运行时切换 Serializer 实现,但所有调用都会被编译器内联优化。想要动态分发的用户要用erased-serde(它自己通过 trick 绕过这个限制,但性能降 10-20%)。
type Ok。序列化完成后返回什么?看不同格式的选择:
serde_json::Serializer输出到io::Write:type Ok = (),副作用已经发生,不需要返回值serde_json::value::Serializer(构建Value树):type Ok = Value,返回构建出的 JSON AST- 自定义"构建 HashMap"的 Serializer:
type Ok = HashMap<String, Value>
Ok 让 Serializer 既能做"写入式"(Ok = (),靠副作用)又能做"构建式"(Ok = SomeTree,靠返回值)。这是一个简洁的统一。
type Error: Error。错误类型必须实现 serde::ser::Error trait。这个 trait 强制所有序列化错误都能从字符串构造(Error::custom(...))。为什么?因为 Serde Data Model 层可能从"用户数据"(比如 #[serde(serialize_with = ...)] 里的自定义函数)收到任意错误信息,需要能统一包装。
7 个 SerializeXxx 关联类型。这是本章最关键的结构。它们对应第 2 章提到的 7 种"状态机原语":seq、tuple、tuple_struct、tuple_variant、map、struct、struct_variant。每一种都有独立的子 trait:
rust
// serde/serde_core/src/ser/mod.rs:1518
pub trait SerializeSeq {
type Ok;
type Error: Error;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where T: ?Sized + Serialize;
fn end(self) -> Result<Self::Ok, Self::Error>;
}注意类型约束连带:type SerializeSeq: SerializeSeq<Ok = Self::Ok, Error = Self::Error>。这是 Rust 的关联类型 bound 语法——它强制 Serializer 实现者选的 SerializeSeq 类型,其 Ok 和 Error 必须和 Serializer 本身相同。为什么?为了让"开始序列化一个 seq"和"结束序列化一个 seq"能无缝衔接:
rust
let mut seq = serializer.serialize_seq(Some(3))?; // 返回 S::SerializeSeq
seq.serialize_element(&1)?;
seq.serialize_element(&2)?;
seq.serialize_element(&3)?;
seq.end() // 返回 S::SerializeSeq::Ok = S::Ok最后 seq.end() 的返回类型必须和 serializer.serialize_seq(...) 外层的 S::Ok 类型相同,否则在 Vec<T> 的 serialize 实现里无法统一返回。Rust 的关联类型约束让这种"跨 trait 约束"成为可能——在其他语言里,你可能需要额外的模板参数才能表达。
3.3 30 个方法的组织
30 个 serialize_* 方法可以分成两大类:
一次性方法(23 个):调用完立刻返回结果。
rust
fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error>;
fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error>;
// ... 14 种数值
fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error>;
fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error>;
fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error>;
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;
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, ...) -> Result<Self::Ok, Self::Error>;
fn serialize_newtype_struct<T>(self, name: &'static str, value: &T) -> ...;
fn serialize_newtype_variant<T>(self, ...) -> ...;状态机入口方法(7 个):返回一个 SerializeXxx 对象,后续继续调用。
rust
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error>;
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error>;
fn serialize_tuple_struct(self, name, len) -> Result<Self::SerializeTupleStruct, Self::Error>;
fn serialize_tuple_variant(self, ..., len) -> Result<Self::SerializeTupleVariant, Self::Error>;
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error>;
fn serialize_struct(self, name, len) -> Result<Self::SerializeStruct, Self::Error>;
fn serialize_struct_variant(self, ..., len) -> Result<Self::SerializeStructVariant, Self::Error>;两种方法有一个关键共同点——self 是 move 语义(不是 &self 也不是 &mut self)。
3.4 为什么 self 是 move 语义
这是 Serde API 设计中最反直觉的细节之一。看:
rust
fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error>;
// ^^^^ 不是 &mut self直觉上 Serializer 是一个"写入目标",应该是 &mut self——我往里写东西,你别销毁我。但 Serde 偏偏用 self——序列化一次就消耗掉 Serializer。
为什么?三个原因:
原因 1:防止多次序列化。 一个 Serializer 只能产出一份"完整的序列化结果"。如果允许 &mut self,用户可能误写:
rust
serializer.serialize_str("hello")?; // ???
serializer.serialize_str("world")?; // ???
// 这输出了什么?"hello""world"?两个独立的 JSON 值?Serde 禁止这种歧义——把 Serializer move 掉,用户想再用只能拿新的。这在编译期杜绝了"多值序列化"的问题。
原因 2:允许 Serializer 在内部做状态转移。 如果 Serializer 的不同状态对应不同类型(比如"未写入"和"已写入"是两个类型),move 语义让这种状态机自然表达:
rust
// 理论上可以写
impl Serializer for JsonSerializer<UninitializedState> {
fn serialize_bool(self, v: bool) -> Result<JsonValue, Error> {
// 消耗 self,返回一个完成态
}
}虽然 serde_json 没有这么做(它用一个 Serializer 贯穿始终),但 move 语义把这种设计空间开放了。
原因 3:和状态机子 trait 统一。 看 SerializeSeq::end:
rust
fn end(self) -> Result<Self::Ok, Self::Error>;也是 self——序列化结束后,SerializeSeq 实例被消耗。如果 Serializer 的方法是 &mut self,状态机方法是 self,用户会困惑"为什么一致性被打破"。统一用 self 更干净。
设计意图:
selfby-value 的设计不是"让用户不方便",而是"让错误不可能发生"。Rust 的所有权系统允许 API 设计者用类型系统编码协议——"这个东西用一次就没了"通过 move 语义完美表达,任何试图二次使用都是编译错误而非运行时错误。
一个实际影响: 当你实现 Serialize 时,如果方法内部需要对同一个 serializer 值做两次不同的事,不能直接调用两次——必须用 &mut *serializer 借用(通过某种方式)或用状态机入口。这正是 Serde 希望的——它强迫你走正确的"要么一次搞定,要么状态机"的路径。
3.5 默认实现与性能兜底
30 个方法里有几个有默认实现,看 i128 为例:
rust
// serde/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"))
}默认实现是"报错"——为什么不 fallback 到 serialize_i64? 因为 i128 的值可能超过 i64 范围(i128::MAX > i64::MAX)。静默降级会丢数据,Serde 选择明确拒绝,让用户知道这种格式不支持 128 位。
对比 serialize_some:
rust
fn serialize_some<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(self) // 默认就是透明转发
}注意区别:serialize_some 的默认实现是透明转发——默认情况下 Some(42) 就等于直接序列化 42,不加任何标签。这是对 JSON 那种"Option 透明"格式的默认支持。二进制格式想加标签的话,自己覆写就行。
默认实现的设计哲学:
| 方法 | 默认行为 | 原因 |
|---|---|---|
serialize_i128/u128 | 返回 custom("i128 not supported") | 不能静默丢精度 |
serialize_some | 透明转发 value.serialize(self) | JSON-like 默认 |
serialize_none | 无默认(必须实现) | 格式必须显式选择如何编码"空" |
serialize_str | 无默认(必须实现) | 格式必须显式选择字符串表示 |
serialize_newtype_struct | 透明转发 | 大多数格式都选透明 |
serialize_newtype_variant | 无默认(必须实现) | 变体名信息太重要,格式必须显式处理 |
默认实现把"常见情况"自动化,但对"可能导致歧义或数据丢失"的方法坚持要求显式实现——这是 Serde API 对"明确性"的坚持。
3.6 Impossible:用类型系统表达"不支持"
有些格式天然不支持某些原语。比如一个极简的"只支持单个值"的玩具格式,不支持 seq、map、struct 等复合类型。这种情况下,实现者需要在 serialize_seq 里返回错误,但 serialize_seq 必须返回 Self::SerializeSeq 类型的对象——即使马上报错,也得有这个类型。
Serde 提供了一个聪明的工具类型解决这个问题:Impossible<Ok, Error>。
rust
// serde/serde_core/src/ser/impossible.rs:60
pub struct Impossible<Ok, Error> {
void: Void,
ok: PhantomData<Ok>,
error: PhantomData<Error>,
}
enum Void {} // 空 enum——无法被构造Void 是一个空 enum——Rust 里空 enum 无法实例化,因为没有任何变体。这意味着 Impossible 这个 struct 也永远无法实例化(它有一个 Void 类型的字段,而 Void 没有值)。
然后 Impossible 实现了所有 7 个子 trait:
rust
// serde/serde_core/src/ser/impossible.rs:68
impl<Ok, Error> SerializeSeq for Impossible<Ok, Error>
where
Error: ser::Error,
{
type Ok = Ok;
type Error = Error;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Error>
where T: ?Sized + Serialize,
{
let _ = value;
match self.void {} // 对空枚举做 match——编译器能推断这段代码不可达
}
fn end(self) -> Result<Ok, Error> {
match self.void {}
}
}match self.void {} 是关键。对一个空 enum 做 match,不需要任何 arm——因为不存在任何值能满足。Rust 编译器推断这段代码是"不可达代码",不需要返回值。
用法示例(一个只支持单值的格式):
rust
impl Serializer for MyMinimalSerializer {
type Ok = String;
type Error = MyError;
type SerializeSeq = Impossible<String, MyError>;
// ... 其他关联类型都是 Impossible
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Err(MyError::Unsupported("seq"))
}
// ...
}当用户试图用这个 Serializer 序列化 Vec 时,serialize_seq 返回错误,永远不会进入 SerializeSeq 的方法——但类型系统要求"我可以返回这个类型"的承诺仍然被满足。
设计意图:
Impossible用类型系统完成了一个本来需要运行时错误的事:"这个路径永远不会被走到"。这是 Rust 零成本抽象的又一个漂亮例子——一个空 enum 加 PhantomData 就把"不可能"写进了类型,编译后完全没开销(因为永远不会被构造)。
3.7 状态机子 trait 的详细结构
7 个子 trait 的结构高度类似。拿其中最有代表性的 SerializeStruct 举例:
rust
// serde/serde_core/src/ser/mod.rs:1760 左右
pub trait SerializeStruct {
type Ok;
type Error: Error;
fn serialize_field<T>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error>
where T: ?Sized + Serialize;
fn skip_field(&mut self, key: &'static str) -> Result<(), Self::Error> {
let _ = key;
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error>;
}三个方法:
serialize_field: 写一个字段。接收&'static str作为 key——编译期已知字段名。&mut self而不是self——因为会连续调用多次。skip_field: 跳过一个字段。默认实现是空操作。这个方法的存在是为了支持#[serde(skip_serializing_if)]属性——当字段被判断为"不需要序列化"时,格式有机会做特殊处理(比如 JSON 里什么都不写,但某些格式可能需要写一个占位符)。end: 结束。selfby-value,消耗状态机实例。
7 个子 trait 的对比表:
| 子 trait | 元素添加方法 | key 类型 | 备注 |
|---|---|---|---|
SerializeSeq | serialize_element(&mut self, &T) | 无 key | 变长 |
SerializeTuple | serialize_element(&mut self, &T) | 无 key | 定长 |
SerializeTupleStruct | serialize_field(&mut self, &T) | 无 key | 定长,有类型名 |
SerializeTupleVariant | serialize_field(&mut self, &T) | 无 key | 定长,有 enum 信息 |
SerializeMap | serialize_key(&mut self, &T) + serialize_value(&mut self, &T) | 运行时 key | 分两步 |
SerializeStruct | serialize_field(&mut self, &'static str, &T) | 编译期 key | 一步带 key+value |
SerializeStructVariant | serialize_field(&mut self, &'static str, &T) | 编译期 key | struct + enum 信息 |
注意 SerializeMap 的特殊性——它把 key 和 value 分成两个方法调用。为什么?因为运行时 key 是任意 Serialize 类型,可能是复杂结构;有时你想先把 key 写进输出流再决定 value 怎么算。Map 还提供了一个便捷方法 serialize_entry(key, value) 同时写,默认实现是调用 serialize_key 再 serialize_value。
3.8 完整调用序列:追踪 Vec<User> 的序列化
把前面的概念串起来,追踪一个实际例子:序列化 Vec<User>,其中 User { id: u64, name: String }。
这张图揭示了几个关键点:
1. 控制反转。 Vec::serialize 不"写"任何字节——它只调用 serializer.serialize_seq(...) 告诉格式"我是一个 seq,有 N 个元素"。具体怎么写 [、]、,,是 JsonSerializer 自己的事。
2. 递归结构。 一个 struct 里有字段,字段又可能是 struct。每次递归都是 "开始新的容器 → 写入内容 → 结束容器" 的三段式。
3. 静态分发贯穿全程。 每一个方法调用都是 <S: Serializer> 或 <S: SerializeSeq> 这种泛型约束——编译期单态化后,所有调用都是直接跳转,没有虚表查询。这是 Serde 零成本的本质保证。
4. 错误传播。 每个方法返回 Result<_, Self::Error>,? 一路传到最外层。一旦某个字段序列化失败,整个调用链立刻返回错误。
3.9 Serialize trait:从"被序列化"的一端看
前面讨论的都是 Serializer(格式侧)。另一端是 Serialize(数据结构侧),它的定义简单得多:
rust
// serde/serde_core/src/ser/mod.rs:220 左右
pub trait Serialize {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer;
}只有一个方法。 参数是 &self(借用——序列化不应该消耗原值)和 serializer: S(move——上节讲过)。返回值用 S::Ok 和 S::Error——类型由具体 Serializer 决定。
为什么 serializer 是泛型参数而不是 trait object? 这呼应了 Serializer: Sized 的约束——只有泛型才能保证静态分发。如果这里写 serializer: &mut dyn Serializer,编译器就无法单态化,性能会下降一个数量级。
Serialize trait 的实现者是"被序列化的类型"。标准库提供了大量原生实现(文件 serde/serde_core/src/ser/impls.rs 1045 行,覆盖了几十种标准库类型),看其中最简单的:
rust
// serde/serde_core/src/ser/impls.rs
impl Serialize for bool {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer,
{
serializer.serialize_bool(*self)
}
}
impl Serialize for str {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer,
{
serializer.serialize_str(self)
}
}实现的模式非常一致:每个原始类型调用对应的 serialize_xxx 方法,然后结束。没有多余逻辑。这种朴素的一一对应是 Serde 可以被编译器激进优化的前提。
对复合类型稍复杂,以 Vec<T> 为例:
rust
impl<T> Serialize for Vec<T>
where T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.len()))?;
for e in self {
seq.serialize_element(e)?;
}
seq.end()
}
}这就是典型的"状态机使用模式"——serialize_seq 开始、循环 serialize_element、最后 end。#[derive(Serialize)] 在做的事,就是为用户自定义类型生成类似这种模板化的代码——第 12 章会详细分析。
3.9.1 真实 impls.rs 里为什么不用 ?:tri! 宏的 5.5% 编译时间
翻开 Serde 的 impls.rs 会遇到一个一开始让人困惑的地方——所有错误传播都不用 ?、也不用 try!,而是一个叫 tri! 的自定义宏。对照 Range 这个类型的真实实现(serde_core/src/ser/impls.rs:256):
rust
impl<Idx: Serialize> Serialize for Range<Idx> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer,
{
let mut state = tri!(serializer.serialize_struct("Range", 2));
tri!(state.serialize_field("start", &self.start));
tri!(state.serialize_field("end", &self.end));
state.end()
}
}每一行的 ? 都被换成了 tri!(...)。这不是代码风格洁癖——打开 serde_core/src/crate_root.rs:122,宏定义上面有一段精确交代动机的注释:
rust
// None of this crate's error handling needs the From::from error conversion
// performed implicitly by the ? operator or the standard library's try!
// macro. This simplified macro gives a 5.5% improvement in compile time
// compared to standard try!, and 9% improvement compared to ?.
macro_rules! tri {
($expr:expr) => {
match $expr {
Ok(val) => val,
Err(err) => return Err(err),
}
};
}三点值得拆解:
1. ? 隐式调用 From::from 是个成本。标准 ? 操作符在返回错误前会插入 From::from(err) 做错误类型转换——允许 Result<T, E1> 在 E1: From<E2> 时自动转为 Result<T, E2>。但Serde 的错误处理从不需要跨类型转换:每个 impl 里返回的 S::Error 就是它收到的 S::Error。那个 From::from 调用在 Serde 场景里恒等于身份函数——但编译器仍要对它做 trait 解析、内联分析、再消除。生成上万个 Serialize 实例时,每次都多几个编译器 work unit。
2. 5.5% / 9% 这两个数字不是估算——是 Serde 维护者实测出来的。源码注释里硬编码写着这两个数字,对应"把所有 ? 改成 tri!"和"把所有 try! 改成 tri!"两种基线。对一个被上万个下游 crate 依赖的库来说,每次 cargo check 省 5-9% CPU 时间会直接反映为生态级的编译时间节约。
3. tri! 只能在 crate 内部使用——它是用 macro_rules! 定义在 crate_root.rs 里、没有 #[macro_export],外部 crate 看不到。这是刻意的:tri! 的简化是"只适用于错误类型从不转换"这个特定约束,导出给普通用户用反而容易被错用——在自己的代码里写 tri!(serde_json::from_str::<T>(s)),就因为丢失了 From::from 转换而编译不过或语义变错。
这种"为每一分编译时间较真"的工程风格贯穿整个 Serde 代码库。下一章讲 #[derive(Serialize)] 生成代码时还会见到同样的 pattern——宏展开出来的代码每一处 tri!,都是这个 5.5% 的一份贡献。
3.10 一个真实的 Serializer 是什么样?
为了让前面的抽象具象化,看 serde_json::Serializer 是如何实现 Serializer trait 的。这里只展示几个关键方法,完整实现在第 17 章分析。
rust
// serde_json 简化版
pub struct Serializer<W> {
writer: W, // 写入目标
formatter: CompactFormatter, // 控制空格、缩进等格式细节
}
impl<W: io::Write> ser::Serializer for &mut Serializer<W> {
type Ok = (); // 写入式,副作用完成,无需返回
type Error = Error;
type SerializeSeq = Compound<'a, W>;
type SerializeStruct = Compound<'a, W>;
// ... 所有复合类型都共用一个 Compound 类型
fn serialize_bool(self, v: bool) -> Result<()> {
self.formatter.write_bool(&mut self.writer, v).map_err(Error::io)
}
fn serialize_i64(self, v: i64) -> Result<()> {
self.formatter.write_i64(&mut self.writer, v).map_err(Error::io)
}
fn serialize_str(self, v: &str) -> Result<()> {
format_escaped_str(&mut self.writer, &mut self.formatter, v).map_err(Error::io)
}
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
self.formatter.begin_array(&mut self.writer).map_err(Error::io)?;
Ok(Compound::Map { ser: self, state: State::Empty })
}
fn serialize_struct(self, _name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
self.serialize_map(Some(len)) // JSON 里 struct 和 map 一样处理
}
// ...
}几个观察:
1. JSON 序列化器把 struct 和 map 等同处理。JSON 的对象语法 {"key":value} 对两者没有区别。但这种等同是 JSON 格式的选择,Bincode 会把它们区分开(struct 省略字段名)。Data Model 的区分给了格式选择权,JSON 选择不利用。
2. 所有复合类型复用一个 Compound 类型。SerializeSeq、SerializeTuple、SerializeMap、SerializeStruct 的关联类型都是 Compound<W>。这个 Compound 内部用一个 State 枚举区分自己处于哪种原语状态——这是一个实现细节的简化技巧。
3. 方法体极短。大部分方法只做"写几个字符"的工作。这正是 Data Model 层抽象剥离干净之后的效果——serialize_seq 只管写 [ 并创建 Compound,后续的 serialize_element 各自管自己。
3.11 Serializer trait 的演化与兼容性
Serde 1.0 是 2017 年发布的,API 至今保持向后兼容。这对一个使用如此广泛的库来说是惊人的——意味着任何 2017 年写的 Serializer 今天仍能编译。
做到这点的关键是"默认方法的克制添加"。Serde 1.x 版本里偶尔会向 Serializer trait 加方法,但总是带默认实现(比如 serialize_i128 是 1.26 才加的)。带默认实现的方法对下游是非破坏性——老的 Serializer 代码不需要改就能升级。
一个反面案例是类型系统的变化。Rust 在 2.0 edition 后修改了 trait object 的安全性规则,Serde 也跟着调整过几次内部实现,但公共 API 保持了原样。这种在"接口稳定"前提下内部重构的能力,很大程度上归功于 Serializer trait 结构的清晰分层。
3.11.1 实测:30 是 Serializer trait 内的方法数、39 是含 SerializeXxx 子 trait 的总数
§3.3 说"30 个 serialize_* 方法 = 23 一次性 + 7 状态机入口"——实测 serde_core/src/ser/mod.rs 给出两个互不矛盾的数字——
| 计数口径 | 实测 | 包含范围 |
|---|---|---|
pub trait Serializer { ... } 块内的 fn serialize_* | 30 | 本章 §3.3 的 23+7 划分;用户实现 Serializer 时要面对的方法数 |
整个 ser/mod.rs 文件里所有 fn serialize_* | 39 | 30 + 9 个分布在 7 个 SerializeXxx 子 trait 上的 serialize_element / serialize_key / serialize_value / serialize_entry / serialize_field |
| SerializeXxx 子 trait | serialize_* 方法(除 end) |
|---|---|
SerializeSeq | serialize_element |
SerializeTuple | serialize_element |
SerializeTupleStruct | serialize_field |
SerializeTupleVariant | serialize_field |
SerializeMap | serialize_key / serialize_value / serialize_entry (3 个,serialize_entry 是 key+value 合并的 helper) |
SerializeStruct | serialize_field |
SerializeStructVariant | serialize_field |
两个数字背后的工程含义——用户写一个新 Serializer 时:必须实现 23 个一次性方法(bool/i*/u*/f*/char/str/bytes/none/some/unit/...)+ 7 个状态机入口,和 7 个 SerializeXxx 子 trait(每个里 1~3 个方法)。总实现负担接近 39 个方法——所以 ch04 §4.10.1 测得的"39"和本章"30"不矛盾——只是计数口径不同。
serialize_entry 是 SerializeMap 的便利 helper——一次传入 key + value、默认实现就是 serialize_key(k)? ; serialize_value(v)——格式 crate 可以重写它做合并优化(比如 binary 格式可以一次 length-prefix 写两个长度)——这是"默认实现的克制添加"(§3.11 主题)的一个具体案例。
3.11.2 serde_json 如何把关联类型落成一个状态机
第 3 章前面说过,Serializer 的 7 个关联类型不是装饰,而是"复合数据写到一半时的状态"。serde_json 给了一个很干净的实物样本:serde_json/src/ser.rs:63-77 为 &mut Serializer<W, F> 实现 serde::Serializer,其中 SerializeSeq、SerializeTuple、SerializeMap、SerializeStruct 等 7 个关联类型全部指向同一个 Compound<'a, W, F>。
这不是偷懒,而是 JSON 语法的结果。JSON 的数组、tuple、tuple struct 都是 [...];map、struct 都是 {...};struct variant 也只是对象外面多一层 variant 名。格式层只需要知道"现在是在写对象还是数组、是否已经写过第一个元素",不需要为每一种 Serde Data Model 形态单独造一个类型。
serde_json/src/ser.rs:349-360 的 serialize_map 先调用 formatter 写对象起始符,再返回 Compound::Map { ser, state }。Compound 本体在 serde_json/src/ser.rs:471-479,核心 variant 只有 Map,另有 Number / RawValue 受 feature 控制。随后 serde_json/src/ser.rs:611-632 给 Compound 实现 SerializeMap,serialize_key 会根据 State::First 判断是否需要逗号,再把 key 交给 MapKeySerializer。MapKeySerializer 在 serde_json/src/ser.rs:773-795 单独实现 Serializer,它的职责只有一个:保证 JSON object 的 key 能被写成字符串。
这一小段源码把关联类型的意义讲透了:
| Serde trait 概念 | serde_json 落点 | 工程含义 |
|---|---|---|
type SerializeMap | Compound<'a, W, F> | object 写入是一个有状态过程 |
serialize_map | ser.rs:349-360 | 入口只打开 {,不立即完成对象 |
serialize_key | ser.rs:619-632 | key 必须通过专用 serializer 限制成字符串 |
State | Compound::Map { state } | 逗号不是字段属性,而是流式写入状态 |
如果没有关联类型,serialize_map 只能返回某种动态 trait object,或者把后续 serialize_key / serialize_value 都塞回主 Serializer。前者引入运行时分发,后者让主 trait 变成"所有状态共享一个大接口"。Serde 选择关联类型,是把状态机拆成类型机:进入 map 后,编译器知道你拿到的是一个实现 SerializeMap 的对象;结束前不能把它当普通 serializer 乱用;end 消费状态,表达"对象写完"。
这也解释了为什么用户实现 Serializer 时会觉得方法多。方法多换来的不是 API 臃肿,而是协议阶段被类型化。serde_json 用一个 Compound 复用多个阶段,是格式实现内部的简化;对 Serde trait 来说,阶段仍然清楚分开。
还有一个细节能反证这套设计的必要性:JSON object 的 key 必须是字符串。serde_json/src/ser.rs:787-792 定义了 key_must_be_a_string 和 float_key_must_be_finite 两类错误,MapKeySerializer 的存在就是为了在"正在写 key"这个阶段收窄可接受类型。顶层 serde_json/src/ser.rs:2240-2244 的 to_string 文档也明确说,序列化可能因为 T 自己失败,或者因为 T 包含非字符串 key 的 map 而失败。
如果 Serializer 只有一个"写任意值"入口,就很难表达"现在写的是 object key,所以 bool、array、object 都不合法"。Serde 把 map 拆成 serialize_key / serialize_value,serde_json 再用 MapKeySerializer 把 key 阶段单独约束起来,错误才能发生在正确位置。这个设计会让 trait 看起来更宽,但它换来的不是抽象洁癖,而是格式约束可以精确落点。
换成二进制格式,类似边界仍然存在,只是约束不同:有的格式要求先写长度,有的格式允许 key 是整数,有的格式完全不保留字段名。Serde 不把这些规则硬编码进 Data Model,而是让每个 Serializer 在对应状态机方法里决定。Serializer trait 的设计目标不是替所有格式做决定,而是给格式一个足够细的决策点。
因此,实现 Serializer 时最重要的不是把三十多个方法机械填满,而是先画清楚本格式的状态机:哪些入口立即写值,哪些入口只开启一个复合结构,哪些阶段要拒绝某些 Rust 值。方法数量只是表象,真正的设计对象是这些阶段约束。
3.12 本章小结
Serializer trait 是 Serde 格式侧的唯一入口。它的设计核心是:
Sized + selfby-value:强制静态分发,零运行时开销,且用所有权表达"用一次就没"的协议。type Ok/type Error:允许"写入式"和"构建式"两种风格共存。- 7 个关联类型 + 7 个子 trait:状态机原语的类型化表达,
Ok/Error的连带约束保证跨 trait 一致性。 Impossible类型:用空 enum + PhantomData 在类型系统里表达"不支持",零运行时开销。- 默认实现的克制:对"明确必要"的方法保持裸 trait,对"可默认行为"的方法提供默认,在便利和明确之间取平衡。
动手实验
- 实现一个"最小 Serializer"。创建一个只支持
bool、i32、str的 Serializer,其他方法用Impossible。测试它序列化42i32(成功)和vec。 - 观察静态分发。用
cargo asm查看serde_json::to_string(&true)的汇编输出。你会看到完全没有虚表、没有间接调用——只是直接的字节写入。 - 理解 self by-value。写一个
Serialize实现,尝试在里面调用serializer.serialize_str(...)两次——看编译器报什么错(value used here after move)。这个错误就是 Serde API 故意制造的。
延伸阅读
- Serde Serializer trait 官方文档:本章内容的 API 参考版。
- Serde "Implementing a Serializer" 教程:从零实现一个 Serializer 的官方教程。
- erased-serde 源码:看一下"如何把 Sized trait 转成 trait object"的 trick,能深化对静态分发约束的理解。
serde/serde_core/src/ser/impossible.rs全文:216 行代码,读完对"空类型技巧"有完整认识。