Serde 元编程
第 15 章 借用反序列化与零拷贝的生命周期魔法
15.1 一个看起来不可能的特性
回到最基本的问题:反序列化 JSON 得到一个字符串,Rust 端怎么表示?
// 方案 A:String(拥有所有权)
#[derive(Deserialize)]
struct UserOwned { name: String }
// 方案 B:&str(借用)
#[derive(Deserialize)]
struct UserBorrowed<'a> { name: &'a str }
方案 A 每次反序列化都要复制一遍字符串——把 JSON 里的字节复制到新分配的 String。方案 B 让字段指向原始 JSON 输入里的字节——零拷贝、零分配。
对处理大量 JSON 的服务(API 网关、日志解析器、配置加载器),方案 B 通常显著更快(本章 15.11 节给出的实测数据是 2.5 倍、分配次数差 3800 倍)。但它看起来很危险——&'a str 指向哪里?它的生命周期从哪来?Serde 如何保证借用安全?
更神奇的是,你不用改变任何反序列化调用代码:
let s = r#"{"name": "alice"}"#;
let owned: UserOwned = serde_json::from_str(s).unwrap(); // 复制
let borrowed: UserBorrowed = serde_json::from_str(s).unwrap(); // 零拷贝
同一个 serde_json::from_str,对 UserOwned 自动复制、对 UserBorrowed 自动借用——Serde 在编译期根据字段类型决定策略。
这是 Serde 最精妙的一块——用 Rust 的生命周期系统把"能否零拷贝"写进类型。本章要拆解这个魔法:'de 生命周期在 trait 签名里如何流动、visit_borrowed_str 和 visit_str 如何分工、#[serde(borrow)] 属性做什么、哪些场景支持零拷贝哪些不支持。
本专栏基于 serde 1.0.228。对应的官方文档是 Understanding deserializer lifetimes——值得配合阅读。
15.2 Deserialize trait 的 'de 参数回顾
我们在第 4 章见过 Deserialize 的定义:
// serde_core/src/de/mod.rs:554
pub trait Deserialize<'de>: Sized {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>;
}
'de 是 Deserializer 输入数据的生命周期——输入数据活着的那段时间。
具体解释:
- 用户调用
serde_json::from_str(s),s是&'a str。 - serde_json 内部创建一个
Deserializer<StrRead<'a>>,它借用s。 - 反序列化一个
T: Deserialize<'a>,'a就是 trait 的'de。 - 如果 T 内部有
&'a str字段(通过'de = 'a),它可以指向原始s的某段字节。
'de 不是固定的某个生命周期——它由调用方决定。from_str 传入 &'a str,'de = 'a。from_reader 从 io::Read 流式读,没有长期存在的借用源,所以只接受 DeserializeOwned(对任意 'de 都能反序列化的类型):
// serde_core/src/de/mod.rs:632
pub trait DeserializeOwned: for<'de> Deserialize<'de> {}
impl<T> DeserializeOwned for T where T: for<'de> Deserialize<'de> {}
for<'de> 是高阶生命周期绑定(Higher-Ranked Trait Bound, HRTB)——表达"对任意可能的 'de 都能实现"。&'a str 不是 DeserializeOwned(它依赖特定 'a),只有 String 这类自持有类型才满足。
15.3 Visitor 的三种 visit_str
第 4 章提到 Visitor 有三个字符串相关方法:
// serde_core/src/de/mod.rs:1526-1573
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E>;
fn visit_borrowed_str<E: Error>(self, v: &'de str) -> Result<Self::Value, E>;
fn visit_string<E: Error>(self, v: String) -> Result<Self::Value, E>;
差异在字符串参数的生命周期和所有权:
visit_str(&str):任意短期借用,不能保留(生命周期未知,只能立即复制)。visit_borrowed_str(&'de str):'de生命周期借用——可以保留到返回值里。visit_string(String):拥有所有权,可以 move 走。
源码文档还给了实现者一条硬规则(serde_core/src/de/mod.rs:1560-1561):只实现 visit_string、不实现 visit_str 永远是错的,原文紧接着写明 "Implement neither, both, or just visit_str."——两个都不实现、两个都实现、或只实现 visit_str。原因是文档明说并非每个 Deserializer 都会照顾 deserialize_string 的请求(:1557-1558),很多路径只会调 visit_str 或 visit_borrowed_str;后者默认转发给 visit_str(:1541),而 visit_str 的默认实现直接返回 invalid_type 错误(:1530)——只实现 visit_string 的 Visitor 在这些路径上全部失败。
Deserializer 根据自己的能力选调用哪个。看 serde_json(简化):
// 解析到一个 JSON 字符串,在原 bytes 里是 s[start..end]
if need_unescape {
// 字符串有 \n \t 等转义字符,必须解码 → 解到 Deserializer 自带的 scratch 缓冲里
// (serde_json 的 src/de.rs:1428-1430:Reference::Copied → visit_str)
let decoded: &str = unescape_into(&mut self.scratch, &s[start..end])?;
visitor.visit_str(decoded)
} else {
// 无转义,可以直接借用原 bytes
let slice = &s[start..end]; // 类型是 &'de str(因为 s: &'de str)
visitor.visit_borrowed_str(slice)
}
关键:Deserializer 尽可能走 visit_borrowed_str——这是零拷贝路径。只有转义等特殊情况才回落到 visit_str(拷贝路径)。注意范围:从文本解析的那条路(serde_json 的 src/de.rs)确实一次 visit_string 都不调——它手里只有 scratch 里的 &str;调 visit_string 的是 Value 的 Deserializer(serde_json 的 src/value/de.rs:295、:402、:420、:1427),因为 Value::String 本来就持有一个 owned String,直接 move 出去比复制划算。
Visitor 的实现决定能否接受借用:
String::deserialize的 Visitor(serde_core/src/de/impls.rs:585-628的StringVisitor)实现visit_str(复制)、visit_string(直接使用)以及visit_bytes/visit_byte_buf;visit_borrowed_str用 trait 默认实现转发到visit_str,还是复制。所有路径产出 String。&'de str::deserialize的 Visitor 只实现visit_borrowed_str(和visit_borrowed_bytes)——visit_str/visit_string用默认实现返回 invalid_type 错误。这意味着&'de str无法从"必须复制"的场景反序列化。
15.4 &'de str 的 Deserialize 实现
看 serde 里 &str 的真实实现(serde_core/src/de/impls.rs,简化):
impl<'de: 'a, 'a> Deserialize<'de> for &'a str {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>,
{
struct StrVisitor;
impl<'a> Visitor<'a> for StrVisitor {
type Value = &'a str;
fn expecting(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("a borrowed string")
}
fn visit_borrowed_str<E: Error>(self, v: &'a str) -> Result<&'a str, E> {
Ok(v) // 直接返回借用
}
// 真实源码还实现了 visit_borrowed_bytes(做一次 UTF-8 校验,见 15.15 节)
// visit_str / visit_string 用默认实现(返回 invalid_type 错误)
}
deserializer.deserialize_str(StrVisitor)
}
}
'de: 'a 是一个生命周期约束——"'de 至少活得和 'a 一样长"。保证返回的 &'a str 在 'de 范围内有效。
关键:StrVisitor 只实现 visit_borrowed_str。如果 Deserializer 调 visit_str(不能借用的路径),默认实现返回 invalid_type 错误——&str 反序列化失败。
这就是为什么:
let s: String = r#"{"name": "alice"}"#.into();
let user: UserBorrowed = serde_json::from_str(&s).unwrap(); // OK
let s = String::from(r#"{"name": "alice"}"#);
let reader = s.as_bytes();
let user: UserBorrowed = serde_json::from_reader(reader).unwrap(); // 编译错!
from_reader 背后的 deserializer 是 IoRead 模式——流式读取,不能 visit_borrowed_str(因为数据读过就可能被丢弃)。所以它的签名直接要求 T: DeserializeOwned,编译器检查 UserBorrowed 是否对任意 'de 都实现了 Deserialize——发现不行,编译期拒绝,报 implementation of `Deserialize` is not general enough(serde 1.0.228 + serde_json 1.0.149 实测)。
15.5 serde_derive 如何为 &'de 字段生成代码
生成代码的关键:字段类型里出现的生命周期 'a,serde_derive 默认不会把它和 'de 绑在一起。要触发借用,要么字段类型属于少数几种"一看就是借用"的形态,要么显式告诉 serde。
对比两种字段:
// 用户代码 A:隐式借用——&'a str 是 serde_derive 直接认得的形态
#[derive(Deserialize)]
struct A<'a> {
name: &'a str, // 不用写 #[serde(borrow)],'de: 'a 自动加上
}
// 用户代码 B:显式借用——Cow 必须写 #[serde(borrow)],否则永远得到 Cow::Owned
#[derive(Deserialize)]
struct B<'a> {
#[serde(borrow)]
name: Cow<'a, str>,
}
为什么默认不借用? 因为 serde 不能可靠地猜测 'a 是不是想要 'de。用户可能希望 'a 是某个更短的生命周期,甚至完全无关。保守地,serde_derive 只对最明显的形态自动借用,其余要求用户显式加 #[serde(borrow)]。
隐式借用的白名单只有四种(serde_derive/src/internals/attr.rs:1579-1585 的 is_implicitly_borrowed):
&'a str&'a [u8]Option<&'a str>Option<&'a [u8]>
Cow<'a, str> / Cow<'a, [u8]> 不在白名单里——标准库 Cow<'a, T> 的 Deserialize impl(serde_core/src/de/impls.rs:1980-1992)总是产出 Cow::Owned。真实逻辑在 serde_derive/src/internals/attr.rs:1195-1245——只有当字段带了 #[serde(borrow)](borrowed_lifetimes 非空)时,才检查这个字段是不是 Cow<'a, str> / Cow<'a, [u8]>,命中就自动挂上对应的 deserialize_with:
// serde_derive/src/internals/attr.rs:1195-1245(节选、精简,不是源码原文)
let mut borrowed_lifetimes = borrowed_lifetimes.get().unwrap_or_default();
if !borrowed_lifetimes.is_empty() {
// Cow<str> and Cow<[u8]> never borrow by default: ... A #[serde(borrow)]
// attribute enables borrowing that corresponds roughly to these impls:
// impl<'de: 'a, 'a> Deserialize<'de> for Cow<'a, str>
// impl<'de: 'a, 'a> Deserialize<'de> for Cow<'a, [u8]>
if is_cow(&field.ty, is_str) {
// 等价于给这个字段加上 #[serde(deserialize_with = "_serde::__private228::de::borrow_cow_str")]
deserialize_with.set_if_none(/* _serde::__private228::de::borrow_cow_str */);
} else if is_cow(&field.ty, is_slice_u8) {
deserialize_with.set_if_none(/* _serde::__private228::de::borrow_cow_bytes */);
}
} else if is_implicitly_borrowed(&field.ty) {
// Types &str and &[u8] are always implicitly borrowed. No need for
// a #[serde(borrow)].
collect_lifetimes(&field.ty, &mut borrowed_lifetimes);
}
is_cow 自己在同文件 :1609;#[serde(borrow)] 显式声明的生命周期由 borrowable_lifetimes(:1716)校验——写了字段类型里不存在的生命周期会直接报错。
15.5.1 真实的自动识别:7 个彼此组合的 is_* 谓词
上面那段"简化"伪代码掩盖了 serde_derive 里这一块的真实工程——不是一个函数、是 7 个小谓词组合拼出"这类型是不是能借用"的判定(serde_derive/src/internals/attr.rs:1609-1707):
fn is_cow(ty, elem) -> bool // Cow<'_, T> 且 T 满足 elem 谓词
fn is_option(ty, elem) -> bool // Option<T> 且 T 满足 elem
fn is_reference(ty, elem) -> bool // &'_ T 且不可变 + T 满足 elem
fn is_str(ty) -> bool // str(primitive 路径)
fn is_slice_u8(ty) -> bool // [u8] slice
fn is_primitive_type(ty, primitive) // Type::Path + 路径为 primitive
fn is_primitive_path(path, primitive) // 严格单段 path 匹配
这种"谓词组合"设计允许复合类型的递归检测——比如 Option<&'a str> 通过 is_option(ty, is_implicitly_borrowed_reference) 识别、Cow<'a, [u8]> 通过 is_cow(ty, is_slice_u8) 识别。隐式借用的 4 种形态(&str、&[u8]、Option<&str>、Option<&[u8]>)加上显式 #[serde(borrow)] 才生效的 2 种(Cow<str>、Cow<[u8]>),全部靠这几个谓词组合出来。
is_primitive_path 的严格定义(line 1702-1707)尤其值得看:
fn is_primitive_path(path: &syn::Path, primitive: &str) -> bool {
path.leading_colon.is_none()
&& path.segments.len() == 1
&& path.segments[0].ident == primitive
&& path.segments[0].arguments.is_empty()
}
四条硬约束:无前导 ::、单段、ident 精确匹配、无泛型参数。写这么严格是因为——下面要讲的那个诚实注释解释了为什么。
15.5.2 源码里少见的"False negative / False positive"坦白注释
is_reference 上方有一段 serde_derive 里最诚实的注释(line 1657-1676,原文直引):
// Whether the type looks like it might be `&T` where elem="T". This can have
// false negatives and false positives.
//
// False negative:
//
// type Yarn = str;
//
// #[derive(Deserialize)]
// struct S<'a> {
// r: &'a Yarn,
// }
//
// False positive:
//
// type str = [i16];
//
// #[derive(Deserialize)]
// struct S<'a> {
// r: &'a str,
// }
直白承认两种错误:
- False negative:用户写了
type Yarn = str别名、再写&'a Yarn。语义上确实是"借用 str"、应该被 auto-borrow。但is_str只做语法匹配、看到Yarn不是str、返回 false——漏识别借用。用户必须显式写#[serde(borrow)]。 - False positive:用户疯狂地把
str重新定义为[i16]——此时&'a str语义上是&'a [i16]、不是真正的"借用字符串"。但is_str还是语法匹配、返回 true——错认借用,照样给它加上'de: 'a。(这个别名同时会遮蔽生成代码里的str,实测报出一串 E0277 / E0308 / E0053,编译不过。)
为什么 serde 明知有缺陷还选这个 syntactic 路径?——因为proc-macro 阶段根本看不到类型别名解析(type Yarn = str 在 derive 展开时只是 token、别名要到类型检查阶段才展开)、也看不到 str 被重定义。proc-macro 输入只是 token 流、做不了语义检查。两个错误方向都认了、作为语法分析的本质限制。
这种"承认自己能力边界"的注释是读 rustc/serde 源码时最有价值的地方——比任何设计文档都更清楚地告诉你"这段代码工作在什么假设下"。如果你的代码库里用 type Yarn = str 这类别名、别指望 #[derive(Deserialize)] 自动识别——写显式 #[serde(borrow)] 是你的责任。
ungroup(ty) 的调用(除了转手给 is_primitive_type 的 is_str 和只接收 path 的 is_primitive_path,每个谓词开头都调)——处理 syn::Type::Group,这是 macro-generated 代码里会出现的"看不见的括号"。用户看不到、但 syn 的 AST 里可能有。ungroup 递归剥掉这层、让语法匹配不因为组分隔符被干扰。这个工具函数不起眼但必要——proc-macro 开发里的一个通用 gotcha。
15.6 借用字段对生成代码的影响
回到第 13 章的反序列化生成。普通字段:
// name: String
let mut __field0: Option<String> = None;
// visit_map 里:
__field0 = Some(__map.next_value::<String>()?);
借用字段:
// name: &'a str(隐式借用,不必写 #[serde(borrow)])
let mut __field0: Option<&'a str> = None; // 字段类型原样照搬,还是 &'a str
// visit_map 里:
__field0 = Some(__map.next_value::<&'a str>()?);
区别:
- 局部变量类型就是字段类型
Option<&'a str>——生成器不改写字段类型(serde 1.0.228 展开实测)。 next_value::<&'a str>()调用<&'a str as Deserialize<'de>>::deserialize——它只接受visit_borrowed_str(和visit_borrowed_bytes);这个 impl 要求'de: 'a,由下面 impl 头上的约束保证。- 如果 Deserializer 不能提供
'de生命周期的借用,这一步会失败(返回 invalid_type 错误)。
impl 块的生命周期约束也会变化:
// 普通
impl<'de> Deserialize<'de> for User { ... }
// 带借用字段
impl<'de: 'a, 'a> Deserialize<'de> for User<'a> { ... }
多了一个 'a 生命周期参数和 'de: 'a 约束。这是 serde_derive/src/de.rs:292 的 borrowed_lifetimes 收集、再由 DeImplGenerics 拼进 impl 头部的(第 13 章 13.15 节)。
15.7 Cow<'a, str>:两全其美
Cow<'a, str> 的定义:
pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
"Copy on Write"——可以借用也可以拥有。但要注意:标准库 Cow<'a, T> 的 Deserialize impl(serde_core/src/de/impls.rs:1980-1992)永远走 Owned——它只是 T::Owned::deserialize(deserializer).map(Cow::Owned),和 String 没区别。真正"自适应"的版本是字段加了 #[serde(borrow)] 后 serde_derive 自动挂上的 borrow_cow_str(serde/src/private/de.rs:64-135,15.5 节):
// serde/src/private/de.rs:64(精简)
pub fn borrow_cow_str<'de: 'a, 'a, D, R>(deserializer: D) -> Result<R, D::Error>
where D: Deserializer<'de>, R: From<Cow<'a, str>>,
{
struct CowStrVisitor;
impl<'a> Visitor<'a> for CowStrVisitor {
type Value = Cow<'a, str>;
fn visit_str<E: Error>(self, v: &str) -> Result<Cow<'a, str>, E> {
Ok(Cow::Owned(v.to_owned())) // 短期借用 → 复制为 Owned
}
fn visit_borrowed_str<E: Error>(self, v: &'a str) -> Result<Cow<'a, str>, E> {
Ok(Cow::Borrowed(v)) // 'a 借用 → 直接用 Borrowed
}
fn visit_string<E: Error>(self, v: String) -> Result<Cow<'a, str>, E> {
Ok(Cow::Owned(v)) // String → Owned(零拷贝 move)
}
// 还有 visit_bytes / visit_borrowed_bytes / visit_byte_buf 三个字节版本,各做一次 UTF-8 校验
}
deserializer.deserialize_str(CowStrVisitor).map(From::from)
}
三种路径都成功:
visit_str(短期借用):复制为 Owned。visit_borrowed_str('de 借用):Borrowed(零拷贝)。visit_string(拥有):move 成 Owned(零拷贝)。
#[serde(borrow)] Cow<'de, str> 是反序列化的"最佳选择"——如果输入支持借用就零拷贝,否则复制。行为自适应。忘了写 #[serde(borrow)],它就退化成一个绕了一圈的 String。
三者的定性对比(15.11 节有实测数字):
String:永远复制、每个字段一次分配&'de str:总是借用、零分配(但from_reader编译错)#[serde(borrow)] Cow<'de, str>:从from_str借用,遇到转义就复制——平均最好(但它同样不是DeserializeOwned,from_reader照样编译错;只有不加borrow的Cow能进from_reader,那时它永远是Owned)
15.8 #[serde(borrow)] 属性的语义
#[serde(borrow)] 有几种形态:
// 1. 不带参数:自动推导借用的生命周期
#[derive(Deserialize)]
struct A<'a> {
#[serde(borrow)]
name: &'a str,
}
// 2. 明确指定生命周期
#[derive(Deserialize)]
struct B<'a, 'b> {
#[serde(borrow = "'a")]
name: &'a str,
#[serde(borrow = "'b")]
tags: Vec<&'b str>, // 嵌套借用
}
无参数版本:serde 扫描字段类型,收集所有出现的生命周期,全部约束为 'de。
有参数版本:只约束指定的生命周期为 'de。其他生命周期自由。
这在什么场景用? 不同字段可能来自不同输入:
#[derive(Deserialize)]
struct Merged<'a, 'b> {
#[serde(borrow = "'a")]
from_file: &'a str, // 从一个文件借
#[serde(skip)]
from_env: &'b str, // 从环境变量借(不参与反序列化)
}
这种场景 'a 和 'b 必须是不同生命周期,不能都等于 'de。
实现在 serde_derive/src/internals/attr.rs 里:
// attr::Field:解析结果直接是一个生命周期集合
borrowed_lifetimes: BTreeSet<syn::Lifetime>,
// attr::Variant:newtype variant 上的 #[serde(borrow)]
borrow: Option<BorrowAttribute>,
struct BorrowAttribute {
path: syn::Path,
lifetimes: Option<BTreeSet<syn::Lifetime>>,
}
生成 impl 块的 generics 时,serde_derive/src/de.rs:292 的 borrowed_lifetimes 把各字段的 borrowed_lifetimes 集合取并集,de_lifetime_param(serde_derive/src/de.rs:270)再把它们变成 'de: 'a + 'b 这样的 bound;bound.rs 管的是 T: Deserialize<'de> 这类类型参数约束,'de: 'a 不经过它。
15.9 字节数组的借用:&'de [u8] 和 &'de Bytes
字节串的借用反序列化和字符串类似:
#[derive(Deserialize)]
struct Msg<'a> {
#[serde(borrow, with = "serde_bytes")]
payload: &'a [u8],
}
这里的 serde_bytes 主要是为序列化那一侧准备的。第 2 章讲过——序列化时 &[u8] 默认走 seq 路径(每个字节一个元素),而不是 bytes 路径(整体 blob)。serde_bytes 是一个辅助 crate,提供 serialize/deserialize 函数走 bytes 路径。对这个只 derive 了 Deserialize 的 Msg,去掉 with = "serde_bytes"(连同 borrow)照样能从 serde_json::from_str 借出 payload(实测)。
准确地说,&'a [u8] 的 Deserialize 本身并不走 seq——serde 自带的 impl(serde_core/src/de/impls.rs:741-772)直接调 deserialize_bytes:
struct BytesVisitor;
impl<'a> Visitor<'a> for BytesVisitor {
type Value = &'a [u8];
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a borrowed byte array")
}
fn visit_borrowed_bytes<E>(self, v: &'a [u8]) -> Result<Self::Value, E>
where
E: Error,
{
Ok(v)
}
fn visit_borrowed_str<E>(self, v: &'a str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(v.as_bytes())
}
}
impl<'de: 'a, 'a> Deserialize<'de> for &'a [u8] {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_bytes(BytesVisitor)
}
}
这需要 Deserializer 在 deserialize_bytes 里调 visit_borrowed_bytes——二进制格式(MessagePack、CBOR、bincode)自然支持;serde_json 也支持:它把 JSON 字符串的原始字节交给 visit_borrowed_bytes(serde_json 的 src/de.rs:1648-1650)。走 seq 路径的是 Vec<u8>、[u8; N] 这些"元素类型恰好是 u8"的容器——serde 的泛型 impl 看不出它们是字节串。
serde_bytes 的作用:Serialize 侧,&[u8] / Vec<u8> 默认按 [T] / Vec<T> 序列化成 seq(JSON 里就是 [1, 2, 3]);Deserialize 侧,Vec<u8> 默认按元素逐个收集。serde_bytes 提供的 serialize / deserialize 函数和 Bytes / ByteBuf 类型把两侧都切到 serialize_bytes / deserialize_bytes / deserialize_byte_buf,让字节串语义跨格式统一。
15.10 借用的嵌套:Vec<&'de str>
能否 Vec<&'de str>?——可以,但 Vec 本身必须分配(它在堆上),只有 &str 元素可以借用。注意 Vec<&'a str> 不在 15.5 节的隐式借用白名单里,字段上必须写 #[serde(borrow)],否则报 lifetime may not live long enough(实测)。
生成代码类似:
let mut __field0: Option<Vec<&'a str>> = None;
__field0 = Some(__map.next_value::<Vec<&'a str>>()?);
Vec<&'de str>::deserialize 做的事:
deserialize_seq开始- 每个元素按
&'de str反序列化(必须 visit_borrowed_str 成功) - 所有元素推到新分配的 Vec 里
- 完成
Vec 分配但元素零拷贝——对很多日志/配置场景已经够好。
如果想连 Vec 都零拷贝?serde 只给 &'a [u8] 提供了借用 impl(serde_core/src/de/impls.rs:765),没有泛型的 &'de [T]——只能自己写 Deserialize,且格式要支持连续布局(JSON 不支持)。实际应用中很少用。
15.11 零拷贝反序列化的性能意义
本节数据来自 2026-04-20 本地实测,非估算。测试条件:
- 机器:macOS Darwin 23.6.0(Intel x86_64)、rustc 1.89.0
- 版本:serde 1.0.228、serde_json 1.0.149
- JSON:10,000 条日志记录(5 个字符串字段 × 平均 ~20 字节),总大小 2.06 MB
- 计时:criterion 0.5,每组 100 次采样
- 分配:自定义 GlobalAlloc(
std::alloc::GlobalAlloc)逐次计数
| 策略 | 反序列化时间(中位) | 分配次数 | 总分配字节 |
|---|---|---|---|
全部 String |
5.68 ms | 50,013 | 5.22 MB |
全部 &'de str |
2.25 ms | 13 | 2.50 MB |
全部 #[serde(borrow)] Cow<'de, str> |
2.73 ms | 13 | 3.75 MB |
关键观察:
- 借用版本快 2.5 倍(
5.68 / 2.25 ≈ 2.52)。 - 分配次数差 3800 倍(
50013 / 13 ≈ 3847)——String 每个字段都 alloc 一个 String(5 个字段 × 10k 行 = 50k 次);borrowed 只有 Vec 本身的扩容等 13 次。 - Cow 和 &str 分配次数一样(13)——因为这份 JSON 没有转义字符,Cow 全走 Borrowed 路径。Cow 总字节数多 1.25MB 是因为
Cow<str>占 24 字节(3 个字,判别值借String容量字段的 niche 存放,不另占空间),比&str(16 字节)大,Vec<Log>每个元素随之变大。 - Cow 比 &str 略慢——本次测量慢 21%(
2.73 / 2.25 ≈ 1.21,上述 macOS / rustc 1.89.0 环境);在 Linux / rustc 1.96.0 上复测只慢 0–8%。都是借用,差别来自 Cow 的 match 和构造 Borrowed variant 的轻微开销,幅度随环境浮动,不宜当成固定比例。
零拷贝版本快 2.5 倍、分配次数降到原来的 1/3847。对日志处理、API 网关这种"解析一次就丢"的场景,省下的 CPU 和分配都是真金白银——尤其是分配压力(减少 GC 式压力、减少内存碎片)。
复现方式——按下述方法自行复现(本节数字是一次示例测量,换机器、换输入形状后绝对值会变,但量级关系稳定)。用 criterion 0.5 写一个 borrow_bench 基准,三组分别把 5 个字段声明为 String / &'de str / #[serde(borrow)] Cow<'de, str>(不加 borrow 的 Cow 每个字段都会分配),对同一份 10,000 条、约 2 MB 的 JSON 调 serde_json::from_str::<Vec<Log>>;分配计数用一个包装 std::alloc::System 的 GlobalAlloc,在 alloc 里原子递增计数器,跑一次解析后打印:
# 时间基准
cargo bench --bench borrow_bench
# 分配计数
cargo run --release --bin alloc_count
但零拷贝不是银弹:
- 字段要保留超过输入存活期 → 必须复制到 String
- 流式输入(from_reader)→ 无法借用
- 跨线程传递 → 生命周期绑定难处理
所以实际 API 设计常用 Cow——在能借用时借用、不能时复制。
15.12 常见错误与调试
错误 1:
error[E0106]: missing lifetime specifier
在 struct User { name: &str } 里 &str 没生命周期。改成 User<'a> { name: &'a str }。
错误 2:
error: lifetime may not live long enough
...
= help: consider adding the following bound: `'de: 'a`
生命周期关系不对:字段类型带 'a,但 impl 头上没有 'de: 'a(比如 Vec<&'a str>、Box<&'a str> 这类不在隐式借用白名单里的字段)。不要手改生成代码,在字段上加 #[serde(borrow)] 让 serde_derive 加上这条约束。
错误 3:
invalid type: string "a\nb", expected a borrowed string at line 1 column 15
运行时错误(serde_json 1.0.149 实测,输入 {"name": "a\nb"})。原因:字符串里有转义,serde_json 只能先解码到 scratch 缓冲再调 visit_str,&str 借不到原输入。解决方法:改用 String 或 #[serde(borrow)] Cow<str>。(from_reader 不会走到这一步——它在编译期就拒绝了,见错误 4。)
错误 4:
error: implementation of `Deserialize` is not general enough
试图把 &str(或含借用字段的结构体)交给要求 DeserializeOwned 的 API(from_reader、fn load<T: DeserializeOwned> 之类)。它只对某个特定的 'de 实现了 Deserialize,满足不了 for<'de>。改用 String。
15.13 和本系列其他专栏的关联
生命周期和借用是 Rust 独有的。理解 Serde 的生命周期设计能深化对 Rust 本身的认知:
- 《Rust 编译器与运行时揭秘》专栏第 4 章"生命周期:编译器如何推断引用的有效范围"是本章最重要的前置——它解释了编译器如何从代码推导生命周期关系。Serde 的
'de: 'a这种约束不是 serde 发明的,是 Rust 基础语法。读过那一章再看本章,所有生命周期约束都有"家"。 - 《Tokio 源码深度解析》专栏第 12 章"异步 Mutex / RwLock / Semaphore"里的
MutexGuard<'_, T>和本章的&'de str是同一类模式——"持有一个短期借用"。两者都用生命周期编码访问时间约束。 - 《Rust 编译器与运行时揭秘》专栏第 5 章"内存布局:编译器如何排列数据"讨论过 Rust 数据的内存表示——你会理解为什么
&'de str不需要分配而String需要。
15.14 本章小结
Serde 的借用反序列化把"零拷贝"写进了类型系统。关键机制:
Deserialize<'de>的'de:Deserializer 输入数据的存活生命周期。- 三个 visit 方法分工:
visit_str(短期借用→复制)、visit_borrowed_str('de借用→保留)、visit_string(拥有→move)。 &'de str的 Visitor 只实现visit_borrowed_str——强制零拷贝,借不到就运行时报 invalid_type;from_reader这类要求DeserializeOwned的入口则在编译期就拒绝。#[serde(borrow)] Cow<'de, str>三路都实现——最灵活,性能自适应。#[serde(borrow)]让serde_derive生成带借用约束的 impl。- DeserializeOwned 是
for<'de> Deserialize<'de>——限制"不借用"。
实际工程建议:
- 高性能解析(日志/网关):用
&'de str强制零拷贝(输入里带转义的字符串会报错)。 - 通用数据类型:用
#[serde(borrow)] Cow<'de, str>,自适应。 - 跨线程或长期持有:用
String,接受复制代价。
第 16 章继续高阶主题——#[serde(with)]、remote、flatten 三个最复杂的属性。它们在源码里触发完全不同的代码生成路径,是 Serde 扩展性的来源。
动手实验
- 性能对比:用 criterion 写一个 benchmark,比较
Stringvs&'de strvsCow<'de, str>反序列化 1MB JSON 的时间和分配次数。 - 错误路径:写一个 struct 包含
&'de str,尝试用serde_json::from_reader反序列化,观察编译错误。 - 手写 Deserialize:不用 derive,手写一个
impl<'de: 'a, 'a> Deserialize<'de> for UserBorrowed<'a>——对照第 13 章理解 derive 宏到底在做什么。 - 思考题:为什么 Rust 没让
'de成为一个"特殊"生命周期(比如'static那种)?它是一个普通泛型参数意味着什么?
延伸阅读
- Serde "Understanding deserializer lifetimes":生命周期故事的权威版本。
- Serde "Borrowing data in a derived impl":专门讨论
#[serde(borrow)]的文档。 - serde_bytes 仓库:字节串借用的实现。
- 《Rust 编译器与运行时揭秘》专栏第 4 章:"生命周期:编译器如何推断引用的有效范围",理解生命周期从编译器视角。
- 《Tokio 源码深度解析》专栏第 12 章:
MutexGuard<'_, T>和&'de str的相似性——都是"短期借用"。
15.15 源码对照:&'a str 的 Deserialize impl(impls.rs:706-737)
打开 serde 1.0.228 的源码 serde_core/src/de/impls.rs:706-737——&'a str 的 Deserialize 实现总共 32 行(下面把源码里的 where 子句收进了尖括号、去掉了空行,逻辑与源码一致):
struct StrVisitor;
impl<'a> Visitor<'a> for StrVisitor {
type Value = &'a str;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a borrowed string")
}
fn visit_borrowed_str<E: Error>(self, v: &'a str) -> Result<Self::Value, E> {
Ok(v) // so easy
}
fn visit_borrowed_bytes<E: Error>(self, v: &'a [u8]) -> Result<Self::Value, E> {
str::from_utf8(v).map_err(|_| Error::invalid_value(Unexpected::Bytes(v), &self))
}
}
impl<'de: 'a, 'a> Deserialize<'de> for &'a str {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(StrVisitor)
}
}
五处值得逐行拆——
1——impl<'a> Visitor<'a>——Visitor 本身就用 'a 作生命周期——类型级别强制"只产出&'a str"。
2——只有 visit_borrowed_str 和 visit_borrowed_bytes——没有 visit_str——"非借用数据"一来就落到 visit_str 的默认实现,运行时返回 invalid_type 错误。
3——Ok(v) // so easy 的注释——源码里的原话——零拷贝反序列化的"核心逻辑" 就是一句 Ok(v)——真 0 开销。
4——visit_borrowed_bytes 的额外 UTF-8 校验——把字节数组解释为 str 时要验 utf8——失败返回 Error::invalid_value。
5——impl<'de: 'a, 'a>——"'de outlives 'a" 的生命周期约束——要求 Deserializer 的输入数据至少活 'a 长——这就是"借用数据必须能活得足够久" 的编译期表达。
15.16 'de: 'a 约束的"直觉理解"
这个 'de: 'a(读作 "'de outlives 'a")是 Rust 生命周期里最常被误解的语法——用一个实际例子讲透:
fn parse<'buf>(json: &'buf str) -> MyStruct<'buf> {
serde_json::from_str::<MyStruct<'buf>>(json).unwrap()
}
#[derive(Deserialize)]
struct MyStruct<'a> {
name: &'a str, // 必须满足 'buf: 'a
}
编译器推导——
json: &'buf str—— buffer 活'bufMyStruct<'a>—— MyStruct 借用'a- serde 反序列化时从
'buf借出'a——需要'buf: 'a
如果没这个约束——&'a str 能比 'buf 活得更久 → buffer 已被 drop 但 &str 还在 → 悬挂引用。
所以 'de: 'a——是"零拷贝安全" 的静态保证——编译期保证运行时不悬挂。
15.17 为什么 Visitor<'a> 的 'a 出现在 trait 定义上
Visitor<'a> 的 'a 参数——让 visit_borrowed_str 的参数 &'de str 能"绑定到 trait 参数"——不是方法级生命周期——而是"整个 Visitor 对这段 data 的访问能力"绑定在同一 'de 上。
关键区别——
- 方法级
'a——每次调用独立、不相关 - trait 级
'de——整个 Visitor 都"基于同一段 input buffer" 操作——类型系统保证一致性
这个设计——让 Visitor 能在"多次 visit_* 调用" 之间保留生命周期追踪——比如 visit_seq 里连续 visit_borrowed_str 多个元素——全部指向同一 'de buffer。
15.18 borrow_cow_str 私有函数——Cow<'de, str> 的内部实现
serde 有个私有函数 serde/src/private/de.rs:64-135 的 borrow_cow_str——#[serde(borrow)] Cow<'de, str> 反序列化的核心(15.7 节已经看过它的 visitor)。R: From<Cow<'a, str>> 约束——让这段代码既能直接产出 Cow<'a, str>、也能产出任何实现了 From<Cow<str>> 的用户类型——通过 trait bounds 实现代码复用;字节版本 borrow_cow_bytes(:138)同理。
六个 visit 方法全实现——字符串三个对应本章 §15.3 讨论的三种场景,字节三个(visit_bytes / visit_borrowed_bytes / visit_byte_buf)各做一次 UTF-8 校验后走同样的三条路:
visit_str——输入是临时&str(不是'de借用)——必须.to_owned()得到Cow::Ownedvisit_borrowed_str——输入是&'de str——直接包成Cow::Borrowed(v)visit_string——输入是 owned String——直接包成Cow::Owned(v)
三条路产出的都是 Cow<'a, str>,最后由 deserialize_str(CowStrVisitor).map(From::from) 统一转成 R。
这就是"零拷贝 + 灵活性" 同时做到的底层原理——serde 的精华就在这几十行里。
15.19 serde_bytes 为什么存在
Rust 的 &[u8] 和 Vec<u8>——原本在 serde 里会被当"sequence of u8" 处理——逐字节调用,在二进制格式里低效。
serde_bytes crate 就是为了解决这个——提供 serde_bytes::ByteBuf、&serde_bytes::Bytes——让 serde 把它们当"one-shot byte string" 处理(bincode 直接序列化为原始字节)。
差别在哪——
Vec<u8>通用路径——每个字节都是一次serialize_element/next_element调用(在 JSON 里两条路径输出一样,都是[1, 2, 3, ...]整数数组——serde_json 的serialize_bytes也走write_byte_array,src/ser.rs:195-199)ByteBuf专用路径——一次serialize_bytes/deserialize_byte_buf;bincode 等二进制格式直接 memcpy 原始字节
字节数越多差距越大——serde_bytes 是二进制 payload 场景的"必选项"。
15.20 DeserializeOwned trait——"不借用" 的 alias
serde::de::DeserializeOwned 是一个 trait alias(serde_core/src/de/mod.rs:632-633,整个定义就是这两行):
pub trait DeserializeOwned: for<'de> Deserialize<'de> {}
impl<T> DeserializeOwned for T where T: for<'de> Deserialize<'de> {}
一行 trait、一行 blanket impl——就这 2 行。
含义——对于T、如果它对任意'de 都能 Deserialize(for<'de> 即 higher-ranked trait bound)——说明 T 根本不依赖 'de——是"拥有所有数据" 的类型。
使用场景——
- 泛型函数要接收"拥有型" T:
fn load<T: DeserializeOwned>(...) -> T - 跨线程传递 T——
T: Send + DeserializeOwned的组合 - JSON 文件读入(
serde_json::from_reader的签名把T约束成de::DeserializeOwned,见 serde_json 的src/de.rs:2614-2618——因为 reader 的 buffer 不能出函数)
15.21 跨语言对比——其他语言里的"borrow deserialize"
Rust 的 borrow deserialize——业内极少见——对比几大生态:
- Go
encoding/json——永远是 copy、没有 zero-copy——因为 Go 有 GC、借用的意义小 - Java Jackson——永远 copy——对象模型就是"拥有"
- Python
json.loads——永远 copy——动态类型 + 引用计数 - C++ RapidJSON——有 in-situ parsing("原地修改 buffer、保留 pointer")——和 serde borrow 最像、但没类型系统保护
Rust 的独特价值——零拷贝 + 编译期安全保证——两者兼得。
15.22 性能数据:真实 benchmark
本章唯一一组实测数字在 §15.11(2.06 MB JSON、10,000 条记录)——这里只重述结论:
| 方法 | 耗时(中位) | 分配次数 |
|---|---|---|
String 版本 |
5.68 ms | 50,013 |
&'de str 版本 |
2.25 ms | 13 |
#[serde(borrow)] Cow<'de, str> 版本 |
2.73 ms | 13 |
三组对比——
String→&'de str——速度约 2.5× + 分配从五万次降到十几次&'de str→Cow——速度略慢(两次测量分别慢 21% 和 0–8%,环境见 §15.11)、但灵活性大- 总结——性能敏感用
&'de str、通用场景用Cow
15.23 #[serde(borrow)] 的编译器魔法
#[serde(borrow)] 是 #[derive(Deserialize)] 的辅助属性(helper attribute,不是独立的 attribute macro)——作用是告诉 derive 宏生成带生命周期约束的 impl:
#[derive(Deserialize)]
struct User<'a> {
#[serde(borrow)]
name: &'a str,
#[serde(borrow)]
bio: Cow<'a, str>,
}
derive 宏生成的代码(简化)——
impl<'de: 'a, 'a> Deserialize<'de> for User<'a> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct __Visitor<'de: 'a, 'a> { marker: PhantomData<User<'a>>, lifetime: PhantomData<&'de ()> }
impl<'de: 'a, 'a> Visitor<'de> for __Visitor<'de, 'a> { ... }
deserializer.deserialize_struct("User", FIELDS, __Visitor { marker: PhantomData, lifetime: PhantomData })
}
}
关键点——impl<'de: 'a, 'a> 自动加上——用户不用手写生命周期约束——derive 宏静默处理。
没有 #[serde(borrow)]——&'a str 这类白名单形态(§15.5)照样隐式借用;Cow<'a, str> 则退化成永远 Cow::Owned——能编译、但零拷贝悄悄没了。上面例子里 name 上的 #[serde(borrow)] 其实可以省,bio 上的不能。
15.24 一个假想的生产事故
设想一个高性能日志解析系统——用 serde + &'de str 做零拷贝 JSON 反序列化——下面这条路是很多人都会走一遍的"悬挂引用"弯路(幸好 Rust 会拦):
场景——
- 线程 A:
let json = read_from_kafka(); let log: LogEntry<'_> = serde_json::from_str(&json)?; - 线程 A 想把
log发给线程 B - 编译错误——
LogEntry有&'de str字段、不能跨json的作用域
尝试"修复"——用 unsafe:
let log: LogEntry<'static> = unsafe { std::mem::transmute(log) };
channel.send(log); // 看似能工作
结果——未定义行为(可能崩溃,也可能悄悄读到垃圾数据)——因为线程 B 在用 log.name、线程 A 里 json 已经释放——典型的 use-after-free。
正确修复——要么 .to_owned() 拷贝一份、要么把 json buffer 也发到 B 线程(用 Arc<String> 共享)。
教训——&'de str 的性能红利有代价——不能 'static 化、不能随便跨线程——静态类型系统已经警告你、别用 unsafe 绕过。
15.25 deserialize_str 和 deserialize_any 的语义差异
serde 的 Deserializer trait 提供了两类 deserialize_* 方法——语义完全不同:
deserialize_str(visitor) —— 明确类型请求——"我需要一个 str、请以 str 调 visitor 的对应方法"——如果数据不是 str、反序列化失败。
deserialize_any(visitor) —— 自描述请求——"按 data 实际类型调 visitor 的对应方法"——serde_json 里 null 调 visit_unit、42 调 visit_u64(负数才是 visit_i64)、"hi" 调 visit_borrowed_str / visit_str(serde_json 的 src/de.rs:1405-1430)。
为什么要两种——
- 自描述格式(JSON、YAML)——两者都支持
- 非自描述格式(bincode、postcard)——只支持
deserialize_str、不支持deserialize_any——因为 wire 上没有"type tag"
设计影响——如果你的 Deserialize impl 只用 deserialize_any——bincode 这类非自描述格式没法支持它,会在运行时拒绝(怎么拒绝由各格式 crate 自己决定,见第 4 章)——必须显式 deserialize_str。
本章主题 &'de str——走的是 deserialize_str 路径——任何格式都兼容。
15.26 Visitor::expecting 的错误信息工程
每个 Visitor 实现都有 fn expecting(&self, formatter) 方法——serde 错误信息的根源:
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a borrowed string")
}
当反序列化失败——serde 拼出这样的错误:
invalid type: integer `42`, expected a borrowed string at line 5 column 10
expected a borrowed string 就是 expecting 返回的字符串——直接给用户。
工程建议——自定义 Deserialize 时、expecting 要写人话——"a valid email address" 比 "EmailVisitor" 有用得多——用户 debug 时直接知道问题。
15.27 真实 JSON parser 的内部:serde_json 的 SliceRead vs StrRead vs IoRead
serde_json 对"输入源"的抽象分三种 Read——每种对借用支持不同:
SliceRead<'a>——输入&'a [u8]——无转义的字符串直接返回Reference::Borrowed、走visit_borrowed_*(serde_json 的src/read.rs:514-519),有转义才退回Reference::CopiedStrRead<'a>——输入&'a str——同上、UTF-8 已验证IoRead<R: io::Read>——输入任意R: io::Read(静态分发,不是 trait object)——只能visit_str(字符串逐字节读进 scratch 缓冲,parse_str永远返回Reference::Copied,serde_json 的src/read.rs:335-338)、无法 borrow
用法对照——
serde_json::from_slice::<MyStruct<'a>>(bytes)—— 可以借用serde_json::from_str::<MyStruct<'a>>(s)—— 可以借用serde_json::from_reader::<_, MyStruct<'a>>(file)—— 编译错误(serde_json 的src/de.rs:2617直接要求T: de::DeserializeOwned,MyStruct<'a>满足不了)——换成MyStruct<'static>也不行(它只实现了Deserialize<'static>,不是for<'de>),必须改用不含借用字段的拥有型结构体
这三种 Read 的存在——完美映射本章§15.20 的"DeserializeOwned vs 'de: 'a" 二分——不是概念上的、是代码实现层面的分离。
15.28 一个"借用但意外拥有"的 trap
Rust 里 String 可以 Deref<Target = str> 到 &str——所以 Cow<'de, str>::Owned(String) 也能"读起来像"借用的:
let data: Cow<'de, str> = ...;
let s: &str = &data; // deref 到 &str、看起来是借用
println!("{}", s); // 能用
但——如果 data 是 Cow::Owned(String)、s 借用的是 String 内部的 heap——当 data drop 时 heap 被释放、s 悬挂——Rust 编译器当然会阻止。
为什么说是 trap——初学者会以为"Cow borrow 就一定不分配"——实际 runtime 可能分配 String(visit_str 路径)——"字面上借用、物理上拥有"。
辨别方法——
match &data {
Cow::Borrowed(_) => println!("zero-copy!"),
Cow::Owned(_) => println!("allocation happened!"),
}
生产建议——用 Cow 时、log 一下 Borrowed vs Owned 命中率——如果 Owned 占多、说明 data format 不支持 borrow(比如 JSON 有 escape char \n、必须 unescape → 必须新分配)——这时改用 String 更直接。
15.29 \n 转义对借用的影响
JSON 里 "line1\nline2" 遇到反斜杠——必须"展开转义"——展开后的"真实字符串" 不在原 buffer 里、必须新分配——不能借用。
serde_json 的行为——
- 无
\的字符串 → 可以visit_borrowed_str(直接借 buffer 里的 slice) - 有
\的字符串 → 只能visit_str(先展开到临时 buffer)
经验上——机器生成的日志 JSON 里转义很少见——借用命中率高。
但 user-generated content(评论、聊天记录)——常有 \n / \"——借用命中率会明显下降,具体比例取决于你的数据,用 §15.28 的 match 统计一下就知道。
这就是"借用的运行时代价"——不是所有数据都能借用——format-level 的约束超出你的控制。
15.30 #[serde(borrow = "'a + 'b")] 的多生命周期
进阶——#[serde(borrow)] 的参数是一个 'a + 'b 形式的生命周期列表,可以只挑其中一部分。serde 自己的测试里就有这个形态(test_suite/tests/test_borrow.rs:183-187,逐字):
#[derive(Deserialize)]
pub struct Wrap<'a, 'b> {
#[serde(borrow = "'b")]
_cows: Cows<'a, 'b>,
}
什么时候需要——类型里有多个独立生命周期、只想让其中一条挂到 'de 上。上面这个例子的意图写在紧随其后的测试函数注释里:// Tests that `'de: 'a` is not required by the Deserialize impl.(test_suite/tests/test_borrow.rs:189)——只借 'b,'a 就不被 'de: 'a 钉住。
两条会被 derive 当场拒绝的写法(都在 test_suite/tests/ui/borrow/ 有 UI 测试兜底):
- 列表里同一条生命周期写两遍 →
duplicate borrowed lifetime `'a`(报错点serde_derive/src/internals/attr.rs:1556,就在解析这个字符串的parse_lit_into_lifetimes里;UI 测试test_suite/tests/ui/borrow/duplicate_lifetime.stderr:1) - 挑了一条字段类型里根本没有的生命周期 →
field `s` does not have lifetime 'b(报错点serde_derive/src/internals/attr.rs:1153-1156,先由borrowable_lifetimes(serde_derive/src/internals/attr.rs:1716)收集字段类型里真实出现的生命周期,再逐条比对;UI 测试test_suite/tests/ui/borrow/wrong_lifetime.stderr:1)
15.31 PhantomData 在 derive 产生的 Visitor 里的用途
上面§15.23 derive 生成的 Visitor 里有 marker: PhantomData<User<'a>> 和 lifetime: PhantomData<&'de ()> 两个字段——PhantomData 是什么?
PhantomData 的语义——"类型系统里假装拥有 T、实际不占空间"——零大小类型(ZST)。
为什么 Visitor 需要 PhantomData——
- Visitor 结构体本身不持有
User<'a>类型的数据 - 但 Visitor 的 trait impl 需要声明"和 User<'a> 生命周期绑定"
- 如果不用 PhantomData、结构体声明了
'de/'a却没有字段用到——报错 E0392(rustc 1.96.0rustc_hir_analysis的诊断模板原文是{$param_def_kind} `{$param_name}` is never used)
PhantomData 的三种形态——
PhantomData<T>—— 像拥有 T(影响 drop check、variance、auto traits)PhantomData<&'a T>—— 像借用 T(影响生命周期)PhantomData<fn() -> T>—— 对 T 协变(covariant),且不参与 drop check;要对 T 不变(invariant)得写PhantomData<fn(T) -> T>,只对 T 逆变(contravariant)是PhantomData<fn(T)>。标准库把这三种封装成core::marker的PhantomCovariant<T>/PhantomInvariant<T>/PhantomContravariant<T>,内部定义就分别是这三个PhantomData(rustc 1.96.0 标准库library/core/src/marker/variance.rs;目前仍是 unstable 特性phantom_variance_markers,稳定版 Rust 用不了)
serde_derive 大量使用 PhantomData——因为生成的 Visitor 要带上用户类型的泛型参数和 'de,自己却不持有对应的数据。
15.32 Variance(型变)的快速讲解
Rust 的生命周期不只有长短、还有"型变方向":
- covariant(协变)——
&'long T能当&'short T用(长的能当短的用) - invariant(不变)——
&'a mut T对T不变:&'a mut Sub不能当&'a mut Super用(注意它对'a仍然是协变的,不变的只是T这一维) - contravariant(逆变)——出现在函数参数位置上的生命周期/类型
serde 的 Deserialize<'de>——trait 的生命周期参数没有型变可言——Deserialize<'de> 和 Deserialize<'x> 是两个不同的 trait,编译器不会自动在它们之间放宽;把它们联系起来的只有 'de: 'a 这样的显式 outlives 约束。
这直接影响"高阶生命周期组合"——如果用户写 fn foo<'a, 'b>(x: &'a str) -> MyStruct<'b> 企图通过 serde 把 'a 借出成 'b——缺少 'a: 'b,编译失败。
理解 variance——Rust 进阶的必修课——本章§15.31+§15.32 是简略概述——完整深度见 Nomicon。
15.33 Box<&'a str> 为什么不加属性就编译不过
用户常困惑——为什么 Box<&'a str> 直接 derive Deserialize 会报错?
// 编译失败:'a 没有和 'de 绑定
#[derive(Deserialize)]
struct Wrapper<'a> {
inner: Box<&'a str>,
}
原因不在 Box——Box<T> 的 impl 只是转发给 T(serde_core/src/de/impls.rs:1954-1958 的 forwarded_impl!)——而在 §15.5 的白名单:Box<&'a str> 既不是 &'a str 也不是 Option<&'a str>,serde_derive 不会隐式给它加 'de: 'a,于是 &'a str: Deserialize<'de> 这个 bound 满足不了。加上 #[serde(borrow)] 就能编译——但"拥有一个借用" 本身是奇怪的类型——应该直接是 &'a str 或 Box<str>(拥有 str)。
常见替代——
- 借用:
&'a str(零拷贝) - 拥有:
String或Box<str>(后者内存更紧凑,不 reserve capacity) - 混合:
Cow<'a, str>
设计启示——serde_derive 对借用宁可保守——compile error 的代价是"用户要思考自己真正想要什么"——长期利大于弊。
15.34 与本专栏其他章、其他专栏的交叉
本章和本专栏其他章的连接——
- 第 13 章 serde_derive——
#[derive(Deserialize)]的宏展开在那章讲、本章关注结果 - 第 4 章 Deserializer 与 Visitor——
'de第一次出现在 trait 签名上的地方、那章讲 - 第 16 章 #[serde(with)]——下章重点、和本章
#[serde(borrow)]形成对比
同样和本系列其他专栏的连接——
- 《Rust 编译器与运行时揭秘》专栏第 4 章——生命周期从 rustc 视角
- 《Tokio 源码深度解析》专栏第 12 章——
MutexGuard<'_, T>和&'de str相似性
"borrow" 这个概念——在 Rust 生态里反复出现——本章是它在序列化领域的具体落地。
15.35 serde_bytes::Bytes vs bytes::Bytes vs &[u8] 三角
字节串场景常被混淆——三个类型:
| 类型 | 归属 | 特性 | serde 支持 |
|---|---|---|---|
&[u8] |
std | 借用切片 | visit_borrowed_bytes |
serde_bytes::Bytes |
serde_bytes | [u8] 的透明包装(unsized,按 &Bytes 使用) |
让 serde 走 byte-string 路径 |
bytes::Bytes |
tokio 生态 | 引用计数缓冲 | bytes crate 开 serde feature 后自带 impl(总是拥有、不借用) |
典型场景——
- 解析 binary log →
&'de [u8]+serde_bytes::Bytes - HTTP body in tokio →
bytes::Bytes(零拷贝共享) - 存到数据库 →
Vec<u8>(拥有)
三者的生命周期哲学——
&[u8]是"纯借用"serde_bytes::Bytes是"借用 + serde 语义"bytes::Bytes是"拥有但共享"(Arc-backed)——更像Arc<[u8]>的优化版
读者在 hyper + serde 场景——常常需要这三者相互转换——本章给的心智模型能帮你快速决定。
15.36 本章的"元教训"
学完本章——三个"元教训"超越 serde 本身:
元教训 1——好的抽象让"简单事简单、复杂事可能"——#[derive(Deserialize)] 一行搞定 90% 场景、但 #[serde(borrow)] + Cow + 'de: 'a 让 10% 复杂场景也能写——这是 API 设计的北斗。
元教训 2——生命周期不是限制、是能力——'de 让你能"零拷贝借用"、而不是"被迫 clone"——理解生命周期、才能释放 Rust 的最大性能。
元教训 3——编译器是你的朋友不是敌人——'de: 'a 约束编译失败时、不是编译器在刁难、是它在保护你——按错误提示修、你的代码会变好。
15.37 一个"自研格式"的例子:如果你要写 Deserializer
本章一直假设"有现成的 Deserializer"——但如果你要给自己的自定义格式写 Deserializer、怎么支持借用?
关键步骤——
- Deserializer struct 持有
input: &'de [u8]或&'de str impl<'de> Deserializer<'de> for MyDeser<'de>——关键是 trait 参数'de- 在
deserialize_str(self, visitor)里(Deserializer的方法都是selfby value,serde_core/src/de/mod.rs:1052)——判断输入是否需要 unescape- 不需要 →
visitor.visit_borrowed_str(slice_of_input)—— 零拷贝 - 需要 → 先到临时 buffer unescape →
visitor.visit_str(&buffer)—— 有拷贝
- 不需要 →
- 支持
&'de T字段——Deserializer 本身的'de参数自然传递
这个 pattern 就是 serde_json 源码的缩影——本章讲的一切"物理借用" 都是 Deserializer 实现者的职责——derive 和 Visitor 只是"声明" 自己能接受借用。
如果你写过一个自定义 Deserializer——你对本章的理解会再深一层——因为你成为了 "能借出 buffer" 的那个人。
15.38 &'de str 在 enum 里的"variant 借用"
enum 里也能用借用字段——规则和 struct 一样按字段算,只是有一条限制:变体级的 #[serde(borrow)] 只允许写在 newtype variant 上(serde_derive/src/internals/attr.rs:876:#[serde(borrow)] may only be used on newtype variants):
#[derive(Deserialize)]
enum Event<'a> {
#[serde(borrow)] // newtype variant 上允许(&'a str 本来就隐式借用,这里其实可省)
Click(&'a str),
Submit {
#[serde(borrow)] // struct variant 只能标在字段上
form: Cow<'a, str>,
timestamp: u64,
},
// 也可以有 owned variant
System(String),
}
derive 推断——borrowed_lifetimes(serde_derive/src/de.rs:292)遍历所有变体的所有字段,把借用到的生命周期合并、整个 enum 的 'de: 'a。
使用时——match event { Event::Click(s) => ..., Event::Submit { form, .. } => ..., Event::System(owned) => ... }——三种 variant 按需用。
这是 Rust 的 ADT + serde 的 borrow 完美结合——比 JSON-RPC style union 的手动 dispatch 简洁得多。
15.39 一段"生命周期 elision rule"的快速回顾
serde 的 Deserialize<'de> 签名看似复杂——其实和 Rust 的通用生命周期 elision 规则一致:
Rule 1——输入参数各自不同生命周期(除非显式统一)——fn foo<'a, 'b>(x: &'a str, y: &'b str) 默认如此。
Rule 2——如果只有一个输入生命周期、输出用它——fn foo<'a>(x: &'a str) -> &'a str 可写为 fn foo(x: &str) -> &str。
Rule 3——&self / &mut self 的生命周期赋给所有输出——fn method(&self) -> &str 输出生命周期绑定到 self。
serde 的 'de——对应 Rule 1 的"显式统一"——因为 Visitor 要保证"同一个 buffer 的多次访问都安全"——必须 explicit。
理解这三条 elision 规则——读 Rust 代码时"看得懂为什么这里不用写生命周期"——本章的 'de 是"必须显式" 的例外情况。
15.40 借用反序列化的三层工程价值
&'de str 借用反序列化的总价值——三层:
层 1——性能——约 2.5× 速度 + 分配次数从五万次降到十几次(§15.11 benchmark)——高 QPS 系统能省下可观的 CPU 与分配开销。
层 2——内存——大 buffer + 零 String 拷贝——字符串内容不再在堆上另存一份,内存占用接近输入本身,而不是再加一份字符串副本。
层 3——安全——编译器强制"引用不超出 buffer 寿命"——避免 use-after-free + 不使用 unsafe。
这三层合起来——是 Rust 在这类场景里相对 GC 语言的优势所在。
15.41 Cow::Borrowed 优化在 serde_yaml 的实战
serde_yaml(已停止维护,最后一个版本是 0.9.34+deprecated)只能部分借用——它先把整篇 YAML 解析成事件流,拿到每个标量的解码值后,再用 parse_borrowed_str 比对这个值是否恰好等于输入里对应的那段原文(见 crates.io 上 serde_yaml 0.9.34 的源码):plain 标量和单/双引号标量能对上就调 visit_borrowed_str,对不上(比如带转义)或是 | / > 这种 block scalar 就退回 visit_str。实测 #[serde(borrow)] Cow<'de, str> 读 name: alice、name: 'alice' 得到 Cow::Borrowed,读 name: "a\nb" 和 block scalar 得到 Cow::Owned。YAML 的多行折叠、block scalar 这些语法决定了输出字符串常常不是输入 buffer 的一段切片,格式层面就没法借用。
工程启示——零拷贝是 Deserializer 实现者给的,不是 serde 给的——选 format 前先看它的 deserialize_str 调的是哪个 visit 方法、别盲信 'de 的存在。
需要借用的场景——serde_json 的借用路径最直接:从 &str / &[u8] 解析时,无转义的字符串一律借出(§15.27)。
15.42 simd-json 的"极速解析"
simd-json crate(独立于 serde_json)——用 SIMD 指令做第一遍结构扫描——在大文档上通常明显快于 serde_json,具体倍数取决于数据和 CPU。
和 serde 的关系——
simd-json可选地实现Deserializertrait- 与 serde
Deserialize兼容 - 仍然支持
&'de str借用(SIMD parse 完保留 buffer slice)
为什么没取代 serde_json——
simd-json要求输入是&mut [u8](in-situ parsing)——不是不可变借用- 依赖特定 CPU 指令集、
unsafe面更大 - 生态依赖
serde_json::Value类型——迁移成本
现状——simd-json 在"极致性能" 场景被采用、serde_json 仍是默认——两者并存。
15.43 一个 #[serde(borrow)] 常见误用
用户常犯的错误——
#[derive(Deserialize)]
struct Config {
#[serde(borrow)] // ❌ Config 没有 'a 参数、borrow 无意义
name: String, // name 是 String、没借用
}
编译错误——field `name` has no lifetimes to borrow(serde_derive/src/internals/attr.rs:1724)——#[serde(borrow)] 只能用在类型里带生命周期的字段上——String / Vec 等 owned 类型不适用。
正确用法——只在 &'a str / Cow<'a, T> / &'a [u8] 等借用字段上加。
这是 serde_derive 的"健壮性"——不让用户加上"无意义的 borrow"——editor 立刻提示。
15.44 "impls.rs 3173 行"——整个文件的结构
前面引用过 serde_core/src/de/impls.rs 的几处代码——这个文件在 1.0.228 上是 3173 行——是 serde 的"标准库类型 deserialize 大全"。(仓库里 serde/src/core 是指向 serde_core/src 的符号链接,所以 serde/src/core/de/impls.rs 和它是同一个文件。)
它是按类型分段排的,段与段之间用一行 ////…//// 隔开——照着这张图翻,不用从头读:
| 行号 | 内容 |
|---|---|
| 12-78 | () 与 bool——最简单的两个,适合当读源码的入口 |
| 79-534 | 数值。impl_deserialize_num! 配合 num_self! / int_to_int! 等辅助宏铺开 i8…u64、f32/f64 与 NonZero 家族,456 行全是宏定义和宏调用,没有一处手写 impl |
| 535-867 | 字符与字符串:char、String、&'a str(706-737,本章主角)、&'a [u8]、CString |
| 868-975 | Option 与 PhantomData |
| 976-1498 | 序列:seq_impl! 在 serde_core/src/de/impls.rs:1076/1087/1098/1109/1120 展开五次——BinaryHeap / BTreeSet / LinkedList / HashSet / VecDeque;Vec<T> 不在其中——它在 serde_core/src/de/impls.rs:1135 有一份单独手写的 impl,配一个自己的 VecVisitor(serde_core/src/de/impls.rs:1143)。再往后是定长数组 [T; N] 与 tuple_impls! 的 1..16 元组 |
| 1499-1572 | 映射:map_impl! 展开 BTreeMap 与 HashMap,两处调用 |
| 1573-1951 | 系统类型:网络地址(IpAddr / Ipv4Addr / Ipv6Addr / SocketAddr 及 V4、V6)、Path / PathBuf、OsString |
| 1952-2118 | 转发与容器包装:Box<T> / Box<[T]> / Box<str> / Box<OsStr> 的 forwarded_impl!、Rc / Arc / Weak(rc feature 门控)、Cell / RefCell / Mutex / RwLock |
| 2119-2412 | 时间:Duration、SystemTime |
| 2413-3173 | 区间与其余:Range 四件套(Range / RangeInclusive / RangeFrom / RangeTo)、Bound、Result(result feature 门控)、Wrapping |
实测构成:显式 impl ... Deserialize<'de> for ... 31 处、Visitor 结构体 16 个,另有 seq_impl! 5 处、map_impl! 2 处、forwarded_impl! 10 处(散布全文:Box<CStr>、Reverse、Box<Path>、四种 Box、RefCell、Mutex、RwLock)——大部分类型不是一个一个手写的,而是宏铺开的。这正是这个文件能用 3173 行覆盖整个标准库的原因。
对用户的价值——serde 自带"几乎所有常见 Rust 类型" 的 Deserialize 支持——你 derive 一个 struct、里面用到的所有类型都已经有 Deserialize impl——不用自己写。
这 3173 行是 serde 的"冰山水下"——上层 API 只看到 #[derive(Deserialize)] 一行、下面是这 3173 行的支撑。
15.45 读源码的"推荐路径"
如果读者想读 serde 源码——给一个分层路径:
Level 1——入门——
serde_core/src/de/mod.rs—— trait 定义(Deserialize / Deserializer / Visitor;全文 2392 行,大半是文档注释)serde_core/src/de/impls.rs:706-737—— 本章讲的&strimpl
Level 2——进阶——
serde_core/src/de/impls.rs—— 看几个类型的 impl 模式serde/src/private/de.rs—— borrow_cow_str 等私有工具serde_core/src/de/value.rs—— Deserializer 的基础实现
Level 3——深入——
serde_derive/src/de.rs—— derive 宏展开- serde_json 的
src/read.rs—— 看 SliceRead / IoRead 实现
15.46 String vs &str 的反序列化"语义对比表"
本章讨论了三种字符串类型——一张对比总结表:
| 维度 | String |
&'de str |
Cow<'de, str> |
|---|---|---|---|
| 所有权 | 拥有 | 借用 | 二者之一 |
| 反序列化路径 | visit_string + visit_str | 仅 visit_borrowed_str(+ visit_borrowed_bytes) | 三个都实现(需 #[serde(borrow)]) |
| 分配次数 | 每个字段 1 次 | 0 | 看命中 |
| 可跨线程 | 是 | 受 'de 限制(需 scoped thread 或整个 buffer 一起传) |
同左 |
DeserializeOwned |
是 | 否 | 否 |
| 可存入长期 struct | 是 | 否 | 否 |
| 性能(§15.11,2 MB JSON) | 5.68 ms | 2.25 ms | 2.73 ms |
| 代码复杂度 | 最简 | 需 'a 参数 | 需 'a 参数 |
| 最适合场景 | 通用 / 跨线程 | 整段在内存里的日志 / 网关解析 | 平衡性能与灵活 |
15.47 "有关 serde 生命周期的 10 个快问快答"
Q1——Deserialize<'de> 的 'de 可以是 'static 吗?—— 可以、意味着输入 buffer 活到程序结束。但 Deserialize<'static> 比 DeserializeOwned 弱:&'static str 实现前者、不实现后者(第 13 章 BorrowedLifetimes::Static 就是这种 impl)。
Q2——'de 能不能省略不写?—— 不能、trait 参数必须显式。
Q3——一个 struct 有多个借用字段、生命周期必须一样吗?—— 不必、struct S<'a, 'b> { a: &'a str, b: &'b str } 直接 derive 就行,生成的是 impl<'de: 'a + 'b, 'a, 'b>(实测)。
Q4——Vec<&'de str> 能 Deserialize 吗?—— 能、Vec 拥有 element、每个 element 是借用——整体可借用、不需要 Vec 本身借用;derive 时字段要加 #[serde(borrow)](§15.10)。
Q5——HashMap<&'de str, i32> 呢?—— 能、&str 是 Hash + Eq 的、借用 key 完全没问题——同样要加 #[serde(borrow)],而且整个 map 都被 'de 钉住、不能比输入活得久。
Q6——能不能序列化后再反序列化成借用?—— 能、只要 Serialize 输出的 buffer 还在作用域内。
Q7——Option<&'de str> 和 Option<Cow<'de, str>> 哪个更好?—— 看场景:前者只要零拷贝、后者要适应 format 差异。
Q8——Debug 输出时借用字段会泄漏吗?—— 不会、{:?} 只打印值、不影响 lifetime。
Q9——能不能把借用字段转成 Arc?—— 能、Arc<str>: From<&str>——Arc::from(s) 一次分配、比 Arc::new(s.to_string()) 少一层间接。
Q10——Box<str> 和 String 反序列化一样吗?—— 几乎一样、但 Box<str> 省 capacity 字段、内存更紧凑。
15.48 一段 "用生命周期构建更安全 API" 的工程感想
serde 的 'de 这种做法在 Rust 生态里随处可见——不只是序列化库用:
sqlx——查询结果的"借用 row" 用类似思路tokio::io::BufReader——fill_buf返回&'buf [u8]也是借用nom解析组合子——parser 返回(&'a [u8], Output)——剩余输入借用regex::Captures——match 后的.get(n)返回借用原输入的Match<'h>(.as_str()得到&'h str)
一个共同的设计模式——"把 buffer 的生命周期 'a 作为 API 签名的第一参数"——让用户在编译期知道"什么时候不能再用结果"。
这个模式——不是 serde 发明的、但 serde 是它最广为人知的落地之一——学会它、你写 API 也能更安全。