Appearance
第 1 章 为什么需要 Serde:M×N 问题与 M+N 解法
1.1 一个被所有人忽视的工程灾难
在 2016 年以前,Rust 社区的数据序列化是一场灾难。
想象你在写一个后端服务:从 HTTP 请求里读 JSON、把配置文件写成 TOML、用 MessagePack 和另一个微服务通信、把热数据写进 Bincode 格式的缓存文件。你定义了 30 个业务数据结构——User、Order、Product、Config、Event……然后你遇到了第一个问题:
每一个数据结构,都要为每一种格式写一份序列化代码。
30 个结构体 × 5 种格式 = 150 份几乎重复的胶水代码。这还没完。当业务变化、User 结构多了一个字段,你得去 5 个地方同步修改。任何一处漏改,就是一个运行时 bug。
这个问题有个正式的名字,叫 M×N 问题——M 种数据结构、N 种格式,实现复杂度是 M 乘以 N。2016 年以前的 Rust 生态就陷在这里。那时有一个叫 rustc_serialize 的标准库内置解法,但它有几个致命缺陷:只支持 JSON 一种格式、无法扩展自定义格式、错误处理粗糙、性能远不及手写。社区开始意识到,如果 Rust 想成为一门严肃的系统编程语言,必须先解决序列化这个底层问题。
2016 年 5 月(第一个稳定发布),一位名叫 David Tolnay 的工程师发布了 Serde 1.0。十年后的今天,Serde 是 crates.io 下载量最高的几个 crate 之一(和 syn、quote 等过程宏基础设施一起位列 Top 10,具体排名可在 crates.io 首页的 "Most Downloaded" 查证),几乎所有 Rust 项目都直接或间接依赖它。它定义了 Rust 生态如何处理序列化,并且在过程中意外地成为 Rust 过程宏系统最重要的案例。
本章要回答一个简单的问题:Serde 是怎么把 M×N 变成 M+N 的? 这个看似朴素的"数学公式转换"背后,藏着 Rust 零成本抽象最精彩的一次实战。
1.2 M×N 灾难的真实样貌
让我们先把"M×N 问题"具象化。假设没有 Serde,你需要为 User 结构体支持 JSON 和 Bincode 两种格式:
rust
// 没有 Serde 的朴素实现——仅为说明问题,不是真实 API
struct User {
id: u64,
name: String,
email: String,
}
// JSON 编码
impl User {
fn to_json(&self) -> String {
format!(
r#"{{"id":{},"name":"{}","email":"{}"}}"#,
self.id, self.name, self.email
)
}
fn from_json(s: &str) -> Result<User, String> {
// ... 手写 JSON parser,解析三个字段
todo!()
}
}
// Bincode 编码(假设格式:u64 + u32长度+字节 + u32长度+字节)
impl User {
fn to_bincode(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&self.id.to_le_bytes());
out.extend_from_slice(&(self.name.len() as u32).to_le_bytes());
out.extend_from_slice(self.name.as_bytes());
out.extend_from_slice(&(self.email.len() as u32).to_le_bytes());
out.extend_from_slice(self.email.as_bytes());
out
}
fn from_bincode(bytes: &[u8]) -> Result<User, String> {
// ... 手写二进制 parser
todo!()
}
}这段代码有三个问题:
第一,大量重复。 to_json、to_bincode、from_json、from_bincode 四个方法做的事情本质上一样——把 User 的三个字段按某种规则写出去,或按某种规则读回来。规则变了,但"读三个字段"的动作没变。每次加字段、改字段名,四个方法都要修改。
第二,难以扩展。 如果要加 YAML 支持,你得再写两个方法 to_yaml 和 from_yaml。如果有 30 个结构体要支持 5 种格式,那就是 30 × 5 × 2 = 300 个方法。
第三,类型不安全。 from_json 返回 Result<User, String>,错误类型是 String——你失去了所有结构化的错误信息。调用者无法区分"字段缺失"和"类型不匹配"。
这就是 M×N 灾难。用一张图看得更清楚:
每一条连线代表一份实现代码。M 个结构体连向 N 种格式,一共 M×N 条线。当 M=30、N=5,总线数是 150——而且每加一种格式,要新画 30 条线;每加一个结构体,要新画 5 条线。
1.3 其他语言怎么解决这个问题
M×N 问题不是 Rust 独有的。所有支持多种序列化格式的语言都遇到过。让我们看一下其他主流语言的解法,才能理解 Serde 为什么选择了一条和它们都不同的道路。
Java / Python / Go:反射
Java 的 Jackson、Python 的 pickle、Go 的 encoding/json 都使用同一个思路:反射(reflection)。
反射的核心能力是——在运行时,程序可以"看到"自己定义的类/结构体有哪些字段、每个字段是什么类型、甚至读写私有字段。有了这个能力,一个 JSON 序列化器只需要实现一次:
go
// Go 风格伪代码
func ToJSON(obj any) string {
t := reflect.TypeOf(obj) // 运行时拿到类型信息
v := reflect.ValueOf(obj)
var out strings.Builder
out.WriteString("{")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i) // 每个字段的元信息
value := v.Field(i) // 每个字段的实际值
out.WriteString(fmt.Sprintf("\"%s\":%v,", field.Name, value))
}
out.WriteString("}")
return out.String()
}这段代码不需要知道 User 长什么样。它在运行时查询类型信息,动态遍历字段。一份代码通吃所有结构体——M×N 被反射降成了 N(每种格式写一份反射代码即可)。
这个方案看起来完美,但对 Rust 不可接受,原因有三:
第一,运行时开销。 反射必须查询类型元数据、做动态分发、遍历字段描述表。具体开销依赖语言实现、对象形状和格式库,不应脱离基准测试给出固定百分比。Rust 的核心承诺是"零成本抽象"——不能为了方便把每一次序列化都变成运行时类型查询。
第二,Rust 不支持反射。 Rust 编译器默认不生成类型元数据(为了二进制体积和隐私)。std::any::Any 提供有限的类型 ID 查询能力,但不能枚举字段。想要真正的反射,必须靠编译期代码生成——这又回到了 serde 的路线。
第三,运行时反射抹杀类型检查。 用 Go 的 encoding/json 时,如果字段拼写错了,编译器不会提醒你,要等到运行时解析失败才发现。Rust 的类型系统是它最大的武器,不能丢。
C++:手写 + 代码生成器
C++ 社区分两派。一派是 Boost.Serialization,用模板特化(template specialization)让用户手动实现每种类型的序列化——这本质上和手写没区别。另一派是 Protobuf、Thrift、Cap'n Proto,让用户先写一个独立的 .proto/.thrift 文件定义 schema,然后由外部代码生成器(protoc)生成 C++ 代码。
代码生成路线的优点是性能极致、schema 可跨语言共享。缺点是:
- schema 和类型系统割裂。你得在两个地方维护同样的信息:一份
.proto、一份 C++ 类。虽然生成器能从.proto生成 C++ 代码,但反过来不行——已有的 C++ 类型不能"自动变成" Protobuf 消息。 - 构建流程复杂。
.proto编译要在 C++ 编译之前跑,构建系统(CMake、Bazel)要专门配置。 - 格式绑定死。每个
.proto绑定到 protobuf 格式,想换成 JSON?重写一遍。
动态语言的启示
注意 Java/Python/Go 的反射方案有一个很诱人的隐含优点:它把"序列化器"和"数据结构"完全解耦。写 User 的人不用为序列化操心,写序列化器的人也不用为 User 操心。这是一种漂亮的抽象——一种可扩展性。
Serde 的设计者想要保留这个解耦,同时避开运行时反射的性能代价和类型不安全问题。答案是:把反射搬到编译期。
1.4 Serde 的核心洞察:用中间层击碎 M×N
Serde 的解法从一个数学观察开始:M×N 问题之所以是 M×N,是因为每个数据结构直接连到每种格式。如果插入一个中间层,让所有数据结构只连到中间层,所有格式也只连到中间层,那么总线数就变成 M+N。
这个中间层叫 Data Model——Serde 定义的一套"通用数据表示"。它有 29 种原语:bool、i8..i64、u8..u64、f32、f64、char、str、bytes、option、unit、unit_struct、unit_variant、newtype_struct、newtype_variant、seq、tuple、tuple_struct、tuple_variant、map、struct、struct_variant 等。
所有数据结构都只需要"告诉 Data Model 自己长什么样":User 告诉 Data Model "我是一个 struct,有 3 个字段,分别是 u64、String、String"。这通过 Serialize trait 实现。
所有格式只需要"接受 Data Model 的描述、按自己的规则写出去":JSON 收到 "struct 有 3 个字段" 的描述,就写 {"field1":..,"field2":..,"field3":..};Bincode 收到同样的描述,就按二进制布局写。这通过 Serializer trait 实现。
关键洞察:Serialize 和 Serializer 解耦了。User 实现 Serialize 时,不需要知道最终是 JSON 还是 Bincode;JSON 格式实现 Serializer 时,不需要知道数据来自 User 还是 Order。
有了这个中间层:
- 新加一个数据结构(比如
Event):只要为它实现Serialize和Deserialize,所有格式自动支持。这就是#[derive(Serialize, Deserialize)]的威力。 - 新加一个格式(比如 CBOR):只要实现
Serializer和Deserializer,所有现有的数据结构自动支持。这就是serde_cbor、rmp-serde、postcard等格式 crate 的工作原理。
M+N 达成。
但魔鬼在细节里。这套抽象要真正 work,有三个必须回答的问题:
- Data Model 的 29 种原语够不够? 如果某个数据结构表达不出来,整个抽象崩溃。
- 抽象会不会带来性能损失? 中间层看起来多了一层开销,Rust 的零成本承诺还作数吗?
- 谁来为每个 User 结构体写
Serialize实现? 如果要用户手写,M×N 只是变成了 M×(N=1),并没有真正解决问题。
下面三节分别回答这三个问题——它们对应本书后续的三条主线。
1.5 Data Model:29 种原语的取舍
Data Model 的设计本身就是一门艺术。太少的原语无法表达复杂数据;太多的原语让格式实现复杂化。Serde 最终选了 29 种。
设计意图:29 种原语的选取不是凭感觉,而是对"真实世界数据形态"的归纳。Serde 覆盖了:7 种无符号整数 + 6 种有符号整数 + 2 种浮点 + bool/char + 2 种字符串(str/bytes)+ option/unit + 4 种"命名但无数据"的变体(unit_struct、unit_variant 等)+ newtype 包装 + 4 种序列类(seq、tuple、tuple_struct、tuple_variant)+ 3 种映射类(map、struct、struct_variant)。每一种都对应 Rust 类型系统中一个常见场景。
比如为什么要区分 seq(同类型元素变长序列)和 tuple(异构类型定长)?因为 JSON 数组和 Rust 的 Vec<T> 对应 seq,而 (i32, String, bool) 这种元组在 MessagePack 里可以被编码成更紧凑的固定长度数组——格式可以利用"我知道长度提前"这个信息做优化。
再比如为什么 struct 和 map 分开?看起来它们都是"key-value 集合"。区别是 struct 的 key 在编译期已知(字段名),map 的 key 在运行时才确定。Bincode 利用这个区别,对 struct 不写字段名(反正编译期双方都知道),省下大量字节;而对 map 就必须写 key。
Data Model 的完整设计是第 2 章的主题。这里只说结论:29 种原语足以表达 Rust 类型系统中几乎所有可序列化的形态,且每种原语都给了格式实现者利用特定信息优化的空间。
1.6 零成本:trait 与单态化的合谋
抽象有没有代价?让我们看一段简化的 Serde 调用:
rust
let user = User { id: 1, name: "alice".into(), email: "a@x.com".into() };
let json = serde_json::to_string(&user).unwrap();to_string 内部会:
- 创建一个
serde_json::Serializer - 调用
user.serialize(&mut serializer) user.serialize内部会调用serializer.serialize_struct("User", 3)?- 然后依次调用
serialize_field("id", &self.id)?、serialize_field("name", &self.name)?等
步骤 3、4 看起来是对一个 trait 对象的方法调用。trait 方法调用有两种实现:
- 动态分发(dynamic dispatch):
&dyn Serializer,运行时查虚表。有开销。 - 静态分发(static dispatch):
<S: Serializer>,编译期单态化。零开销。
Serde 选择了静态分发。看 Serialize trait 的真实定义:
rust
// serde/serde_core/src/ser/mod.rs
pub trait Serialize {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer;
}serialize 方法的 serializer 参数是泛型参数 S: Serializer,不是 &dyn Serializer。这意味着当你写 user.serialize(json_serializer) 时,编译器会为 User + serde_json::Serializer 这个具体组合生成一份专门的代码(单态化),把所有 trait 方法调用静态化成直接函数调用,然后内联、常量传播、死代码消除一通优化——最终产物和你手写的 user.to_json() 几乎一模一样。
设计意图:这里有一个关键权衡。静态分发零开销,但每个
(数据结构, 格式)组合都会生成一份单态化代码,二进制膨胀。Serde 选了零开销、容忍膨胀——对系统编程来说这是正确的取舍。想要动态分发的用户可以用erased-serde这个 crate,它把 Serde 包装成 trait 对象,代价是 10-20% 性能损失。
单态化 + 内联的组合是第 18 章的主题。那一章会给出汇编证据:serde_json::to_string(&user) 在 release 模式下编译出的代码,和手写 format!(...) 几乎字节相同。
1.7 derive 宏:让用户不再手写
M+N 的最后一个拼图是——谁来为 30 个业务结构体写 Serialize 实现?
如果要用户手写:
rust
impl Serialize for User {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("User", 3)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("name", &self.name)?;
state.serialize_field("email", &self.email)?;
state.end()
}
}这段代码的每一行都是机械的——它完全由 User 的字段结构决定。我们可以让编译器自动生成它。这就是过程宏的用武之地:
rust
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}一行 #[derive(Serialize)] 告诉编译器:"请扫描 User 的字段,为我生成上面那段代码"。
具体怎么生成?Serde 提供了一个独立 crate serde_derive,它注册了一个过程宏:
rust
// serde/serde_derive/src/lib.rs:59
#[proc_macro_derive(Serialize, attributes(serde))]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(input as DeriveInput);
ser::expand_derive_serialize(&mut input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}当编译器看到 #[derive(Serialize)],它把 User 的定义(完整的 TokenStream)传给 derive_serialize 函数,函数返回新的 TokenStream——也就是 impl Serialize for User { ... } 那段代码。编译器把这段代码插入当前 crate,继续编译。
整个过程发生在编译期。 运行时没有任何 "derive" 的痕迹——只有普通的 trait 实现代码。这就是为什么 Serde 能既有"反射级别的便利"又保持"零开销"。
第 5-9 章会完整讲过程宏工具链;第 10-13 章会拆解 serde_derive 的实现细节。这里只需要记住一个事实:Serde 用过程宏把"手写 Serialize 实现"这件事自动化了,完成了 M+N 解法的最后一块拼图。
1.8 三个基石:Data Model、trait、过程宏
把前三节串起来,Serde 的架构可以画成这张图:
三个基石:
- Data Model(第 2-4 章):29 种原语定义了"所有可序列化数据的通用表示"。
Serialize和Serializertrait 通过这套表示通信。 - 过程宏(第 5-14 章):
serde_derive在编译期读取用户定义的数据结构,自动生成Serialize/Deserialize实现。用户只写一行#[derive(...)]。 - 零成本抽象(贯穿全书,第 18 章总结):trait 泛型 + 单态化 + 内联让整套抽象在编译后消失,产物和手写代码性能无差别。
这三者缺一不可:
- 没有 Data Model,
Serialize没有统一接口,M×N 回归。 - 没有过程宏,用户要手写 M 份实现,等于 M×(N=1),收益减半。
- 没有零成本抽象,中间层的性能代价会让用户抛弃 Serde 转向手写。
1.9 Serde 和它的竞争者们
Serde 不是 Rust 里唯一的序列化方案。理解竞争格局能帮你判断什么时候用 Serde、什么时候不用。
Prost / protobuf-rust:如果你必须用 Protobuf 格式(比如跨语言 RPC),Prost 是比 serde-protobuf 更好的选择——它基于 .proto 文件生成 Rust 类型,类型和 schema 一一对应。但它绑死 Protobuf,换格式重来一遍。
rkyv:零拷贝反序列化专用库。把二进制数据直接映射为 Rust 结构体,完全跳过"解析"步骤。据 rkyv 官方 benchmark 和社区测评,针对大 payload 反序列化性能可以达到 Serde + Bincode 的数十倍(具体倍数视场景波动极大,读者应以自己场景实测为准),代价是格式固定、不兼容任何其他语言。适合 Rust-only 的高性能缓存。
手写 nom/winnow:对于协议实现(HTTP/2 帧、Postgres wire protocol),Serde 不合适——这些协议有复杂的控制流,不是简单的 struct-to-bytes。nom 是 Rust 最流行的 parser combinator 库,适合这类场景。
Serde 的统治领域:应用层数据交换——JSON API、配置文件、缓存、消息队列、数据库行映射。在这些场景里,Serde 是事实标准,没有竞争者。
一个实用判断:如果你的数据可以用 struct + enum + Vec + HashMap 表达,并且你可能换格式,用 Serde。如果不满足(协议解析、极致零拷贝、单一格式锁定),考虑专用方案。
1.9.1 M+N 真实成本:把"中间层"放到秤上称一称
§1.4 给出 Serde 的核心公式 M+N——本章前半反复说"消除 M×N 灾难"——但**这个中间层本身值多少行代码?**实测把 §1.8 提到的"三个基石"对应到具体 crate——
| 基石 | crate | 真实行数 |
|---|---|---|
| Data Model + trait | serde_core (含 ser/ + de/) | 11139(ser/ 3441 + de/ 7698,ch04 §4.10.1 实测) |
| 过程宏(用户侧自动生成) | serde_derive | 8969(ch10 §10.2 实测) |
| 工具链:TokenStream/quote | proc-macro2 | 6030(ch06 §6.11.1 实测 v1.0.106) |
| 三者合计 | — | 约 26000 行 |
这就是 "M+N" 的真实物理重量——#[derive(Serialize, Deserialize)] 这一行用户代码的背后、约 26000 行基础设施在支撑。但比较一下"如果不用 Serde"的代价——
- M = 50 个用户类型、N = 8 种格式(JSON/YAML/TOML/MessagePack/CBOR/Bincode/Postcard/Avro)
- 每对 (T, F) 平均 手写 80 行(包含 schema、解析、错误处理)
- M×N 路线:50 × 8 × 80 = 32000 行用户代码
- M+N 路线:50 × 30 行 derive 注解 + 8 × 1 个格式 crate 引入 = 大约 1500 行用户代码 + 26000 行 Serde 基础设施(写一次、所有用户共享)
对单个用户:0.05% 的基础设施成本(26000 / 50_user_projects = 520 行/项目摊销)换来 30x 用户代码减少(1500 vs 32000)。
对整个生态:26000 行只写一次,节省下来的是全 Rust 生态所有用户的 M×N 工程量——按 crates.io 上 100K+ 个用 serde 的 crate 估算、节省的总代码量是 26000 的 几个数量级。
这条计算让 §1.4 "M+N 击碎 M×N" 从口号变成账本——任何质疑"中间层值不值得"的人都能拿这组数字算 ROI。这也解释了为什么 Serde 从 2017 一路到现在十年保持核心 API 不变(§3.11)——26000 行基础设施破坏不起,社区会把任何 breaking change 推回去。
1.9.2 三个 crate 把 M+N 固化成源码边界
M+N 不是一个口号,它在源码里被切成三条边界:
| 边界 | 源码锚点 | 负责的事 | 不负责的事 |
|---|---|---|---|
serde / serde_core | serde/src/lib.rs:252-254 重导出 Serialize、Serializer、Deserialize、Deserializer | 定义 Data Model 和 trait 协议 | 不知道 JSON、Bincode、YAML 的字节格式 |
serde_derive | serde_derive/src/lib.rs:113-124 注册 Serialize / Deserialize 两个 derive 入口 | 读取用户类型,生成 trait impl | 不解析 JSON,不执行用户业务逻辑 |
serde_json | serde_json/src/lib.rs:396-406 暴露 from_str、to_string、Value 等格式 API | 把 Serde Data Model 映射到 JSON 字节和 Value 树 | 不关心用户结构体字段如何被宏展开 |
这个分工解释了为什么 Serde 能长期稳定。serde/src/lib.rs:268-279 只是把 derive 宏作为可选 feature 重导出,核心 trait 本身不依赖过程宏;serde_derive/src/lib.rs:77-80 把输入先交给 syn::parse_macro_input,说明宏入口只处理 TokenStream 到 AST 的转换;serde_json/src/lib.rs:400-404 暴露 to_string / to_writer / Serializer,说明格式 crate 的公共面是"把任何 Serialize 类型写成 JSON",而不是"认识每一个用户类型"。
工程上这带来三条直接后果。
第一,格式 crate 可以独立演化。serde_json 可以优化字符串转义、浮点格式化、错误定位,不需要修改 serde_derive。第 17 章会看到 serde_json/src/ser.rs 里 Serializer 和 Formatter 分层,正是这个边界的格式侧落地。
第二,宏生成代码可以保持格式无关。第 12、13 章展开 cargo expand 时,生成代码调用的是 serialize_struct、deserialize_struct、serialize_field、MapAccess::next_value 这些 trait 方法,而不是 write('{') 或 parse_string()。这意味着同一份 derive 结果可以喂给 JSON、MessagePack、CBOR。
第三,用户依赖可以按需裁剪。只写手工 impl 的库可以依赖 serde 而不开启 derive;需要 JSON 才引入 serde_json;需要宏才启用 serde = { features = ["derive"] }。M+N 的真正价值不只是减少代码量,而是让"类型协议、代码生成、格式实现"三者可以分别测试、分别发布、分别优化。
还有一个经常被忽略的收益:错误隔离。用户类型写错属性,错误来自 serde_derive;JSON 输入不合法,错误来自 serde_json;某个格式不支持 map 的非字符串 key,错误来自格式 crate。边界清楚,排查路径就短。反过来,如果把反射、格式解析、类型规则塞进一个运行时大框架,用户看到的往往只是"serialization failed",很难判断是类型声明、格式语法还是协议约束出了问题。Serde 的 M+N 不是把复杂度消灭,而是把复杂度放到可定位的层里。
因此,本书后面读源码时会始终沿着这三层走:先看 trait 协议,再看 derive 如何生成协议调用,最后看格式 crate 如何兑现协议约束。
1.10 本章小结与预告
本章的核心是一个公式:
Serde = Data Model(中间层) + trait(抽象边界) + 过程宏(代码生成)
这三样东西把序列化的工程复杂度从 M×N 降到 M+N。每一部分都不是 Serde 发明的——中间层思想来自操作系统的分层设计,trait 泛型是 Rust 原生特性,过程宏是编译器提供的能力。Serde 的天才之处在于把它们拼在一起,做到了其他语言都做不到的事情:运行时零开销 + 编译期类型安全 + 用户零负担。
后续章节的组织如下:
- 第 2 章 深入 Data Model,列出全部 29 种原语,解释每一种的设计意图。
- 第 3、4 章 拆解
Serialize/Serializer/Deserialize/Deserializer四个核心 trait 的 API 设计。为什么Serializertrait 内有 30 个方法(ch03 §3.3 实测:23 个一次性 + 7 个状态机入口)、再加 7 个 SerializeXxx 子 trait 共 9 个serialize_*方法(ch03 §3.11.1)= 用户实现 Serializer 总负担 39 个方法(ch04 §4.10.1)?为什么Deserializer是 31 个方法、Visitor是 27 个?Visitor 模式从哪冒出来的? - 第 5-9 章 从零开始建立过程宏知识体系。TokenStream 是什么、syn 如何解析、quote 如何生成、如何写第一个可工作的 derive 宏。
- 第 10-14 章 读
serde_derive源码。你会看到本章描述的理论在真实代码里如何落地。 - 第 15-16 章 高阶主题——借用反序列化(为什么
&'de str能零拷贝?)、#[serde(with)]和remote的实现。 - 第 17 章
serde_json源码:一个格式 crate 是如何把自己接入 Serde Data Model 的。 - 第 18 章 总结 Serde 的设计哲学,并提炼出你可以用在自己项目里的模式。
动手实验
完成以下实验能帮你建立第 1 章的直觉:
- 安装 cargo-expand:
cargo install cargo-expand。这是阅读本书最重要的工具。 - 最小复现:新建一个 crate,
cargo new --lib serde-demo。在Cargo.toml加上serde = { version = "1", features = ["derive"] }和serde_json = "1"。 - 观察 derive 展开:在
src/lib.rs写:rust然后运行use serde::Serialize; #[derive(Serialize)] pub struct User { pub id: u64, pub name: String, }cargo expand。你会看到#[derive(Serialize)]实际展开成了 50 多行代码——一个完整的impl Serialize for User块。把它抄下来,这就是你的第一份 Serde 源码研究样本。 - 思考题:如果把
u64改成Vec<u64>,展开的代码会变成什么样?为什么?(提示:Vec 不是 struct 字段,它自己会被序列化成 seq。)
延伸阅读
- Serde 官方主页:概念索引,Data Model 的官方描述。
- Serde 1.0 发布博文(David Tolnay, 2017):理解 Serde 历史背景。
- Rust RFC #1681: The
serdecrate:早期设计讨论(RFC 最终没通过,但讨论内容非常有价值)。 - erased-serde:trait 对象版本的 Serde,理解静态分发的取舍。
- 丛书卷一《Rust 编译器》第 6 章"单态化"、第 7 章"trait 分发":本章"零成本"部分的编译器视角补充。