Serde 元编程

第 15 章 借用反序列化与零拷贝的生命周期魔法

作者 杨艺韬 · 13,781 字 · 发布于 · 更新于

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

关键观察:

  1. 借用版本快 2.5 倍(5.68 / 2.25 ≈ 2.52)。
  2. 分配次数差 3800 倍(50013 / 13 ≈ 3847)——String 每个字段都 alloc 一个 String(5 个字段 × 10k 行 = 50k 次);borrowed 只有 Vec 本身的扩容等 13 次。
  3. Cow 和 &str 分配次数一样(13)——因为这份 JSON 没有转义字符,Cow 全走 Borrowed 路径。Cow 总字节数多 1.25MB 是因为 Cow<str> 占 24 字节(3 个字,判别值借 String 容量字段的 niche 存放,不另占空间),比 &str(16 字节)大,Vec<Log> 每个元素随之变大。
  4. 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 的借用反序列化把"零拷贝"写进了类型系统。关键机制:

  1. Deserialize<'de> 的 'de:Deserializer 输入数据的存活生命周期。
  2. 三个 visit 方法分工:visit_str(短期借用→复制)、visit_borrowed_str('de 借用→保留)、visit_string(拥有→move)。
  3. &'de str 的 Visitor 只实现 visit_borrowed_str——强制零拷贝,借不到就运行时报 invalid_type;from_reader 这类要求 DeserializeOwned 的入口则在编译期就拒绝。
  4. #[serde(borrow)] Cow<'de, str> 三路都实现——最灵活,性能自适应。
  5. #[serde(borrow)] 让 serde_derive 生成带借用约束的 impl。
  6. DeserializeOwned 是 for<'de> Deserialize<'de>——限制"不借用"。

实际工程建议:

  • 高性能解析(日志/网关):用 &'de str 强制零拷贝(输入里带转义的字符串会报错)。
  • 通用数据类型:用 #[serde(borrow)] Cow<'de, str>,自适应。
  • 跨线程或长期持有:用 String,接受复制代价。

第 16 章继续高阶主题——#[serde(with)]、remote、flatten 三个最复杂的属性。它们在源码里触发完全不同的代码生成路径,是 Serde 扩展性的来源。

动手实验

  1. 性能对比:用 criterion 写一个 benchmark,比较 String vs &'de str vs Cow<'de, str> 反序列化 1MB JSON 的时间和分配次数。
  2. 错误路径:写一个 struct 包含 &'de str,尝试用 serde_json::from_reader 反序列化,观察编译错误。
  3. 手写 Deserialize:不用 derive,手写一个 impl<'de: 'a, 'a> Deserialize<'de> for UserBorrowed<'a>——对照第 13 章理解 derive 宏到底在做什么。
  4. 思考题:为什么 Rust 没让 'de 成为一个"特殊"生命周期(比如 'static 那种)?它是一个普通泛型参数意味着什么?

延伸阅读

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 活 'buf
  • MyStruct<'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::Owned
  • visit_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::Copied
  • StrRead<'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.0 rustc_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 的方法都是 self by 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 可选地实现 Deserializer trait
  • 与 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 —— 本章讲的 &str impl

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 也能更安全。