Skip to content

第 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) 工作量。

本章的目标有三个:

  1. 拆解 Serializer trait 的结构:为什么有 30 个方法、7 个关联类型、为什么 self 是 move 语义。
  2. 理解 SerializeSeq/SerializeMap/... 这 7 个子 trait:状态机的关联类型如何组织。
  3. 用一个玩具格式走通全流程:从接收调用到产出字节的完整路径。

读完本章,你会拥有"实现一个 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 禁止了这条路。为什么?因为 Serializerserialize_* 方法有泛型参数(例如 serialize_some<T: Serialize>),而泛型参数无法通过虚表动态分发。Serde 放弃了 trait object 能力,换来了静态分发的零开销。

设计意图Sized 是一个 180 度的决策。取舍是"灵活性"换"性能"——你不能运行时切换 Serializer 实现,但所有调用都会被编译器内联优化。想要动态分发的用户要用 erased-serde(它自己通过 trick 绕过这个限制,但性能降 10-20%)。

type Ok。序列化完成后返回什么?看不同格式的选择:

  • serde_json::Serializer 输出到 io::Writetype 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 类型,其 OkError 必须和 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 更干净。

设计意图self by-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:用类型系统表达"不支持"

有些格式天然不支持某些原语。比如一个极简的"只支持单个值"的玩具格式,不支持 seqmapstruct 等复合类型。这种情况下,实现者需要在 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: 结束。self by-value,消耗状态机实例。

7 个子 trait 的对比表:

子 trait元素添加方法key 类型备注
SerializeSeqserialize_element(&mut self, &T)无 key变长
SerializeTupleserialize_element(&mut self, &T)无 key定长
SerializeTupleStructserialize_field(&mut self, &T)无 key定长,有类型名
SerializeTupleVariantserialize_field(&mut self, &T)无 key定长,有 enum 信息
SerializeMapserialize_key(&mut self, &T) + serialize_value(&mut self, &T)运行时 key分两步
SerializeStructserialize_field(&mut self, &'static str, &T)编译期 key一步带 key+value
SerializeStructVariantserialize_field(&mut self, &'static str, &T)编译期 keystruct + enum 信息

注意 SerializeMap 的特殊性——它把 key 和 value 分成两个方法调用。为什么?因为运行时 key 是任意 Serialize 类型,可能是复杂结构;有时你想先把 key 写进输出流再决定 value 怎么算。Map 还提供了一个便捷方法 serialize_entry(key, value) 同时写,默认实现是调用 serialize_keyserialize_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::OkS::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 类型SerializeSeqSerializeTupleSerializeMapSerializeStruct 的关联类型都是 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 实测:30Serializer 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_*3930 + 9 个分布在 7 个 SerializeXxx 子 trait 上的 serialize_element / serialize_key / serialize_value / serialize_entry / serialize_field
SerializeXxx 子 traitserialize_* 方法(除 end
SerializeSeqserialize_element
SerializeTupleserialize_element
SerializeTupleStructserialize_field
SerializeTupleVariantserialize_field
SerializeMapserialize_key / serialize_value / serialize_entry (3 个,serialize_entry 是 key+value 合并的 helper)
SerializeStructserialize_field
SerializeStructVariantserialize_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,其中 SerializeSeqSerializeTupleSerializeMapSerializeStruct 等 7 个关联类型全部指向同一个 Compound<'a, W, F>

这不是偷懒,而是 JSON 语法的结果。JSON 的数组、tuple、tuple struct 都是 [...];map、struct 都是 {...};struct variant 也只是对象外面多一层 variant 名。格式层只需要知道"现在是在写对象还是数组、是否已经写过第一个元素",不需要为每一种 Serde Data Model 形态单独造一个类型。

serde_json/src/ser.rs:349-360serialize_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-632Compound 实现 SerializeMapserialize_key 会根据 State::First 判断是否需要逗号,再把 key 交给 MapKeySerializerMapKeySerializerserde_json/src/ser.rs:773-795 单独实现 Serializer,它的职责只有一个:保证 JSON object 的 key 能被写成字符串。

这一小段源码把关联类型的意义讲透了:

Serde trait 概念serde_json 落点工程含义
type SerializeMapCompound<'a, W, F>object 写入是一个有状态过程
serialize_mapser.rs:349-360入口只打开 {,不立即完成对象
serialize_keyser.rs:619-632key 必须通过专用 serializer 限制成字符串
StateCompound::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_stringfloat_key_must_be_finite 两类错误,MapKeySerializer 的存在就是为了在"正在写 key"这个阶段收窄可接受类型。顶层 serde_json/src/ser.rs:2240-2244to_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 格式侧的唯一入口。它的设计核心是:

  1. Sized + self by-value:强制静态分发,零运行时开销,且用所有权表达"用一次就没"的协议。
  2. type Ok / type Error:允许"写入式"和"构建式"两种风格共存。
  3. 7 个关联类型 + 7 个子 trait:状态机原语的类型化表达,Ok/Error 的连带约束保证跨 trait 一致性。
  4. Impossible 类型:用空 enum + PhantomData 在类型系统里表达"不支持",零运行时开销。
  5. 默认实现的克制:对"明确必要"的方法保持裸 trait,对"可默认行为"的方法提供默认,在便利和明确之间取平衡。

动手实验

  1. 实现一个"最小 Serializer"。创建一个只支持 booli32str 的 Serializer,其他方法用 Impossible。测试它序列化 42i32(成功)和 vec![1, 2, 3](失败,错误信息清晰)。
  2. 观察静态分发。用 cargo asm 查看 serde_json::to_string(&true) 的汇编输出。你会看到完全没有虚表、没有间接调用——只是直接的字节写入。
  3. 理解 self by-value。写一个 Serialize 实现,尝试在里面调用 serializer.serialize_str(...) 两次——看编译器报什么错(value used here after move)。这个错误就是 Serde API 故意制造的。

延伸阅读

基于 VitePress 构建