Serde 元编程

第 3 章 Serializer trait:序列化端的 O(N) 抽象

作者 杨艺韬 · 8,599 字 · 发布于 · 更新于

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 个 serialize_* 方法、9 个关联类型(Ok、Error 加 7 个 SerializeXxx)、为什么 self 是 move 语义。
  2. 理解 SerializeSeq/SerializeMap/... 这 7 个子 trait:状态机的关联类型如何组织。
  3. 看格式侧怎么落地:用 Impossible 占位写出最小 Serializer 的骨架,再对照 serde_json 的实现,看一次调用如何变成输出字节。

读完本章,你会拥有"实现一个 Serializer 的能力"——虽然还不是生产级,但足以理解 serde_json 的 Serializer 代码。

3.2 trait 的骨架:Ok、Error 与关联类型

先看 Serializer 的"头部"——trait 定义和所有关联类型:

// 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 是一个有意为之的决策。取舍是"灵活性"换"性能"——你不能运行时切换 Serializer 实现,但所有调用都是静态分发、可以被编译器内联优化。想要动态分发的用户要用 erased-serde(它自己通过 trick 绕过这个限制,代价是多一层虚表分发)。

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:

// 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"能无缝衔接:

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 个 serialize_* 方法的组织

30 个 serialize_* 方法可以分成两大类:

一次性方法(23 个):调用完立刻返回结果。

fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error>;
fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error>;
// ... 共 12 种数值
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 对象,后续继续调用。

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 设计中最反直觉的细节之一。看:

fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error>;
//                ^^^^ 不是 &mut self

直觉上 Serializer 是一个"写入目标",应该是 &mut self——我往里写东西,你别销毁我。但 Serde 偏偏用 self——序列化一次就消耗掉 Serializer。

为什么?三个原因:

原因 1:防止多次序列化。 一个 Serializer 只能产出一份"完整的序列化结果"。如果允许 &mut self,用户可能误写:

serializer.serialize_str("hello")?;   // ???
serializer.serialize_str("world")?;   // ???
// 这输出了什么?"hello""world"?两个独立的 JSON 值?

Serde 禁止这种歧义——把 Serializer move 掉,用户想再用只能拿新的。这在编译期杜绝了"多值序列化"的问题。

原因 2:允许 Serializer 在内部做状态转移。 如果 Serializer 的不同状态对应不同类型(比如"未写入"和"已写入"是两个类型),move 语义让这种状态机自然表达:

// 理论上可以写
impl Serializer for JsonSerializer<UninitializedState> {
    fn serialize_bool(self, v: bool) -> Result<JsonValue, Error> {
        // 消耗 self,返回一个完成态
    }
}

虽然 serde_json 没有这么做(它用一个 Serializer 贯穿始终),但 move 语义把这种设计空间开放了。

原因 3:和状态机子 trait 统一。 看 SerializeSeq::end:

fn end(self) -> Result<Self::Ok, Self::Error>;

也是 self——序列化结束后,SerializeSeq 实例被消耗。如果 Serializer 的方法是 &mut self,状态机方法是 self,用户会困惑"为什么一致性被打破"。统一用 self 更干净。

设计意图:self by-value 的设计不是"让用户不方便",而是"让错误不可能发生"。Rust 的所有权系统允许 API 设计者用类型系统编码协议——"这个东西用一次就没了"通过 move 语义完美表达,任何试图二次使用都是编译错误而非运行时错误。

一个实际影响: 当你实现 Serialize 时,如果方法内部需要对同一个 serializer 值做两次不同的事,不能调用两次——泛型参数 S 按值传入、无法再借用,只能改走状态机入口(比如先 serialize_tuple,再逐个 serialize_element)。这正是 Serde 希望的——它强迫你走正确的"要么一次搞定,要么状态机"的路径。

3.5 默认实现与性能兜底

30 个 serialize_* 方法里只有 serialize_i128 / serialize_u128 两个有默认实现,看 i128:

// 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——它没有默认实现(serde_core/src/ser/mod.rs:821-823,必须由格式实现)。这里要注意一个容易想当然的地方:它的文档注释并没有推荐任何实现写法。serde_core/src/ser/mod.rs:790-820 那段注释只有一行散文 "Serialize a [Some(T)] value.",后面挂的 doctest 演示的是调用方——impl<T> Serialize for Option<T> 里 match *self { Some(ref value) => serializer.serialize_some(value), None => serializer.serialize_none() }。换句话说,"Some(42) 编成什么" 从头到尾由格式自己决定,Serde 一个字都没暗示。

对比之下,serialize_newtype_struct 的文档是明确给了建议的(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).

同样是"无默认实现",Serde 对这两个方法的态度并不一样——newtype 有推荐值,option 没有。

serde_json 选的是透明转发(serde_json/src/ser.rs:276-281,函数体只有 value.serialize(self) 一行),所以 JSON 里 Some(42) 就等于直接序列化 42,不加任何标签;Bincode 这类二进制格式则会在 serialize_some 里先写一个 0x01 标签再转发。同一个"无默认"方法,两种格式做出完全不同的选择——这正是 Serde 不给默认的原因。

注意区别:i128 是"给默认、默认报错",newtype_struct 是"不给默认、但文档写明推荐透明转发",some 是"不给默认、也不推荐,格式自己定"。

默认实现的设计哲学:

方法 默认行为 原因
serialize_i128/u128 返回 custom("i128 is not supported")(u128 为 "u128 is not supported") 不能静默丢精度
serialize_some 无默认(必须实现),文档也不给推荐写法 由格式决定"存在"怎么表达
serialize_none 无默认(必须实现) 格式必须显式选择如何编码"空"
serialize_str 无默认(必须实现) 格式必须显式选择字符串表示
serialize_newtype_struct 无默认(必须实现),文档推荐透明转发 大多数格式都选透明
serialize_newtype_variant 无默认(必须实现) 变体名信息太重要,格式必须显式处理

Serde 只给"不支持也合理"的 i128/u128 一个报错默认,其余 28 个方法一律要求格式显式实现——常见写法放进文档而不是放进默认实现,这是 Serde API 对"明确性"的坚持。

别把"30 个 serialize_*"和"trait 里全部方法"搞混。 除这 30 个之外,Serializer 里还挂着 4 个不叫 serialize_*、且全部带默认实现的方法(collect_str 按 std / no_std 分成两份 fn 声明,所以 trait 里一共是 35 个 fn),它们才是这个 trait 里真正"给了默认"的大头:

方法 出处 默认行为
collect_seq serde_core/src/ser/mod.rs:1296-1305 serialize_seq → serialize_element × N → end,长度取自 iterator_len_hint
collect_map serde_core/src/ser/mod.rs:1333-1343 serialize_map → serialize_entry × N → end
collect_str serde_core/src/ser/mod.rs:1373-1378(std/alloc 下) 先 value.to_string() 堆分配一个 String,再转发 serialize_str;no_std 下的那份(serde_core/src/ser/mod.rs:1406-1408)没有默认实现,必须格式自己写
is_human_readable serde_core/src/ser/mod.rs:1458-1461 返回 true——不写就默认自己是"人类可读格式"

is_human_readable 的默认值方向值得记住:默认 true,所以 Bincode、Postcard 这类紧凑格式必须主动 override 成 false,才能让 Ipv4Addr 之类的类型走二进制分支(第 2 章 §2.13.1 实测过这 10 个分支点)。源码注释还把这个选择标成不可回头:serde_core/src/ser/mod.rs:1452-1457 说改变它应当被当作破坏性变更——因为在人类可读模式下序列化出的值,不保证能用紧凑模式读回来。

3.6 Impossible:用类型系统表达"不支持"

有些格式天然不支持某些原语。比如一个极简的"只支持单个值"的玩具格式,不支持 seq、map、struct 等复合类型。这种情况下,实现者需要在 serialize_seq 里返回错误,但 serialize_seq 必须返回 Self::SerializeSeq 类型的对象——即使马上报错,也得有这个类型。

Serde 提供了一个聪明的工具类型解决这个问题:Impossible<Ok, Error>。

// 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:

// 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 编译器推断这段代码是"不可达代码",不需要返回值。

用法示例(一个只支持单值的格式):

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 举例:

// serde_core/src/ser/mod.rs:1907
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_derive 在 #[serde(skip_serializing_if)] 判断为跳过时会生成对它的调用(serde_derive/src/ser.rs:1152-1157 生成调用,:1336-1343 选出 SerializeStruct::skip_field / SerializeStructVariant::skip_field 路径),让格式有机会做特殊处理(比如 JSON 里什么都不写,但某些格式可能需要写一个占位符)。
  • end: 结束。self by-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 }。

sequenceDiagram
    participant U as 用户代码
    participant VS as Vec<User>::serialize
    participant S as JsonSerializer
    participant SS as SerializeSeq
    participant US as User::serialize
    participant SSt as SerializeStruct
    U->>S: to_string(&vec_of_users)
    S->>VS: vec.serialize(json_serializer)
    VS->>S: serializer.serialize_seq(Some(N))
    S-->>VS: Ok(seq_serializer) : SerializeSeq
    Note over VS,SS: 对每个元素
    VS->>SS: seq.serialize_element(&user[0])
    SS->>US: user.serialize(&mut elem_serializer)
    US->>S: elem_serializer.serialize_struct("User", 2)
    S-->>US: Ok(struct_serializer)
    US->>SSt: struct_serializer.serialize_field("id", &user.id)
    SSt->>S: 内部调用 serialize_u64(42)
    SSt-->>US: Ok()
    US->>SSt: struct_serializer.serialize_field("name", &user.name)
    SSt->>S: 内部调用 serialize_str("alice")
    SSt-->>US: Ok()
    US->>SSt: struct_serializer.end()
    SSt-->>US: Ok(())
    SS-->>VS: Ok()
    Note over VS,SS: 循环继续...
    VS->>SS: seq.end()
    SS-->>VS: Ok(())
    VS-->>U: 返回序列化字符串

这张图揭示了几个关键点:

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(数据结构侧),它的定义简单得多:

// serde_core/src/ser/mod.rs:234
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_core/src/ser/impls.rs 1045 行,覆盖了几十种标准库类型),看其中最简单的:

// serde_core/src/ser/impls.rs:21(bool 由 primitive_impl! 宏展开,这里写的是展开后的形态)、:40
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> 为例。真实实现由 seq_impl! 宏生成(宏定义在 serde_core/src/ser/impls.rs:188-207,方法体只有一行 serializer.collect_seq(self),在 serde_core/src/ser/impls.rs:203;Vec<T> 的展开点是 serde_core/src/ser/impls.rs:233-237,BinaryHeap/BTreeSet/HashSet/LinkedList/VecDeque 共用同一份);真正的状态机走法在 collect_seq 的默认实现里:

// serde_core/src/ser/mod.rs:1296(Serializer::collect_seq 的默认实现)
fn collect_seq<I>(self, iter: I) -> Result<Self::Ok, Self::Error>
where
    I: IntoIterator,
    <I as IntoIterator>::Item: Serialize,
{
    let mut iter = iter.into_iter();
    let mut serializer = tri!(self.serialize_seq(iterator_len_hint(&iter)));
    tri!(iter.try_for_each(|item| serializer.serialize_element(&item)));
    serializer.end()
}

这就是典型的"状态机使用模式"——serialize_seq 开始、循环 serialize_element、最后 end。#[derive(Serialize)] 在做的事,就是为用户自定义类型生成类似这种模板化的代码——第 12 章会详细分析。

3.9.1 真实 impls.rs 里为什么不用 ?:tri! 宏的 5.5% / 9% 编译时间

翻开 Serde 的 impls.rs 会遇到一个一开始让人困惑的地方——所有错误传播都不用 ?、也不用 try!,而是一个叫 tri! 的自定义宏。对照 Range 这个类型的真实实现(serde_core/src/ser/impls.rs:247):

impl<Idx> Serialize for Range<Idx>
where
    Idx: Serialize,
{
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        use super::SerializeStruct;
        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,宏定义上面有一段精确交代动机的注释:

// serde_core/src/crate_root.rs:122-133
// 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 解析、内联分析、再消除;tri! 出现的地方一多,这些开销就累积起来。

2. 5.5% / 9% 这两个数字出自维护者写在源码注释里的测量结论(注释没有附测量方法)。注意基线的对应关系:上面那段注释的第三、四行(serde_core/src/crate_root.rs:124-125)写得很清楚——5.5% 是相对 try! 的、9% 才是相对 ? 的。今天的代码里 try! 早被 ? 取代,所以 tri! 真正省下的是那 9%。这是编译 Serde 自身的时间节约;Serde 被大量下游 crate 依赖,每个下游构建都要编译它一次,省下的时间会在整个生态里累加。

3. tri! 只能在 crate 内部使用——它是用 macro_rules! 定义在 crate_root.rs 里、没有 #[macro_export],外部 crate 看不到。这是刻意的:tri! 的简化是"只适用于错误类型从不转换"这个特定约束,导出给普通用户用反而容易被错用——在自己的代码里写 tri!(serde_json::from_str::<T>(s)),就会因为丢失了 From::from 转换而编译不过。

这种"为每一分编译时间较真"的工程风格贯穿整个 Serde 代码库。注意 #[derive(Serialize)] 生成的代码是在用户 crate 里展开的,看不到这个不导出的 tri!,所以生成代码用的是 ?(例如 serde_derive/src/ser.rs:1145 的 #func(&mut __serde_state, #key_expr, #field_expr)?;),第 12 章会展开讲生成代码。

3.10 一个真实的 Serializer 是什么样?

为了让前面的抽象具象化,看 serde_json::Serializer 是如何实现 Serializer trait 的。这里只展示几个关键方法,完整实现在第 17 章分析。

// serde_json 简化版
pub struct Serializer<W> {
    writer: W,                        // 写入目标
    formatter: CompactFormatter,      // 控制空格、缩进等格式细节
}

impl<'a, W: io::Write> ser::Serializer for &'a 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)?;
        if len == Some(0) {
            self.formatter.end_array(&mut self.writer).map_err(Error::io)?;
            Ok(Compound::Map { ser: self, state: State::Empty })
        } else {
            Ok(Compound::Map { ser: self, state: State::First })
        }
    }

    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 枚举(Empty / First / Rest)记录"是否已经写过第一个元素"来决定逗号——这是一个实现细节的简化技巧。

3. 方法体极短。大部分方法只做"写几个字符"的工作。这正是 Data Model 层抽象剥离干净之后的效果——serialize_seq 只管写 [ 并创建 Compound,后续的 serialize_element 各自管自己。

3.11 Serializer trait 的演化与兼容性

Serde 1.0 是 2017 年发布的,API 至今保持向后兼容。对一个使用如此广泛的库来说,这意味着按 1.0 API 写的 Serializer 原则上今天仍能编译。

做到这点的关键是"默认方法的克制添加"。Serde 1.x 版本里偶尔会向 Serializer trait 加方法,但总是带默认实现(比如 serialize_i128 是 1.0.60 才加的——在仓库里核过:git grep "fn serialize_i128" v1.0.59 -- serde/src/ser/mod.rs 无输出、换成 v1.0.60 有一处,而 v1.0.60 这个 tag 的提交日期是 2018-05-25,正配合 Rust 1.26 稳定的 128 位整数)。带默认实现的方法对下游是非破坏性——老的 Serializer 代码不需要改就能升级。

一个更大的内部变动是 crate 拆分。1.0.220 把 Serialize/Serializer/Deserialize/Deserializer 等核心定义从 serde 挪进了新的 serde_core crate(提交 f3869307 即 extract serde_core out of serde,git tag --contains f3869307 的第一个 tag 就是 v1.0.220),serde 这一侧在核心 trait 上只剩一句 pub use serde_core::{de, forward_to_deserialize_any, ser, Deserialize, Deserializer, Serialize, Serializer};(serde/src/lib.rs:252-254),外加一个给生成代码用的 private 模块(对外以 __private228 的名字导出,见 serde/build.rs:7-14)和 derive 宏的再导出等少量条目,但公共 API 一字未改。这种在"接口稳定"前提下内部重构的能力,很大程度上归功于 Serializer trait 结构的清晰分层。

3.11.1 实测:30 是 Serializer trait 内的 serialize_* 数、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_*(不计 :740 文档注释示例里的 1 处) 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 个 serialize_*)。所以第 4 章开头对照表里的"39"和本章 §3.3 的"30"不矛盾——只是计数口径不同。

不过 39 不等于"必须动手写 39 个函数体",两侧各有偏差、方向相反:

  • 往下减:serialize_i128 / serialize_u128 带默认实现(§3.5),不写也能编译 → 主 trait 里真正必填的是 28 个;子 trait 里 SerializeMap::serialize_entry 也带默认(serde_core/src/ser/mod.rs:1855-1862,默认体就是 serialize_key 后接 serialize_value),SerializeStruct / SerializeStructVariant 的 skip_field 同样带默认 → 9 个子 trait serialize_* 里必填 8 个。
  • 往上加:7 个子 trait 各有一个没有默认实现的 end(serde_core/src/ser/mod.rs 里 fn end 共 7 处,全部是裸声明),它们不叫 serialize_*,所以不在 39 这个 grep 口径里,但一个都不能省。

28 + 8 + 7 = 43——这才是"从零写一个 Serializer 最少要填多少个函数体"的真实数字。39 是"方法名以 serialize_ 开头的有多少个",43 是"编译器会因为缺哪几个而报错"。

serialize_entry 是 SerializeMap 的便利 helper——一次传入 key + value、默认实现就是 serialize_key(k)? ; serialize_value(v)——格式 crate 可以重写它做合并优化(比如 binary 格式可以一次 length-prefix 写两个长度)。它从 v1.0.0 起就在 trait 里,并非后来添加,但思路与 §3.11 所说的新增方法一致:给默认实现,同时允许格式重写。

3.11.2 serde_json 如何把关联类型落成一个状态机

本章前面说过,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-480,核心 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 能被写成字符串。

flowchart TD
  A[serialize_map] --> B[Compound::Map]
  B --> C[SerializeMap::serialize_key]
  C --> D[MapKeySerializer]
  B --> E[SerializeMap::serialize_value]
  E --> F[普通 Serializer 继续写 Value]

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

Serde trait 概念 serde_json 落点 工程含义
type SerializeMap Compound<'a, W, F> object 写入是一个有状态过程
serialize_map serde_json/src/ser.rs:349-360 入口只打开 {,不立即完成对象
serialize_key serde_json/src/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:2238-2245 的 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 格式侧的唯一入口。它的设计核心是:

  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"。创建一个只支持 bool、i32、str 的 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 故意制造的。

延伸阅读