Serde 元编程

第 12 章 Serialize 代码生成:结构体与枚举

作者 杨艺韬 · 14,236 字 · 发布于 · 更新于

12.1 从设计意图到真实代码

第 10 章画了 serde_derive 的整体架构,第 11 章拆解了属性系统。现在我们走到"生成 impl Serialize 块"这一步——这是整个过程的终点,前面所有工作(解析 AST、解析属性、检查一致性)都是为了这一刻做准备。

本章的主角是 serde_derive/src/ser.rs——1369 行代码、21 个 serialize_* 分派函数(grep -c "^fn serialize_" serde_derive/src/ser.rs 得到的精确计数)。它的任务:接收一个已经处理好的 Container,输出一段 impl Serialize for T 的 TokenStream。

核心模式很简单:根据 Container::data 分派到不同的生成函数。每个函数按照第 3 章讲的 Serializer 协议调用对应方法,组合成最终 impl 块。

但实际情况复杂得多,因为要处理:

  • 四种 struct 形态:Struct(命名)、Tuple、Newtype、Unit
  • enum 的五种子情况:Unit variant、Newtype variant、Tuple variant、Struct variant,外加四种 tag 模式的组合
  • 十几种影响生成代码的属性:rename、skip、skip_serializing_if、flatten、transparent、serialize_with、remote...

这使得 ser.rs 虽然"只是模板代码",也成了千余行的工程。本章带你走一遍主要分支,理解每一处的生成策略。不把全部代码讲完(那样会变成枯燥的代码朗读)——只聚焦最有代表性的几个分派点。

12.2 expand_derive_serialize 的全貌

// serde_derive/src/ser.rs:13 (精简)
pub fn expand_derive_serialize(input: &mut syn::DeriveInput) -> syn::Result<TokenStream> {
    replace_receiver(input);    // 处理 Self 引用
    let ctxt = Ctxt::new();
    let Some(cont) = Container::from_ast(&ctxt, input, Derive::Serialize, &private.ident()) else {
        return Err(ctxt.check().unwrap_err());
    };
    precondition(&ctxt, &cont);
    ctxt.check()?;

    let ident = &cont.ident;
    let params = Parameters::new(&cont);
    let (impl_generics, ty_generics, where_clause) = params.generics.split_for_impl();
    let body = Stmts(serialize_body(&cont, &params));

    let impl_block = if let Some(remote) = cont.attrs.remote() {
        // remote 模式的 impl:生成固有 impl 里的关联函数,而不是 trait 实现
        let vis = &input.vis;
        quote! {
            #[automatically_derived]
            impl #impl_generics #ident #ty_generics #where_clause {
                #vis fn serialize<__S>(__self: &#remote #ty_generics, __serializer: __S)
                    -> _serde::#private::Result<__S::Ok, __S::Error>
                where __S: _serde::Serializer,
                {
                    #body
                }
            }
        }
    } else {
        // 普通 impl Serialize
        quote! {
            #[automatically_derived]
            impl #impl_generics _serde::Serialize for #ident #ty_generics #where_clause {
                fn serialize<__S>(&self, __serializer: __S)
                    -> _serde::#private::Result<__S::Ok, __S::Error>
                where __S: _serde::Serializer,
                {
                    #body
                }
            }
        }
    };

    Ok(dummy::wrap_in_const(cont.attrs.custom_serde_path(), impl_block))
}

八步:

  1. replace_receiver:把类型定义里的 Self 替换成具体类型名(处理一个 Rust 的边缘情况,见下文)。
  2. Container::from_ast:加工 AST(第 10 章讲过)。
  3. precondition:一些早期检查(如 #[serde(field_identifier)] 不能用于 Serialize)。
  4. ctxt.check()?:把累积的错误转换成 Result 返回。
  5. Parameters::new:封装生成代码需要的参数(self 的变量名、generics 等)。
  6. serialize_body:核心分派——根据 cont.data 生成方法体 Fragment。
  7. 选择 impl 外壳:remote 模式(生成关联函数)vs 普通模式(实现 trait)。
  8. dummy const 包装(第 10 章讲过)。

Parameters 是一个很重要的辅助类型:

struct Parameters {
    self_var: Ident,              // "self" 或 "__self"(remote 模式)
    this_type: syn::Path,          // impl 目标类型的路径(本地类型是单个 Ident,remote 是远端路径)
    this_value: syn::Path,         // 同上,但泛型参数写成 ::<T> 形式,用于表达式位置
    generics: syn::Generics,       // 加了 Serialize bound 的 generics
    is_remote: bool,
    is_packed: bool,
}

把"生成代码时需要的上下文"打包——下层函数只需要 &Parameters 就能访问所有必要信息。

12.3 serialize_body:五路分派

serialize_body 函数是代码生成的入口(serde_derive/src/ser.rs:171-189):

// serde_derive/src/ser.rs:171-189(原文,match 各臂折成了单行)
fn serialize_body(cont: &Container, params: &Parameters) -> Fragment {
    if cont.attrs.transparent() {
        serialize_transparent(cont, params)
    } else if let Some(type_into) = cont.attrs.type_into() {
        serialize_into(params, type_into)
    } else {
        match &cont.data {
            Data::Enum(variants) => serialize_enum(params, variants, &cont.attrs),
            Data::Struct(Style::Struct, fields) => serialize_struct(params, fields, &cont.attrs),
            Data::Struct(Style::Tuple, fields) => serialize_tuple_struct(params, fields, &cont.attrs),
            Data::Struct(Style::Newtype, fields) => serialize_newtype_struct(params, &fields[0], &cont.attrs),
            Data::Struct(Style::Unit, _) => serialize_unit_struct(&cont.attrs),
        }
    }
}

两个前置短路 + 一个五臂 match:

  • transparent 属性(serde_derive/src/ser.rs:172):直接转发字段的 serialize
  • into 属性(type_into(),serde_derive/src/ser.rs:174):先 clone 转成目标类型,再序列化
  • 都不命中才进 match &cont.data(serde_derive/src/ser.rs:177-187)——五臂:Enum 一臂,Struct 的 Struct / Tuple / Newtype / Unit 各一臂

本章后文说的「serialize_body 五路分派」指的就是这个 match 的五臂;加上前面两个属性短路,serialize_body 实际会落到 7 个生成函数中的一个。

Fragment 是什么? 看 serde_derive/src/fragment.rs(74 行):

pub enum Fragment {
    Expr(TokenStream),        // 单个表达式
    Block(TokenStream),        // 一段语句块
}

Fragment 是"代码片段"的抽象——它要么是一个表达式(42、foo()),要么是一段语句块({ let x = 1; x })。这个区分重要,因为上下文可能要求表达式(赋值右侧)或语句(单独一行)。

quote_expr! / quote_block! 这两个宏分别构造 Expr 和 Block Fragment。最终 Stmts(fragment) 把 Fragment 包装成可以在 impl fn serialize {} 里放的语句。

12.4 最简单的:unit struct

// serde_derive/src/ser.rs:223
fn serialize_unit_struct(cattrs: &attr::Container) -> Fragment {
    let type_name = cattrs.name().serialize_name();

    quote_expr! {
        _serde::Serializer::serialize_unit_struct(__serializer, #type_name)
    }
}

用户写:

#[derive(Serialize)]
struct Empty;

生成:

_serde::Serializer::serialize_unit_struct(__serializer, "Empty")

一个方法调用搞定——因为 unit struct 没有数据。这是本章最简单的例子。

注意 cattrs.name().serialize_name()——属性系统的输出被用来决定类型名。如果用户写了 #[serde(rename = "OtherName")],生成的代码里就是 "OtherName" 而不是 "Empty"。属性系统和代码生成在这里自然衔接。

为什么 unit struct 还要传 type_name?Serializer 协议设计时的权衡——严格说 unit struct 没有任何数据、理论上 serializer.serialize_unit() 就够了(serialize_unit 是 Serializer trait 上另一个方法、完全没字符串参数)。Serde 却专门留了 serialize_unit_struct(serializer, name) 的独立方法——多出一个 name 参数。原因是某些格式需要区分"unit 值"和"具名 unit 类型"——bincode 两者没差别(都 0 字节)、JSON 两者都是 null、但一些自描述格式(比如 RON 打开 struct name 输出时)会把 struct Foo; 编成 Foo 这个 token、而把裸 unit () 编成别的。传 name 是给这些格式保留区分能力——Serde 的哲学是 "让格式自己决定"、数据模型层总是把信息提供给下游、哪怕 JSON 这种主流格式会忽略它。

这是 Serde 设计纪律的一次体现——"宁可多传、不能少传"。serialize_unit_struct 在 Serializer trait 里是必须实现的方法(没有默认实现,serde_core/src/ser/mod.rs:861),trait 文档只写了一句 "A reasonable implementation would be to forward to serialize_unit"(serde_core/src/ser/mod.rs:845)——serde_json 就是这么做的(serde_json 的 src/ser.rs:209-211 的方法体只有一行 self.serialize_unit()),多传的 name 被"转发到上级丢弃"。上游付一行代码的代价、换来下游任意格式的自由度。

12.5 Newtype struct:透明转发

// serde_derive/src/ser.rs:231
fn serialize_newtype_struct(
    params: &Parameters,
    field: &Field,
    cattrs: &attr::Container,
) -> Fragment {
    let type_name = cattrs.name().serialize_name();

    let mut field_expr = get_member(
        params,
        field,
        &Member::Unnamed(Index { index: 0, span: Span::call_site() }),
    );
    if let Some(path) = field.attrs.serialize_with() {
        field_expr = wrap_serialize_field_with(params, field.ty, path, &field_expr);
    }

    let span = field.original.span();
    let func = quote_spanned!(span=> _serde::Serializer::serialize_newtype_struct);
    quote_expr! {
        #func(__serializer, #type_name, #field_expr)
    }
}

生成代码:

_serde::Serializer::serialize_newtype_struct(__serializer, "Millimeters", &self.0)

三个细节:

1. get_member(serde_derive/src/ser.rs:1279-1306)根据 Parameters::self_var、is_remote、is_packed、getter 四个条件生成字段访问表达式。本地非 packed 类型是最简单的 &self.0(serde_derive/src/ser.rs:1282-1288);remote 模式并不是简单换个变量名——它还会套一层 _serde::#private::ser::constrain::<#ty>(...)(serde_derive/src/ser.rs:1289-1301,有无 getter 两条分支都套),用这个恒等函数(serde/src/private/ser.rs:12-14)把 #[serde(getter)] 的返回类型钉死成字段声明的类型;repr(packed) 时表达式又变成 &{self.0}(先按值拷出来再取引用,避免对未对齐字段取引用,serde_derive/src/ser.rs:1283-1287)。封装这个细节让上层生成代码对四种组合无感知。

2. serialize_with 属性处理:如果字段上有 #[serde(serialize_with = "my_fn")],wrap_serialize_field_with 会包装字段表达式成 &SerializeWith { field: &self.0, ... }——其中 SerializeWith 是一个临时 struct,它的 Serialize 实现调用用户指定的 my_fn。第 16 章会深入讲这个技巧。

3. quote_spanned! 用 field.original.span():给生成的 serialize_newtype_struct 这个函数路径挂上用户原字段的 span——如果字段类型不实现 Serialize,主报错仍落在 #[derive(Serialize)] 那一行,但字段上会多出一条次标注,指明是哪个字段。

这就是第 8 章讲的 span 管理的实战应用。字段级的序列化调用都用 quote_spanned! 生成函数路径。

span 的实际效果——用 rustc 1.96.0 实测:struct Millimeters(NoSer);(NoSer 没实现 Serialize)报 E0277 "the trait bound NoSer: serde::Serialize is not satisfied",主标注(^^^)落在 #[derive(Serialize)] 里的 Serialize 上,字段类型 NoSer 下面另有一条次标注 "required by a bound introduced by this call"。拿一个最小的过程宏对照:同样的调用改用 quote! 生成,主标注位置不变,指向字段的这条次标注就没有了——用户只看到"整个 derive 宏在报错",字段一多就得挨个排查。serde_derive 为此在字段级的序列化调用处都用 field.original.span()(field.original 是 &syn::Field、即用户源码里那个字段的原始语法树节点;实测次标注落在命名字段的字段名、tuple 字段的类型上)。代价仅仅是多写一个 quote_spanned! 而非 quote!。

12.6 Struct:状态机模式

// serde_derive/src/ser.rs:296 (简化)
fn serialize_struct(params: &Parameters, fields: &[Field], cattrs: &attr::Container) -> Fragment {
    assert!(fields.len() as u64 <= u64::from(u32::MAX));

    let has_non_skipped_flatten = fields
        .iter()
        .any(|field| field.attrs.flatten() && !field.attrs.skip_serializing());
    if has_non_skipped_flatten {
        serialize_struct_as_map(params, fields, cattrs)
    } else {
        serialize_struct_as_struct(params, fields, cattrs)
    }
}

两种分派:

  • 如果有(没被 skip_serializing 的)#[serde(flatten)] 字段,走 map 模式(因为 flatten 会引入未知字段数量)——判定就是上面 has_non_skipped_flatten 这个 any(serde_derive/src/ser.rs:305-312)
  • 否则走 struct 模式(字段数量编译期已知)

"未知字段数量"在生成代码里的具体形态是:serialize_struct_as_map 开头调的是 _serde::Serializer::serialize_map(__serializer, _serde::#private::None)(serde_derive/src/ser.rs:388)——serialize_map 的 len 形参类型是 Option<usize>(serde_core/src/ser/mod.rs:1188),flatten 模式直接传 None、放弃长度提示。这也是 flatten 在要把长度写进编码头的格式里容易出问题的根因——CBOR 还能退回"不定长 map"编码,MessagePack 的 map 格式则都要求前置长度。

看 serialize_struct_as_struct(精简版):

// serde_derive/src/ser.rs:328
fn serialize_struct_as_struct(
    params: &Parameters,
    fields: &[Field],
    cattrs: &attr::Container,
) -> Fragment {
    let serialize_fields = serialize_struct_visitor(fields, params, false, &StructTrait::SerializeStruct);

    let type_name = cattrs.name().serialize_name();
    let tag_field = serialize_struct_tag_field(cattrs, &StructTrait::SerializeStruct);
    let tag_field_exists = !tag_field.is_empty();

    let mut serialized_fields = fields
        .iter()
        .filter(|&field| !field.attrs.skip_serializing())
        .peekable();

    // 计算字段数:普通字段贡献常量 1,带 skip_serializing_if 的留到运行时判断
    let let_mut = mut_if(serialized_fields.peek().is_some() || tag_field_exists);
    let len = serialized_fields
        .map(|field| match field.attrs.skip_serializing_if() {
            None => quote!(1),
            Some(path) => {
                let field_expr = get_member(params, field, &field.member);
                quote!(if #path(#field_expr) { 0 } else { 1 })
            }
        })
        .fold(
            quote!(#tag_field_exists as usize),
            |sum, expr| quote!(#sum + #expr),
        );

    quote_block! {
        let #let_mut __serde_state = _serde::Serializer::serialize_struct(__serializer, #type_name, #len)?;
        #tag_field
        #(#serialize_fields)*
        _serde::ser::SerializeStruct::end(__serde_state)
    }
}

核心生成代码的形状:

let mut __serde_state = _serde::Serializer::serialize_struct(__serializer, "User", FIELD_COUNT)?;
/* 对每个字段:*/
_serde::ser::SerializeStruct::serialize_field(&mut __serde_state, "FIELD_NAME", &self.FIELD)?;
/* ... */
_serde::ser::SerializeStruct::end(__serde_state)

注意字段计数的精妙处理:

  • 如果字段没有 skip_serializing_if,贡献 1。
  • 如果有 skip_serializing_if = "fn",贡献 if fn(&self.field) { 0 } else { 1 }——运行时决定。
  • 所有字段的贡献求和得到最终字段数——一半编译期常量一半运行时判断。

这种"字段数编译期尽可能静态、必要时动态"的设计来自 Serializer trait 的 serialize_struct(self, name, len: usize) 签名——len 是 usize 必须的。

为什么 len 要是精确值而不是上界?——这个设计约束不是 Rust 语言层面的要求、而是 Serializer trait 文档写死的显式承诺:serialize_struct 的文档原文是 "The name is the name of the struct and the len is the number of data fields that will be serialized. len does not include fields which are skipped with [SerializeStruct::skip_field]."(serde_core/src/ser/mod.rs:1194-1196)——被 skip_field 跳过的字段不计入 len,白纸黑字。对那些把 map / array 的元素个数前置写进编码头的格式(CBOR、MessagePack 这一类),解码侧就按这个数值预读,多报少报都会把流读错位。所以 serde_derive 不能偷懒用"字段总数"、必须精确减去 skip_serializing_if 为真的字段数——这就是上文那个 fold 表达式的由来。这也是为什么 skip_serializing_if 和 skip_field 要同时存在——自描述格式(JSON)不需要"运行时跳过字段但要通知 Serializer",按位置编码的格式需要。SerializeStruct::skip_field 在 trait 里有默认实现(什么都不做、返回 Ok(()),serde_core/src/ser/mod.rs:1919-1926),serde_json 全仓没有覆写它,所以在 JSON 里是 no-op;需要按位置编码的格式可以覆写它写入占位——同一份生成代码兼容两类格式。

注意 skip_serializing(无条件跳过)和 skip_serializing_if(条件跳过)的区别:

  • skip_serializing:在 filter 里排除,不出现在字段迭代中。
  • skip_serializing_if:仍然迭代,但加 if 判断;即使跳过,也要调 skip_field(某些格式需要知道"有这个字段但被略过")。

第 11 章讲的属性系统在这里全部落地为生成代码的 if 分支、字段过滤等。

12.7 Struct 字段迭代:serialize_struct_visitor

// serde_derive/src/ser.rs:1105 (简化)
fn serialize_struct_visitor(
    fields: &[Field],
    params: &Parameters,
    is_enum: bool,
    struct_trait: &StructTrait,
) -> Vec<TokenStream> {
    let mut dst_fields = Vec::new();

    for field in fields {
        if field.attrs.skip_serializing() {
            continue;
        }
        let member = &field.member;

        // enum 的 struct variant 里字段已被 match arm 绑定成同名变量(`ref x`),
        // 直接用 #member;普通 struct 才走 get_member 生成 `&self.x`
        let mut field_expr = if is_enum {
            quote!(#member)
        } else {
            get_member(params, field, member)
        };

        let key_expr = field.attrs.name().serialize_name();

        let skip = field
            .attrs
            .skip_serializing_if()
            .map(|path| quote!(#path(#field_expr)));

        // 如果有 serialize_with,包装成 __SerializeWith
        if let Some(path) = field.attrs.serialize_with() {
            field_expr = wrap_serialize_field_with(params, field.ty, path, &field_expr);
        }

        let span = field.original.span();
        let ser = if field.attrs.flatten() {
            let func = quote_spanned!(span=> _serde::Serialize::serialize);
            quote! {
                #func(&#field_expr, _serde::#private::ser::FlatMapSerializer(&mut __serde_state))?;
            }
        } else {
            let func = struct_trait.serialize_field(span);   // 返回方法路径的 TokenStream
            quote! {
                #func(&mut __serde_state, #key_expr, #field_expr)?;
            }
        };

        dst_fields.push(match skip {
            None => ser,
            Some(skip) => {
                if let Some(skip_func) = struct_trait.skip_field(span) {
                    quote! {
                        if !#skip {
                            #ser
                        } else {
                            #skip_func(&mut __serde_state, #key_expr)?;
                        }
                    }
                } else {
                    // SerializeMap(flatten 模式)没有 skip_field
                    quote! {
                        if !#skip {
                            #ser
                        }
                    }
                }
            }
        });
    }
    dst_fields
}

每个字段生成的代码形状(最复杂情况):

if !is_skipped(&self.field) {
    _serde::ser::SerializeStruct::serialize_field(
        &mut __serde_state, "serialize_name", &self.field)?;
} else {
    _serde::ser::SerializeStruct::skip_field(&mut __serde_state, "serialize_name")?;
}

这是整个 Serialize 生成的"内循环"——每个字段变成一段这样的代码。

StructTrait 抽象:

enum StructTrait {
    SerializeMap,          // flatten 模式(serialize_field 对应 SerializeMap::serialize_entry)
    SerializeStruct,       // 普通 struct
    SerializeStructVariant, // struct variant of enum
}

不同 trait 的 serialize_field 和 skip_field 方法路径不同。StructTrait 把它们抽象成统一接口——struct_trait.serialize_field(span) 返回带 span 的方法路径(_serde::ser::SerializeStruct::serialize_field 等),skip_field(span) 返回 Option——SerializeMap 没有 skip_field,所以 flatten 模式下被跳过的字段干脆不生成 else 分支。

这个抽象让 serialize_struct_visitor 能被 3 种场景复用:普通 struct、enum 的 struct variant、flatten 模式。代码复用换来可读性——同一个"迭代字段并生成 serialize_field 调用"的逻辑,只有调用的 trait 路径不同。

12.8 Tuple struct:按 index 访问

// serde_derive/src/ser.rs:257 (简化)
fn serialize_tuple_struct(
    params: &Parameters,
    fields: &[Field],
    cattrs: &attr::Container,
) -> Fragment {
    let serialize_stmts = serialize_tuple_struct_visitor(
        fields, params, false, &TupleTrait::SerializeTupleStruct,
    );

    let type_name = cattrs.name().serialize_name();

    let mut serialized_fields = fields
        .iter()
        .enumerate()
        .filter(|(_, field)| !field.attrs.skip_serializing())
        .peekable();

    let let_mut = mut_if(serialized_fields.peek().is_some());
    let len = serialized_fields
        .map(|(i, field)| match field.attrs.skip_serializing_if() {
            None => quote!(1),
            Some(path) => {
                let index = syn::Index { index: i as u32, span: Span::call_site() };
                let field_expr = get_member(params, field, &Member::Unnamed(index));
                quote!(if #path(#field_expr) { 0 } else { 1 })
            }
        })
        .fold(quote!(0), |sum, expr| quote!(#sum + #expr));

    quote_block! {
        let #let_mut __serde_state = _serde::Serializer::serialize_tuple_struct(
            __serializer, #type_name, #len)?;
        #(#serialize_stmts)*
        _serde::ser::SerializeTupleStruct::end(__serde_state)
    }
}

生成代码:

let mut __serde_state = _serde::Serializer::serialize_tuple_struct(
    __serializer, "Point", 0 + 1 + 1)?;
_serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.0)?;
_serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.1)?;
_serde::ser::SerializeTupleStruct::end(__serde_state)

(struct Point(u64, u64) 实测展开;tuple struct 的 fold 起点是 quote!(0),所以字段数写成 0 + 1 + 1 而不是 2。)

和普通 struct 几乎一样——只是字段访问从 self.name 变成 self.0,调用的 trait 从 SerializeStruct 变成 SerializeTupleStruct,没有字段名(serialize_field 只传 value 不传 key)。

12.9 Enum:最复杂的分派

Enum 的序列化是 ser.rs 里最复杂的部分,约 600 行。核心入口(serde_derive/src/ser.rs:395):

// serde_derive/src/ser.rs:395(简化:省略开头的 variants 数量 assert)
fn serialize_enum(params: &Parameters, variants: &[Variant], cattrs: &attr::Container) -> Fragment {
    let self_var = &params.self_var;

    let mut arms: Vec<_> = variants
        .iter()
        .enumerate()
        .map(|(variant_index, variant)| {
            serialize_variant(params, variant, variant_index as u32, cattrs)
        })
        .collect();

    // remote + #[non_exhaustive] 的 enum 多一个兜底 arm,返回 CannotSerializeVariant 错误
    if cattrs.remote().is_some() && cattrs.non_exhaustive() {
        arms.push(quote! { ref unrecognized => /* Err(...) */ });
    }

    quote_expr! {
        match *#self_var {
            #(#arms)*
        }
    }
}

生成一个 match *self 表达式,每个变体一个 arm。核心逻辑在 serialize_variant:

// serde_derive/src/ser.rs:421 (简化)
fn serialize_variant(
    params: &Parameters,
    variant: &Variant,
    variant_index: u32,          // 源码就是 u32(ser.rs:424),不是 usize
    cattrs: &attr::Container,
) -> TokenStream {
    let this_value = &params.this_value;
    let variant_ident = &variant.ident;

    if variant.attrs.skip_serializing() {
        // 跳过——这个 arm 返回 Err(Error::custom("the enum variant E::V cannot be serialized"))
        ...
    } else {
        // match arm 的模式部分
        let case = match variant.style {
            Style::Unit => quote! { #this_value::#variant_ident },
            Style::Newtype => quote! { #this_value::#variant_ident(ref __field0) },
            Style::Tuple => {
                let field_names = (0..variant.fields.len()).map(field_i);   // __field0, __field1, ...
                quote! { #this_value::#variant_ident(#(ref #field_names),*) }
            }
            Style::Struct => {
                let members = variant.fields.iter().map(|f| &f.member);
                quote! { #this_value::#variant_ident { #(ref #members),* } }
            }
        };

        // arm 的值部分:按 tag 模式分派;variant 级 #[serde(untagged)] 强制走 untagged
        let body = Match(match (cattrs.tag(), variant.attrs.untagged()) {
            (attr::TagType::External, false) => serialize_externally_tagged_variant(...),
            (attr::TagType::Internal { tag }, false) => serialize_internally_tagged_variant(..., tag),
            (attr::TagType::Adjacent { tag, content }, false) => serialize_adjacently_tagged_variant(..., tag, content),
            (attr::TagType::None, _) | (_, true) => serialize_untagged_variant(...),
        });

        quote! { #case => #body }
    }
}

两个分派:

  1. 按 tag 模式分派(第 14 章专讲):External/Internal/Adjacent/Untagged——4 种不同的 tag 策略。
  2. 按 variant style 分派:Unit/Newtype/Tuple/Struct——4 种变体形态。

4 × 4 = 16 种组合。每种组合的生成代码都不一样。这就是 ser.rs 行数膨胀的直接来源。

但 16 种并非每种都合法——属性解析阶段就会拦掉非法组合。例如 #[serde(tag = "type")] 的 internally tagged enum 不允许 Tuple variant(多个值塞不进一个 map 的"tag + content 同层"里):serde_derive/src/internals/attr.rs:622 的 decide_tag 遇到字段数不为 1 的 unnamed variant 就报错,错误串写在 serde_derive/src/internals/attr.rs:643-646。

代码生成侧则是兜底:serialize_internally_tagged_variant 真走到 Style::Tuple 分支时用 unreachable!("checked in serde_derive_internals") 收场(serde_derive/src/ser.rs:637)。把"不可达"明确写出来是 Rust 的习惯——unreachable! 本质上仍是 panic,不是给编译器的优化提示;它的价值在于万一走到这里会立刻带着说明报错,同时给未来维护者留下"这里曾经思考过"的痕迹。serde_derive 源码里 unreachable! 一共出现 8 次(ser.rs 4 次、de.rs 2 次、de/ 2 次)。大多数对应 internals/attr.rs / internals/check.rs 已经拦截过的路径——例如 serde_derive/src/ser.rs:194(transparent 撞上 enum,已被 check_transparent(serde_derive/src/internals/check.rs:370 起)拦下)、serde_derive/src/ser.rs:1303(非 remote 却带 getter,已被 check_getter(serde_derive/src/internals/check.rs:78 起)拦下)。也有一处纯属函数内部的控制流不变量:serde_derive/src/ser.rs:713 那句 unreachable!() 挡的是"adjacently tagged 的 Unit variant 又没有 serialize_with",这种情况早在同一函数 serde_derive/src/ser.rs:667-675 就 return 掉了、根本走不到那里,和属性检查无关。合起来是"属性检查报错 + 宏执行时兜底"的双重保险(serde_derive 的 panic 发生在宏展开期间,对用户来说仍是编译失败)。

下一章(第 13 章 Deserialize 代码生成)的 enum 部分更复杂——因为反序列化时还要根据 tag 识别走哪个变体。再下一章(第 14 章)专门剖析 4 种 tag 模式,让你彻底理解"为什么 enum 这么难"。

12.10 externally tagged variant:默认模式

最简单的 tag 模式——外部标签。对 enum E { V(u8) }:

enum E { V(u8) }
let value = E::V(42);
// 序列化成 JSON: {"V": 42}

看 serialize_externally_tagged_variant(serde_derive/src/ser.rs:502,精简:省略了变体级 serialize_with 的提前返回,Newtype 分支里的 quote_spanned! 也折进了模板):

fn serialize_externally_tagged_variant(...) -> TokenStream {
    let variant_name = variant.attrs.name().serialize_name();

    match effective_style(variant) {   // 唯一字段被 skip 的 Newtype 按 Unit 处理
        Style::Unit => {
            quote_expr! {
                _serde::Serializer::serialize_unit_variant(
                    __serializer, #type_name, #variant_index, #variant_name)
            }
        }
        Style::Newtype => {
            ...
            quote_expr! {
                _serde::Serializer::serialize_newtype_variant(
                    __serializer, #type_name, #variant_index, #variant_name, __field0)
            }
        }
        Style::Tuple => { /* 用 serialize_tuple_variant + 多次 serialize_field + end */ }
        Style::Struct => { /* 用 serialize_struct_variant + 多次 serialize_field + end */ }
    }
}

生成代码(对 E::V(42)):

match *self {
    E::V(ref __field0) =>
        _serde::Serializer::serialize_newtype_variant(
            __serializer, "E", 0u32, "V", __field0),
}

直接调 Serializer 的 serialize_newtype_variant——把 enum 名、变体索引、变体名、值传给它,格式自己决定 JSON 编码。

四种 tag 模式的生成策略差异(第 14 章详解):

  • External:调 Serializer 的 serialize_xxx_variant 方法(上面例子)。
  • Internal:手动在 map 里插一个 tag 键值对。
  • Adjacent:生成一个带 tag 和 content 两个字段的 map。
  • Untagged:直接调字段的 Serialize,完全丢弃 tag 信息。

从 serialize_internally_tagged_variant 的源码能直接看出"手动插 tag"是什么意思(serde_derive/src/ser.rs:575-639)——对 Style::Unit 变体,生成代码精简如下:

let mut __struct = _serde::Serializer::serialize_struct(__serializer, #type_name, 1)?;
_serde::ser::SerializeStruct::serialize_field(&mut __struct, #tag, #variant_name)?;
_serde::ser::SerializeStruct::end(__struct)

一个有 0 个数据字段的 variant,却调用 serialize_struct 开出一个 1 字段的结构体——因为要把 tag 塞进去。这正是"internally tagged"这个名字的字面意思:tag 不再是 map 外的独立键(那是 external),而是和 data 混在同一层 map 里。对 Newtype variant,则调 __private::ser::serialize_tagged_newtype 这个隐藏在 serde 正式 crate 里的帮手函数(serde/src/private/ser.rs:17-36,derive 侧的调用点在 serde_derive/src/ser.rs:619)——它用一个 TaggedSerializer 包住真正的 Serializer,只有当内部值走 serialize_struct / serialize_map 这类形态时才把 tag 插进去;如果内部值是 u32、str、seq 之类,internally tagged 压根没法表示、运行时报错。这句错误的模板就在 serde/src/private/ser.rs:85-90 的 bad_type,格式串一字不差是 "cannot serialize tagged newtype variant {}::{} containing {}",末尾那个 {} 由 Unsupported 枚举(serde/src/private/ser.rs:46-59)填成 an integer / a string / a sequence 之类——对 enum E { V(u32) } 就得到 cannot serialize tagged newtype variant E::V containing an integer。newtype 包的类型能否表示成 map,derive 在编译期看不出来(类型信息在宏展开阶段还不存在),所以这类错误只能 runtime 兜底。

Adjacent 模式的不同之处在 serialize_adjacently_tagged_variant(serde_derive/src/ser.rs:641-763)里——它生成的是"外层一个 struct、tag 和 content 两个同级兄弟字段"。两个细节值得钉住:

一、不是"固定 2 字段"——Unit variant 只有 1 个字段。 没有数据可放时源码直接 return 一个 serialize_struct(__serializer, #type_name, 1)、只写 tag(serde_derive/src/ser.rs:667-675);Newtype 才是 serialize_struct(..., 2)(serde_derive/src/ser.rs:685-693),Tuple / Struct variant 走末尾那段带 __AdjacentlyTagged 包装的 2 字段版本(serde_derive/src/ser.rs:752-761)。test_suite 里对 Unit variant 的期望 token 就是 Token::Struct { name: "AdjacentlyTagged", len: 1 }(test_suite/tests/test_enum_adjacently_tagged.rs:27-41)。

二、tag 字段的值不是一个字符串字面量。 源码填进去的是 &_serde::#private::ser::AdjacentlyTaggedEnumVariant { enum_name, variant_index, variant_name }(serde_derive/src/ser.rs:652-658),这个类型的 Serialize 实现转手调 serializer.serialize_unit_variant(enum_name, variant_index, variant_name)(serde/src/private/ser.rs:1361-1368)。JSON 里落地成变体名字符串,但把变体编成序号的格式仍然拿得到 variant_index——又是一次"数据模型层总是把信息提供给下游"。上面那条 test 期望的 Token::UnitVariant 而不是 Token::Str,正是这个区别的证据。

所以对 Struct variant,生成代码会嵌套一层 serialize_struct——外层 2 字段(tag + content),内层 N 字段(真实字段)。生成的总代码量是四种 tag 模式里最大的——一个 Struct variant 会展开成外层 struct + 内层 struct 两套 serialize_struct / serialize_field / end 调用,外加一个临时的 __AdjacentlyTagged 包装类型(serde_derive/src/ser.rs:732-737)。

12.11 辅助工具:SerializeWith 包装

一个值得细讲的模式——#[serde(serialize_with = "my_fn")] 如何处理。

用户代码:

#[derive(Serialize)]
struct User {
    #[serde(serialize_with = "serialize_timestamp")]
    created_at: DateTime<Utc>,
}

fn serialize_timestamp<S: Serializer>(t: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error> {
    s.serialize_i64(t.timestamp())
}

期望行为:生成的代码应该调用 serialize_timestamp(&self.created_at, serializer) 而不是 self.created_at.serialize(serializer)。

挑战:serialize_struct 的 serialize_field 方法要求第二个参数必须实现 Serialize trait——不能直接传一个函数调用结果。

serde 的解决方案:生成一个临时 struct SerializeWith,它 wrap 字段值,然后给这个 wrapper 实现 Serialize:

// !这一段不是源码原文,是宏展开结果的去噪示意。真实展开里 __private
// 带 patch 号(见 §12.14 的 __private228)。生成它的模板在
// serde_derive/src/ser.rs:1241-1262。
// 整个 wrapper 定义 + 构造都塞在一个 `&{ ... }` 块表达式里,
// 直接作为 serialize_field 的第三个参数
_serde::ser::SerializeStruct::serialize_field(
    &mut __serde_state,
    "created_at",
    &{
        #[doc(hidden)]
        struct __SerializeWith<'__a> {
            values: (&'__a DateTime<Utc>,),
            phantom: _serde::__private::PhantomData<User>,
        }

        #[automatically_derived]
        impl<'__a> _serde::Serialize for __SerializeWith<'__a> {
            fn serialize<__S>(&self, __s: __S) -> _serde::__private::Result<__S::Ok, __S::Error>
            where
                __S: _serde::Serializer,
            {
                serialize_timestamp(self.values.0, __s)
            }
        }

        __SerializeWith {
            values: (&self.created_at,),
            phantom: _serde::__private::PhantomData::<User>,
        }
    },
)?;

用临时类型 wrap + 为它实现 Serialize——这是一个非常 Rust 风格的技巧。函数指针不是 Serialize,但包装它们的临时类型可以成为 Serialize。

源码实现(serde_derive/src/ser.rs:1173 的 wrap_serialize_field_with,它把字段类型和表达式各包成单元素切片后交给 serde_derive/src/ser.rs:1207-1263 的 wrap_serialize_with)生成上面这段代码。这个模式也用于 deserialize_with(第 13 章)、with(第 16 章)。

为什么要走这么绕的一圈?——因为 Rust 的 trait system 不允许"在 generic struct 方法里直接传函数指针替代 trait 实现"。SerializeStruct::serialize_field<V: Serialize>(&mut self, key, value: &V) 的 value 形参有 Serialize 约束、函数指针 fn(&T, S) -> Result<...> 不满足这个约束。所以必须先造一个满足约束的载体类型(临时 SerializeWith struct)、再让这个载体的 Serialize 实现转发到用户函数。ser.rs 里有三个 wrap_* 函数,但它们不是三种挂载位置:#[serde(serialize_with = "...")] 在属性系统里只解析到两个地方——字段(serde_derive/src/internals/attr.rs:1302 的 Field::serialize_with)和变体(serde_derive/src/internals/attr.rs:964 的 Variant::serialize_with),容器级属性上根本没有这个字段(容器级对应的是 into;反序列化侧是 from / try_from)。所以是两个入口 + 一个共用实现:wrap_serialize_field_with(serde_derive/src/ser.rs:1173-1180,把单个字段的类型和表达式各包成单元素切片)、wrap_serialize_variant_with(serde_derive/src/ser.rs:1182-1205,把变体的全部字段收成两个切片),两者都转调底层的 wrap_serialize_with(serde_derive/src/ser.rs:1207-1263)——真正吐出 __SerializeWith 的只有最后这一个。

和 Rust 标准库的对称——std::fmt::Display 也有类似模式:write!(f, "{:>10}", value) 不能直接格式化任意函数、但可以用 format_args! 构造一个实现了 Display 的临时结构。"给中间值挂 trait 实现"是 Rust 生态的通用 workaround——Serde 把它用到极致、serde_derive 一共 6 个 wrap_*_with 函数,Serialize 侧三个都在 serde_derive/src/ser.rs:1173-1263,Deserialize 侧三个分散在 serde_derive/src/de.rs:664-718(wrap_deserialize_with / wrap_deserialize_field_with)和 serde_derive/src/de/enum_externally.rs:163(wrap_deserialize_variant_with),合计约 160 行。

一个常被问到的细节——__SerializeWith 持有 phantom: PhantomData<User> 的用意是什么?为了让临时 wrapper 带上用户类型的 generic 参数——如果用户的 struct 是 User<T>、那 wrapper 也得是 __SerializeWith<'__a, T>(源码里 wrapper 的 generics 就是 params.generics 再加一个 '__a)、PhantomData 让 generic 参数"出现"在 wrapper 的定义里却不占用运行时空间(PhantomData<T> 是零字节 ZST)。这是 Rust 标准的"把 generic 参数持有住但不实际存储"套路——在 Serde 生成代码里用得非常频繁(__SerializeWith、__AdjacentlyTagged、__EnumFlatten 三个临时 wrapper 全带 phantom 字段,分别见 serde_derive/src/ser.rs:1245、serde_derive/src/ser.rs:736、serde_derive/src/ser.rs:1001)。

反过来,用户自己写的 PhantomData<T> 字段是 bound 推断的硬编码例外——bound::with_bound 遍历字段类型收集"需要 T: Serialize"的类型参数时,看到路径末段是 PhantomData 就直接 return、不再往里看(serde_derive/src/bound.rs:131-138,注释开头一行原文是 "Hardcoded exception, because PhantomData implements",下一行接 "Serialize and Deserialize whether or not T implements it.")。所以 struct S<T> { marker: PhantomData<T> } derive 出来的 impl 不会多一条 where T: _serde::Serialize。

12.12 transparent 模式

#[serde(transparent)] 应用在单字段 struct 上:

#[derive(Serialize)]
#[serde(transparent)]
pub struct UserId(u64);

生成代码(模板在 serde_derive/src/ser.rs:191-212 的 serialize_transparent;下面这块是展开结果、不是源码原文):

// 不是源码原文:这是展开结果。模板里的 #path 默认取
// _serde::Serialize::serialize(ser.rs:201-207),&#self_var.#member 展开成 &self.0
_serde::Serialize::serialize(&self.0, __serializer)

就是这么简单——完全忽略 struct 的存在,直接序列化字段。

为什么要这个属性?在第 2 章讲过,newtype 原语在大多数格式里是透明的——serialize_newtype_struct 是 Serializer 的必需方法(serde_core/src/ser/mod.rs:916-922),trait 文档明写"鼓励把 newtype struct 当成无关紧要的包装"、建议直接转发 value.serialize(self)(serde_core/src/ser/mod.rs:898-900),serde_json 就这么做——方法体只有 value.serialize(self) 一行(serde_json 的 src/ser.rs:225-230)。但某些格式可能不这么实现(比如给 newtype 加一个类型标签)。#[serde(transparent)] 绕过所有格式的 newtype 处理,强制透明——在你想要的就是"这个 struct 不存在"的时候用。

12.13 remote 模式:外部类型的 Serialize

这是 ser.rs 的一个独特分支。用户代码:

use serde::Serialize;
use std::time::Duration;

#[derive(Serialize)]
#[serde(remote = "Duration")]
struct DurationDef {
    #[serde(getter = "Duration::as_secs")]
    secs: u64,
    #[serde(getter = "Duration::subsec_nanos")]
    nanos: u32,
}

用户定义了一个和 std::time::Duration 结构相同的 struct,告诉 serde "请为 std::time::Duration 生成 Serialize 实现——但用我这个 DurationDef 作为描述"。Duration 的 secs / nanos 字段是私有的,直接按字段名读会报 E0616,所以这里用 getter 改走公开方法(第 16 章细讲);另外 serde 本身已经为 Duration 实现了 Serialize(serde_core/src/ser/impls.rs:685),这里只是借它演示 remote 的机制。

为什么要这样? 因为用户不能给不属于自己 crate 的类型实现不属于自己 crate 的 trait(Rust 的 orphan rule)。remote 模式是一个 workaround——让用户通过代理 struct 描述外部类型,serde 在代理类型上生成关联函数而不是 trait 实现。

生成代码(摘自第 12.2 节):

// 不是源码原文,是展开示意。方法的可见性来自 `#vis`(ser.rs:30、36),
// 即代理 struct 自己的可见性——上面的 DurationDef 没写 pub,展开出来
// 就是私有的 `fn serialize`;写成 `pub struct DurationDef` 才会是 `pub fn`。
impl DurationDef {
    fn serialize<__S>(__self: &Duration, __serializer: __S) -> Result<...>
    where __S: Serializer,
    {
        // 和普通 Serialize 同样的逻辑,但用 __self 代替 self
        ...
    }
}

用户如何使用?通过 #[serde(with = "DurationDef")]:

#[derive(Serialize)]
struct MyStruct {
    #[serde(with = "DurationDef")]
    duration: Duration,
}

(这两段在 serde 1.0.228 + rustc 1.96.0 下 cargo check 通过。)

第 16 章会详细讨论 remote 和 with 的配合使用——这是 serde 最巧妙的 workaround 之一。

remote 模式在 ser.rs 里的具体分叉点在 §12.2 已经展示——expand_derive_serialize 最后的 if let Some(remote) = cont.attrs.remote() 这个分支。两种外壳的唯一区别是——

  • 普通模式:impl Serialize for #ident { fn serialize(&self, ...) { #body } }——方法签名按 trait 固定、self 是 &#ident
  • remote 模式:impl #ident { #vis fn serialize<__S>(__self: &#remote #ty_generics, ...) { #body } }(serde_derive/src/ser.rs:35-43)——关联函数、第一参数叫 __self、类型是远端类型而非自身;可见性 #vis 直接抄代理 struct 的(serde_derive/src/ser.rs:30),不是固定的 pub

#body 共用同一份——这是最精彩的设计:serialize_body 生成的代码里对 self 的引用走 params.self_var、而 self_var 在 Parameters::new 里根据 is_remote 选择 "self" 或 "__self"。代码生成基本不知道自己在生成普通还是 remote 版本——除了 §12.5 讲的 get_member 按 is_remote 多套一层 constrain,其余部分只靠换一个标识符就复用了整个生成管线。这种"正交切换"的架构——通过在参数对象里藏一个 bool、让下层代码几乎对两种模式无感知——是 Serde 源码里反复出现的范式。

orphan rule 的历史补丁——serde 0.x 时代没有 remote 模式、用户必须 newtype 包一层或者等第三方 crate 出代理实现。#[serde(remote)] 由 commit a6d17211("Derive for remote types",2017-04-08)引入、随 serde 1.0.0 首发(git tag --contains a6d17211 的最早 tag 就是 v1.0.0)。有了它、第三方 crate 里没有实现 Serde 的类型也能通过用户 crate 里的 derive 支持序列化(std 的 Duration / SystemTime / IpAddr 等则由 serde 自己内置实现,见 serde_core/src/ser/impls.rs)。第 16 章会继续讲 remote + with 的配合,以及私有字段要靠 getter 读取的细节。

12.13.1 dummy::wrap_in_const:生成代码外层的 31 行"卫生间"

每一个 #[derive(Serialize)] 产出的代码都要经过最后一步 dummy::wrap_in_const。这个文件只有 31 行(serde_derive/src/dummy.rs),但它决定了生成代码对用户 crate 的影响边界:

// serde_derive/src/dummy.rs:4-31(原文,整个文件除 use 之外就这些)
pub fn wrap_in_const(serde_path: Option<&syn::Path>, code: TokenStream) -> TokenStream {
    let use_serde = match serde_path {
        Some(path) => quote! {
            use #path as _serde;
        },
        None => quote! {
            #[allow(unused_extern_crates, clippy::useless_attribute)]
            extern crate serde as _serde;
        },
    };

    quote! {
        #[doc(hidden)]
        #[allow(
            non_upper_case_globals,
            unused_attributes,
            unused_qualifications,
            clippy::absolute_paths,
        )]
        const _: () = {
            #use_serde

            _serde::__require_serde_not_serde_core!();

            #code
        };
    }
}

四层设计各司其职:

1. const _: () = { ... }; 匿名常量是宏卫生的关键容器。把整个生成代码塞进一个匿名 const 表达式:

  • _serde 别名只在 const 内部可见——不会污染用户模块的命名空间。用户如果自己写了 mod _serde { ... } 不会和生成代码冲突。
  • 内部声明的 impl 项仍然通过 trait system 注册到用户类型上——trait impl 有"全局锚定"特性,不受 const 表达式作用域限制。这是 Rust "impl 和表达式作用域解耦" 的巧妙使用。
  • const _(下划线名)让同一文件可以有多个 wrap_in_const 产物而不冲突——#[derive(Serialize)] struct A; 和 struct B; 会产生两个 const _: () = { ... };,匿名 const 不需要唯一命名。

2. Serde 路径的两条分叉(line 5-13):

// 用户配置了 #[serde(crate = "my_serde")]
use my_serde as _serde;

// 默认路径
#[allow(unused_extern_crates, clippy::useless_attribute)]
extern crate serde as _serde;

Rust 2018+ 不需要 extern crate 就能用 crate 依赖、但 #[derive(Serialize)] 不知道用户 crate 运行在哪个 edition。统一生成 extern crate serde 在所有 edition 都工作——#[allow(unused_extern_crates)] 抑制 2018+ 的 "无用 extern" 警告。clippy::useless_attribute 进一步抑制 "这个 allow 本身没用" 的 clippy lint——一层 allow 套一层 allow,针对的是不同编译器/clippy 版本的不同 lint。这种针对工具演化的层层防御在生成代码里必要、在手写代码里会被视为过度。

3. _serde::__require_serde_not_serde_core!(); 安全闸门(line 26):

这是一个隐藏的宏,serde 和 serde_core(拆分 commit f3869307、随 1.0.220 首发)各定义了一份:serde_core/src/lib.rs:115-121 的版本展开成 ::core::compile_error!,消息是 "Serde derive requires a dependency on the serde crate, not serde_core";而 serde/src/lib.rs:283-285 的版本宏体就是 () => {};、展开为空。如果用户用 #[serde(crate = "serde_core")] 之类的方式让生成代码指向 serde_core,就会在这里报错。原因:serde_core 不包含 derive 依赖的全部 __private glue code(例如 serialize_tagged_newtype 所在的 serde/src/private/ser.rs 只在 serde 里)。而且即使某个类型的生成代码碰巧只用到公开 trait,这里也照样拦下——commit 8909fc0c 的说明是给以后在生成代码里引入新的私有 helper 留余地。闸门在最外层强制检查"运行环境是完整 serde、不是 serde_core"。

4. 四个 #[allow] 抑制不同的 lint 噪声:

  • non_upper_case_globals——历史遗留:这组 allow 在 2016 年(commit 305fab7c)随 dummy const 一起加入,那时的常量名是 _IMPL_SERIALIZE_FOR_Foo 这种夹带类型名的写法,会触发该 lint;2020 年(commit a50e1c20)改成 const _ 后这条 allow 保留了下来
  • unused_attributes——用户结构体上可能有 serde 不消费的其他属性(如 #[cfg(...)] 在 derive 展开时的处理)
  • unused_qualifications——生成代码大量使用 _serde::Serializer 这种全限定名、即使 _serde::Serializer 已在作用域内也用全名(确保 hygiene),这触发 unused_qualifications lint
  • clippy::absolute_paths——clippy 的"偏好相对路径"规则、同理被禁掉

#[doc(hidden)] 是锦上添花——这个 const 在 rustdoc 里不会出现,用户文档干净。

合起来看,wrap_in_const 是一个"卫生间":所有 serde_derive 对用户代码的影响都被圈在这 31 行定义的 const 作用域里、外部只看到类型上多了一个 impl Serialize 这个"结果"。这种严格的隔离让 serde_derive 能和任意用户代码共存——用户用什么奇葩的 crate name、什么 lint 等级、什么 Rust edition,都不会让 derive 生成的代码出问题。呼应第 10 章 Ctxt 的 Drop 契约、第 8 章 quote! 的实现细节——都是 Serde 生态"每个细节都认真"的工程神经的一次体现。

12.14 实战:追踪一个真实例子的完整生成

把本章内容整合,追踪一个真实类型的完整 Serialize 生成:

用户代码:

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiResponse<T: Serialize> {
    pub status: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<T>,

    pub timestamp: u64,
}

processes(12 步,从 lib.rs 开始):

  1. parse_macro_input! → DeriveInput(serde_derive/src/lib.rs:115)
  2. Container::from_ast(serde_derive/src/internals/ast.rs:62)→ Container:
    • attrs: rename_all=CamelCase
    • data: Struct(Style::Struct, 3 个 Field)
    • 字段的 rename 按 rename_all 转换:status→"status"(无变化)、data→"data"(无变化)、timestamp→"timestamp"(无变化)
    • 字段 data 的属性 skip_serializing_if = Path("Option::is_none")
  3. Parameters::new(serde_derive/src/ser.rs:103):处理 generics,由 build_generics(serde_derive/src/ser.rs:135)往 where 子句追加 T: _serde::Serialize
  4. serialize_body(serde_derive/src/ser.rs:171)→ 分派到 serialize_struct(serde_derive/src/ser.rs:296)
  5. serialize_struct 没有 flatten,走 serialize_struct_as_struct(serde_derive/src/ser.rs:328)
  6. 计算字段数:fold 起点是 #tag_field_exists as usize,逐字段折出 false as usize + 1 + (if Option::is_none(&self.data) { 0 } else { 1 }) + 1(serde_derive/src/ser.rs:348-359)
  7. serialize_struct_visitor(serde_derive/src/ser.rs:1105)生成 3 个 serialize_field 语句
  8. 第二个字段带 if 判断,else 分支调 StructTrait::skip_field 给出的 SerializeStruct::skip_field(serde_derive/src/ser.rs:1152 与 serde_derive/src/ser.rs:1336-1341)
  9. 组装成 serialize_struct + 3 个 serialize_field + end
  10. 外层 impl 模板:impl<T: Serialize> _serde::Serialize for ApiResponse<T> where T: _serde::Serialize(serde_derive/src/ser.rs:46-57)
  11. 用 dummy::wrap_in_const 包装(serde_derive/src/ser.rs:60,实现在 serde_derive/src/dummy.rs:4-31)
  12. 返回 TokenStream

最终生成代码(serde 1.0.228 + serde_derive 1.0.228 在 rustc 1.96.0 上用 RUSTC_BOOTSTRAP=1 -Zunpretty=expanded 实测,按 rustfmt 风格排版;唯一的改动是把 __require_serde_not_serde_core!() 还原成了宏调用——展开输出里它已经展开为空、只剩一个孤立的 ;):

#[doc(hidden)]
#[allow(
    non_upper_case_globals,
    unused_attributes,
    unused_qualifications,
    clippy::absolute_paths,
)]
const _: () = {
    #[allow(unused_extern_crates, clippy::useless_attribute)]
    extern crate serde as _serde;
    _serde::__require_serde_not_serde_core!();
    #[automatically_derived]
    impl<T: Serialize> _serde::Serialize for ApiResponse<T>
    where
        T: _serde::Serialize,
    {
        fn serialize<__S>(
            &self,
            __serializer: __S,
        ) -> _serde::__private228::Result<__S::Ok, __S::Error>
        where
            __S: _serde::Serializer,
        {
            let mut __serde_state = _serde::Serializer::serialize_struct(
                __serializer,
                "ApiResponse",
                false as usize + 1 + if Option::is_none(&self.data) { 0 } else { 1 } + 1,
            )?;
            _serde::ser::SerializeStruct::serialize_field(
                &mut __serde_state,
                "status",
                &self.status,
            )?;
            if !Option::is_none(&self.data) {
                _serde::ser::SerializeStruct::serialize_field(
                    &mut __serde_state,
                    "data",
                    &self.data,
                )?;
            } else {
                _serde::ser::SerializeStruct::skip_field(&mut __serde_state, "data")?;
            }
            _serde::ser::SerializeStruct::serialize_field(
                &mut __serde_state,
                "timestamp",
                &self.timestamp,
            )?;
            _serde::ser::SerializeStruct::end(__serde_state)
        }
    }
};

几处生产级细节值得注意(常被教程忽略,但真实输出就长这样):

  • _serde::__private228:不是 __private,而是带版本 patch 号(228 对应 serde 1.0.228)。这个名字是 serde_derive 里一个叫 private 的零大小类型在 ToTokens 时现拼出来的:Ident::new(concat!("__private", env!("CARGO_PKG_VERSION_PATCH")), Span::call_site())(serde_derive/src/lib.rs:95-111)。第 10.3 节讨论过这个机制——它强制 serde 和 serde_derive 的 patch 版本对齐,不对齐就在这里编译失败。
  • false as usize + 1 + ... + 1:字段数从 false as usize(即 0)加起来。看似奇怪的写法,是为了让生成逻辑统一——serialize_struct_as_struct 的 fold 起点是 #tag_field_exists as usize,struct 自身带 #[serde(tag = "...")] 时这里会变成 true(多加 1 给 tag 字段)。同一套模板处理两种情况。(internally tagged enum 的 struct variant 走的是另一个函数 serialize_struct_variant,serde_derive/src/ser.rs:884——它的 fold 起点是光秃秃的 quote!(0) 而不是 #tag_field_exists as usize(serde_derive/src/ser.rs:911-919),那个 + 1 是在 StructVariant::InternallyTagged 这一条分支上单独拼的 #len + 1,见 serde_derive/src/ser.rs:943,整条分支在 serde_derive/src/ser.rs:938-950;externally tagged 和 untagged 两条分支用的都是不加 1 的 #len。)
  • clippy::absolute_paths:serde 1.0.219(2025 年 3 月,commit 168b6cf7)加入的 allow,让全路径引用不触发 lint 警告。这一整段 #[doc(hidden)] #[allow(...)] const _: () = { .. } 外壳就写死在 serde_derive/src/dummy.rs:16-31。
  • _serde::__require_serde_not_serde_core!();:1.0.228 的 wrap_in_const 在 extern crate serde as _serde; 之后插了这么一句(serde_derive/src/dummy.rs:26)。它在 serde 里是个空展开的 macro_rules!(serde/src/lib.rs:283-285),serde_core 里则是另一份会报错的定义(serde_core/src/lib.rs:115)——作用是让 "把 _serde 指向 serde_core 而不是 serde" 这种误用在展开点就报错。cargo expand / -Zunpretty=expanded 的输出里看不到这个宏名:正常依赖 serde 时它展开为空,只在 extern crate 下面留一个孤立的 ;。这一句由 commit 8909fc0c 加入,最早随 1.0.220 发布,更早的版本连这个 ; 也没有。

短短 3 个字段的用户代码,生成了 40+ 行代码。每一行都精准对应本章讲过的某一处逻辑——属性解析决定字段名、skip_serializing_if 决定 if 判断、状态机模式决定 serialize_struct/serialize_field/end 结构。版本:本展开输出由 serde 1.0.228 + serde_derive 1.0.228 + rustc 1.96.0 产生。

从这个 40 行代码里能反向验证本章讲的设计——

1. 无 panic! 无 unwrap()——生成代码里没有任何 runtime 崩溃点。所有错误都通过 ? 向上传播到 fn serialize 的 Result 返回值。即使用户的类型实现 Serialize 时故意 panic、也是用户的 trait impl 里发生、不是 derive 生成的 glue code 里发生。derive 生成的代码是透明胶水、不引入新的失败模式——这个纪律在 serde_derive/src/ser.rs 的 1369 行里一次未破。

2. 字段数的 false as usize + 1 + if ... + 1——fold 起点 false as usize 在这个例子里被折叠成 0 + 1 + if ... + 1、rustc 常量传播后会优化成 2 + if ... { 0 } else { 1 }。但源码生成不做优化——把 fold 的原始骨架完整交给 rustc、让编译器做后续化简。常量折叠本来就是编译器的活,生成器不必代劳。

3. 外层的 const _: () = { ... }; 包装——用户代码里完全看不到 _serde、_serde::ser::SerializeStruct、__serde_state 这些符号。整个 derive 的影响被严格圈在一个匿名 const 表达式内——§12.13.1 详解过的"卫生间"设计在这里具体兑现。

4. T: _serde::Serialize 追加 bound——用户原本只写了 T: Serialize(裸 Serialize,不带 _serde::)、但生成代码里额外追加了 where T: _serde::Serialize——这是 bound.rs 干的活(用户可能在 crate 里根本没 use serde::Serialize、derive 生成的代码只信得过 _serde::Serialize、所以 bound 条款必须用全限定名)。这是"生成代码不能依赖用户 scope"原则的又一次体现——整个 derive 输出对用户源文件的 use 列表、作用域别名完全免疫。

12.15 serde_derive crate 的全貌:9 个顶层文件 + internals 子模块

读到这里你已经掌握了 ser.rs(1369 行、占全 crate 8969 行的 15%)的分派骨架。把视角抬高一层、看整个 serde_derive/src/。wc -l 给出如下账单——顶层 9 个 .rs 文件 3278 行 + internals/ 子目录 10 个文件 3308 行 + de/ 子目录 9 个文件 2383 行(第 13、14 章的主角,这里不展开),合计 8969 行、每个文件的职责泾渭分明:

文件 行 角色
ser.rs 1369 本章主角——Serialize 的 21 个 serialize_* 分派函数
de.rs 976 下一章主角——Deserialize 代码生成的入口;比 ser.rs 短是因为 struct / tuple / enum 各 tag 模式的具体生成都拆到了 de/ 子目录的 9 个文件里(合计 2383 行)
bound.rs 425 trait bound 推断——7 个 pub fn(without_defaults、with_where_predicates、with_where_predicates_from_fields、with_where_predicates_from_variants、with_bound、with_self_bound、with_lifetime_bound),从字段类型反推 where T: Serialize 子句、处理 #[serde(bound = "...")] 覆盖
pretend.rs 188 假装使用字段以抑制 dead_code 警告——remote derive 的代理 struct 字段从不被真正读取、enum 变体从不被构造,这里生成一段永不执行的代码"假装"用到它们
lib.rs 127 #[proc_macro_derive] 入口、2 个 derive(derive_serialize / derive_deserialize)
fragment.rs 74 Fragment 类型 + quote_expr! / quote_block! 两个包装宏(见本章 §12.3)
deprecated.rs 56 用户类型或其变体带 #[deprecated] 时,给生成的 impl 加 #[allow(deprecated)],免得 derive 自己触发弃用警告
this.rs 32 this::this_type() / this::this_value()——两个小工具生成 impl 目标类型的路径(本地类型是自身 Ident,remote 是远端路径;this_value 把泛型参数写成表达式位置可用的 ::<T> 形式),即 Parameters 里的 this_type / this_value
dummy.rs 31 wrap_in_const——§12.13.1 专门分析的"卫生间"

三点读完全表才看得出的设计——

  1. bound.rs 居然是 pretend.rs 的 2 倍多——说明 trait bound 推断远比想象复杂——字段里可能有 Option<Vec<HashMap<K, V>>>、每层都要展开判断、还要处理用户写 #[serde(bound(serialize = "..."))] 全覆盖 vs 部分覆盖——这是用户最常踩坑的地方
  2. ser.rs + de.rs + de/ = 4728 行、占 53%——serde_derive 本质上是两个代码生成器塞在一个 crate 里、公用 bound / fragment / this / dummy / pretend 五个小工具——要学过程宏的读者、直接切成两块读最划算
  3. 同样一个过程宏 crate、lib.rs 只有 127 行——说明 Serde 的设计纪律性极强——入口文件不塞业务逻辑、只负责 TokenStream → AST → dispatch → TokenStream 的壳子——这是很多"过程宏越写越乱"项目值得学的范式

再往下钻一层、internals/ 子模块是 Serialize 和 Deserialize 共享的基础设施,按职责切成 10 个文件——把它一次算清楚,后面 ser.rs / de.rs 里所有"调属性"、"取名字"、"走一致性检查"的引用你都能秒懂:

internals/ 文件 行 角色
attr.rs 1818 属性解析总仓——第 11 章主角;Container::from_ast / Variant::from_ast / Field::from_ast 三个大状态机把 #[serde(...)] 解析成结构化字段。55% 的 internals 代码在这
check.rs 477 一致性检查——需要同时看多个 attrs 对象的交叉冲突(flatten 不能打在 tuple / newtype struct 的字段上、adjacently tagged 的 tag 和 content 不能同名、from 与 try_from 互斥、transparent 最多一个非 skip 字段……),失败用 cx.error_spanned_by 记到错误账本
receiver.rs 293 replace_receiver——递归 visit_mut 整个 AST、把所有 Self 替换成具体类型、处理边缘情况如泛型参数、where 子句或字段类型里出现的 Self / Self::Assoc。ser.rs:expand_derive_serialize 第一步调用
ast.rs 225 AST 中间表示——Container / Data{Enum,Struct} / Variant / Field / Style{Struct,Tuple,Newtype,Unit};本章 §12.3 的 5 路分派就是对 Data / Style 的穷举(Enum 一臂 + Struct 的四种 Style)
case.rs 200 rename_all 规则——CamelCase / snake_case / SCREAMING_SNAKE_CASE / kebab-case 等 8 种转换的字符串处理(RENAME_RULES 表 8 条,见 serde_derive/src/internals/case.rs:33-42;RenameRule 枚举是 9 个变体,多出来的 None 是"不改名")
name.rs 113 MultiName { serialize: Name, deserialize: Name, deserialize_aliases, .. }——field/variant/container 的两个独立名字槽(Name 是带 span 的字符串),让 #[serde(rename(serialize = "foo", deserialize = "bar"))] 双向分叉
symbol.rs 71 字符串常量表——SERDE = Symbol("serde")、RENAME = Symbol("rename")……集中拼写避免 typo(用 == 对 syn::Path 做匹配用的)
ctxt.rs 67 Ctxt 错误账本 + Drop 卫兵——第 10 章专门讲过、check() 必须消费错误否则 Drop 里 panic。expand_derive_serialize 第 4 步的 ctxt.check()? 就是消费
respan.rs 16 把一段 TokenStream 的 span 整体重设的小工具
mod.rs 28 模块门面、pub use 列表

attr.rs 1818 行一个文件占 internals 55% 的体量——读到这个数字就该明白:第 11 章之所以敢单独拎一章讲属性系统、并不是"水章数",而是因为属性系统本身就是 serde_derive 工程量最重的一块。ser.rs 每调一次 cattrs.name().serialize_name() 或 field.attrs.skip_serializing_if()——背后都是 attr.rs 里几十行状态机的凝结。把代码生成和属性解析切开讲、是读懂源码的前提。

12.15.1 Struct 四形态展开前后对比表

把本章 §12.4、§12.5、§12.6、§12.8 讲的四种 Style 的生成结果摆在一起——字段类型相同、字段数尽量相同(Tuple 至少要 2 个字段,只有 1 个无名字段就归入 Newtype)、只因 Style 不同生成的代码就分四种骨架:

用户写的 Style Serializer 调用序列 字段位置 生成代码行数(示例)
struct Unit; Style::Unit serialize_unit_struct("Unit") —— 1
struct New(T); Style::Newtype serialize_newtype_struct("New", &self.0) 直传 1
struct Tup(T, T); Style::Tuple serialize_tuple_struct(...) → serialize_field(&self.0) → serialize_field(&self.1) → end &self.0、&self.1(按 index) 4
struct S { f: T } Style::Struct serialize_struct(...) → serialize_field("f", &self.f) → end &self.f(按名) 3

几个容易混的点、下面逐一现场讲清楚、避免日后踩坑:

1. Newtype 和 Tuple 的边界在哪? serde_derive/src/internals/ast.rs 的 Style 枚举把只有一个无名字段的 tuple struct 单独列为 Newtype——不是 Tuple(fields.len()==1)。这是因为 Serializer 协议为它专门设计了 serialize_newtype_struct 方法,trait 文档建议实现直接转发 value.serialize(self)(serde_json 正是这样),让 newtype 在大多数格式里不引入额外层级。Tuple 则走 serialize_tuple_struct——多数格式会产出数组 [v]、[v1, v2]。这个区分是用户经常踩坑的地方:struct Millis(u64) 在 JSON 里就是 123,但 struct Point(u64, u64) 在 JSON 里是 [1, 2]——只差一个逗号,语义完全不同。

2. Unit struct 有字段数吗? 没有——serialize_unit_struct(serializer, "Empty") 的签名里就没有 len。但 Serializer 的实现仍然可以输出非空内容:JSON 输出 null、二进制格式通常什么都不写。"Unit 没字段"是语义、不是"输出一定是空"。

3. Tuple/Struct 的 len 参数——serialize_struct(serializer, name, len) 的 len 是编译期常量(无 skip_serializing_if)或带条件的 if 表达式(有 skip_serializing_if)。Serializer 可以信任这个 len(比如预分配数组容量)、也可以忽略(比如 JSON 不需要预分配)。本章 §12.6 的 serialize_struct_as_struct 代码里的 .fold(quote!(#tag_field_exists as usize), ...) 就是在做这个编译期 + 运行时混合计算——fold 起点是 false as usize(即 0),如果 struct 自身带 #[serde(tag = "...")](internally tagged struct),这里会变成 true as usize(多加 1 给 tag 字段)。同一份 fold 代码、通过 tag_field_exists 这个 bool 统一两种场景——是 ser.rs 精简的典型手法。(enum 的 internally tagged struct variant 在 serialize_struct_variant 里另起一个 fold,起点 0、末尾 + 1。)

12.15.2 本章 ↔ ch10(derive 架构)↔ ch13(deserialize-codegen)三章对照

按代码路径把三章接起来——读完本章、下一章就没有"新的宏观结构要学"、只有对称的 Deserialize 的细节。

ch10(derive 架构)给出的入口——第 10 章讲 serde_derive/src/lib.rs(127 行)的两个 #[proc_macro_derive] 函数:derive_serialize、derive_deserialize。每个函数都走同一个三段式骨架——

proc_macro TokenStream
  → syn::parse_macro_input!        ← 第 7 章讲的 syn
  → expand_derive_serialize(&mut) ← 本章讲的入口
      (最后一步在内部调 dummy::wrap_in_const,本章 §12.13.1 讲的卫生间)
  → TokenStream

本章讲的是骨架第二步里的 expand_derive_serialize——再展开成 Container::from_ast → Parameters::new → serialize_body → 套 impl 模板 → dummy::wrap_in_const 的五步(serde_derive/src/ser.rs:13-64)。第 11 章给 Container::from_ast 内部的属性解析分配了 1818 行(serde_derive/src/internals/attr.rs);本章给 serialize_body 及其 21 个分派函数分配了 1369 行(ser.rs)。两章合起来是 serde_derive 的 Serialize 侧全景。

ch13(deserialize-codegen)的镜像关系——下一章讲的是 expand_derive_deserialize → deserialize_body → 分派到 de/ 子目录的 struct_::deserialize / tuple::deserialize / unit::deserialize / enum_::deserialize(grep -cE '^(pub )?fn ' serde_derive/src/de.rs 实测 25 个顶层 fn,其中名字以 deserialize_ 开头的只有 8 个——de.rs 大半篇幅其实花在 build_generics / borrowed_lifetimes / expr_is_missing 这类准备工作上,真正吐 token 的分派函数在 de/ 子目录里;这和 ser.rs 里 21 个 fn serialize_* 挤在一个文件的形态正好相反)——结构完全对称、但内部模型换成了 Visitor:

本章(Serialize) 下一章(Deserialize) 对称性说明
serialize_body 5 路分派(Unit/Newtype/Tuple/Struct/Enum) deserialize_body 4 路分派(同样按 match &cont.data 的臂数计;Tuple 与 Newtype 合成一臂交给 tuple::deserialize,serde_derive/src/de.rs:319-321) Style 穷举共享
serialize_struct_visitor 生成 SerializeStruct::serialize_field 链 deserialize_seq / deserialize_map 生成 Visitor::visit_seq/visit_map 输出线性 vs 输入驱动
serialize_variant 4 tag × 4 style = 16 分支 de/enum_externally.rs 等 4 个文件各自处理 4 种 style + "variant identifier" 枚举 反序列化还要识别 tag
__SerializeWith<'__a> 临时 wrapper(§12.11) __DeserializeWith 临时 wrapper(实现 Deserialize,带 PhantomData,serde_derive/src/de.rs:664-708) 核心手法都是"给中间类型挂 trait 实现"
编译期 + 运行时混合 len 编译期字段索引枚举 + 运行时分派 visitor 两侧都是 Rust 常量化极致

可以先把本章读透、再翻 ch13——你会发现 ch13 要讲的 3359 行源码(de.rs 976 行 + de/ 2383 行)大半能在心里补出结构、只需关注 Visitor 特有的"未知字段如何处理"、"借用还是拥有"、"Deserializer 不信任输入"三个新维度。这是 Serde 源码的对称美——一旦抓住 Serialize 的骨架、Deserialize 就是对镜像的走查。

12.15.3 一张图记住 ser.rs 的 21 个分派函数

纸面上 21 个函数名看着杂、按依赖层级画出来只有 4 层——

layer 1 入口
    serialize_body  ←  唯一分派入口(由 expand_derive_serialize 调用)
      ├→ serialize_transparent    # cattrs.transparent() = true
      ├→ serialize_into           # cattrs.type_into() = Some(_)
      └→ match Data
            ├→ serialize_unit_struct
            ├→ serialize_newtype_struct
            ├→ serialize_tuple_struct    ──┐
            ├→ serialize_struct          ──┼─→ layer 2
            └→ serialize_enum            ──┘

layer 2 struct/enum 主干
    serialize_tuple_struct  ──→ serialize_tuple_struct_visitor
    serialize_struct        ──→ { serialize_struct_as_struct  ──→ serialize_struct_visitor
                               | serialize_struct_as_map     ──→ serialize_struct_visitor (flatten) }
    serialize_struct_tag_field                ← internally-tagged struct 用
    serialize_enum          ──→ serialize_variant

layer 3 variant 分派(tag × style 交叉)
    serialize_variant ──match cattrs.tag()──┐
                                              ├→ serialize_externally_tagged_variant
                                              ├→ serialize_internally_tagged_variant
                                              ├→ serialize_adjacently_tagged_variant
                                              └→ serialize_untagged_variant
        这四个函数内部再 match variant.style,重用:
          ├→ serialize_tuple_variant     # Style::Tuple
          ├→ serialize_struct_variant    # Style::Struct,不含 flatten
          └→ serialize_struct_variant_with_flatten  # Style::Struct,含 flatten

layer 4 叶子工具(重用最多)
    serialize_tuple_struct_visitor    ← 被 tuple_struct + tuple_variant 共用
    serialize_struct_visitor          ← 被 struct + struct_variant + flatten map 共用
    wrap_serialize_field_with         ← serialize_with 属性生成临时 struct
    wrap_serialize_variant_with       ← variant 级 serialize_with
    wrap_serialize_with               ← 通用底层包装

看清这 4 层的意义——所有看似 16 种 enum 组合的生成代码、最终都落在最下层 2-3 个复用函数里。serialize_struct_visitor 被 3 个高层函数(普通 struct、struct variant、flatten map)共用——所以本章 §12.7 讲的"过滤 skip + 生成 if 分支 + 调 serialize_field / skip_field"这套模式掌握一次,普通 struct、struct variant、flatten map 三类场景的字段生成代码就都认得出来。

12.16 本章小结

Serialize 代码生成的核心是"按 Container::data 的结构分派,每种分支对应一种 Serializer 协议调用模式":

Container::data 分派到 Serializer 调用
Struct(Struct, fields) serialize_struct_as_struct serialize_struct → N× serialize_field → end
Struct(Tuple, fields) serialize_tuple_struct serialize_tuple_struct → N× serialize_field → end
Struct(Newtype, fields) serialize_newtype_struct serialize_newtype_struct(field)
Struct(Unit, _) serialize_unit_struct serialize_unit_struct
Enum(variants) serialize_enum → match + serialize_variant 4 tag × 4 style = 16 分支
+transparent serialize_transparent 字段.serialize(s)
+into serialize_into clone().into().serialize(s)

属性系统在这里全部落地——rename 影响字段名字符串、skip_serializing_if 添加 if 分支、serialize_with 触发 SerializeWith 包装、flatten 改走 map 模式、transparent 完全透明转发……

Span 管理 通过 quote_spanned! 和 field.original.span() 让错误信息能指到具体字段——这是 Serde 用户体验的基石。

把本章的知识打个包——读到这里、你面对一个真实的 serde_derive 输出应该能做到:

  1. 立即定位:看到 serialize_struct(__serializer, "X", len)?; 形态、知道是 Style::Struct 走 serialize_struct_as_struct(§12.6);看到 serialize_newtype_variant 知道是 externally tagged enum 的 Newtype variant(§12.10)。
  2. 反推属性:看到 if Option::is_none(&self.data) { 0 } else { 1 } 知道字段有 #[serde(skip_serializing_if = "Option::is_none")];看到生成的临时 __SerializeWith<'__a> 知道字段有 #[serde(serialize_with = "...")](§12.11)。
  3. 诊断错误:derive 报 E0277 "the trait bound …: serde::Serialize is not satisfied" 时,主标注在 derive 行,字段上那条 "required by a bound introduced by this call" 次标注告诉你是哪个字段——这是 quote_spanned! 在起作用(§12.5);报 "cannot serialize tagged newtype variant E::V containing ..."——说明 __private::ser::serialize_tagged_newtype 的 runtime 检查触发了(§12.10)。
  4. 预测生成量:用户 1 行 #[derive(Serialize)] enum E { ... } 生成的行数大致随"变体数 × 每个变体的字段数"线性增长,adjacent 模式还要再乘上一层嵌套。几百行生成代码对应几十行用户代码是 Serde 的常态、不是"代码膨胀"——是精准编译期展开的必然产物。

动手实验

  1. cargo expand 观察:写一个带 3 种字段(普通、skip_serializing_if、serialize_with)的 struct,用 cargo expand 看完整生成代码。对照本章 12.14 节的例子。
  2. 追踪 enum 的 externally tagged:写一个有 4 种 style 变体(Unit、Newtype、Tuple、Struct)的 enum,用 cargo expand 观察 4 个 match arm 的差异。
  3. 触发 remote 模式:照 §12.13 的 DurationDef 写一个 remote 代理 struct(外部类型字段私有时配 #[serde(getter = "...")]),用 cargo expand 看生成的代码如何变成代理类型上的关联函数。
  4. 理解 Fragment:读 serde_derive/src/fragment.rs(74 行),看 quote_expr! 和 quote_block! 两个宏如何实现。

延伸阅读