Rust in Everything
基于 serde 1.0.229

生命周期 'de 与借用

主线位置:反序列化主线上的一个参数。一次 from_str 里,'de 从 &'de str 输入出发,经 Deserializer<'de> 传到 Visitor<'de>,最后落在 &'de str 字段上。本页讲这条线上「能不能借」由谁决定、借不成时会怎样。Visitor 的整体机制在 Deserialize、Deserializer 与 Visitor,serde_json 怎么读字符串在 serde_json 的 Deserializer,derive 怎么给生成的 impl 加 'de: 'a 在 泛型与 bound。

flowchart TB
  input["输入 &'de str"] --> dstr[deserialize_str]
  dstr --> parse[parse_str]
  parse -->|没有转义| borrowed["Reference::Borrowed"]
  parse -->|有转义| copied["Reference::Copied"]
  borrowed --> vb[visit_borrowed_str]
  copied --> vs[visit_str]
  vb --> okref["&str 字段:Ok"]
  vs --> errref["&str 字段:报错"]
  vs --> owned["String 字段:拷贝"]

本页的实验都在 M2 Max、macOS、rustc 1.99.0 上跑,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154"。

'de 是输入的生命周期

Deserialize 带一个生命周期参数,文档只有一句话:

/// The `'de` lifetime of this trait is the lifetime of data that may be
/// borrowed by `Self` when deserialized.
// ...
pub trait Deserialize<'de>: Sized {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>;
    // ...
}

'de 不是 Self 的生命周期,而是「输入」的生命周期。Deserializer<'de> 和 Visitor<'de> 带的是同一个 'de:一个 Deserializer<'de> 承诺,它交给 Visitor 的某些数据至少活 'de 那么久。Deserialize<'de> 的实现可以选择借这些数据,也可以不借。

不借的类型对任何 'de 都能实现,比如 impl<'de> Deserialize<'de> for String。借的类型必须要求 'de 比自己借的生命周期长。&str 的实现:

impl<'de: 'a, 'a> Deserialize<'de> for &'a str {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(StrVisitor)
    }
}

'de: 'a 读作「输入至少活得和 &'a str 一样久」。derive 对含 &'a str 字段的结构体生成同样的约束。cargo expand 一个 struct Post<'a> { title: &'a str },关键几行:

    impl<'de: 'a, 'a> _serde::Deserialize<'de> for Post<'a> {
            // ...
            struct __Visitor<'de: 'a, 'a> {
            // ...
                    let mut __field0: _serde::__private229::Option<&'a str> = _serde::__private229::None;
                    // ...
                                    _serde::de::MapAccess::next_value::<&'a str>(&mut __map)?,

调用一侧,serde_json::from_str 把输入的生命周期直接当作 'de:

pub fn from_str<'a, T>(s: &'a str) -> Result<T>
where
    T: de::Deserialize<'a>,

于是 from_str::<Post>(text) 得到的 Post 不能比 text 活得长,这由借用检查器在编译期保证。

能不能借,格式说了算

Visitor 上有三个接收字符串的方法,visit_str、visit_borrowed_str、visit_string,区别只在参数的生命周期和所有权:

方法参数文档的说法默认实现
visit_str&str(无名生命周期)「ephemeral and it may be destroyed after this method returns」Err(invalid_type(Unexpected::Str(v), &self))
visit_borrowed_str&'de str「lives at least as long as the Deserializer」转给 visit_str
visit_stringString「ownership of the string is being given to the Visitor」转给 visit_str,然后丢掉 String

Visitor 不能要求调哪一个。调哪个由 Deserializer 决定,依据是它手里那段字符串实际活多久。两个默认转发让只写了 visit_str 的 Visitor(比如 String 的 StringVisitor)三种都能收;只有真想借的 Visitor 才需要覆盖 visit_borrowed_str。

serde_json 的 deserialize_str 就是这样分派的:

        let value = match peek {
            b'"' => {
                self.eat_char();
                self.scratch.clear();
                match tri!(self.read.parse_str(&mut self.scratch)) {
                    Reference::Borrowed(s) => visitor.visit_borrowed_str(s),
                    Reference::Copied(s) => visitor.visit_str(s),
                }
            }
            _ => Err(self.peek_invalid_type(&visitor)),
        };

Reference 的两个分支带着两个不同的生命周期:

pub enum Reference<'b, 'c, T>
where
    T: ?Sized + 'static,
{
    Borrowed(&'b T),
    Copied(&'c T),
}

parse_str 返回 Reference<'de, 's, str>:Borrowed 指向输入本身,活 'de;Copied 指向 Deserializer 内部的 scratch 缓冲,只活 's,下一次解析字符串时 scratch 会被清空复用。哪一种由输入决定。SliceRead::parse_str_bytes 一路扫到结尾的引号,没遇到过反斜杠(scratch 还是空的)就直接切输入:

                b'"' => {
                    if scratch.is_empty() {
                        // Fast path: return a slice of the raw JSON without any
                        // copying.
                        let borrowed = &self.slice[start..self.index];
                        self.index += 1;
                        return result(self, borrowed).map(Reference::Borrowed);
                    } else {
                        scratch.extend_from_slice(&self.slice[start..self.index]);
                        self.index += 1;
                        return result(self, scratch).map(Reference::Copied);
                    }
                }
                b'\\' => {
                    scratch.extend_from_slice(&self.slice[start..self.index]);
                    self.index += 1;
                    tri!(parse_escape(self, validate, scratch));
                    start = self.index;
                }

有转义时,解码后的字符串在输入里根本不存在(输入里是 \",结果里是 "),只能拼到 scratch 里,返回 Copied。from_str 用的 StrRead::parse_str 委托给同一个函数。从 io::Read 读的 IoRead,它的 parse_str 则一律返回 Copied:

    fn parse_str<'s>(&'s mut self, scratch: &'s mut Vec<u8>) -> Result<Reference<'de, 's, str>> {
        self.parse_str_bytes(scratch, true, as_str)
            .map(Reference::Copied)
    }

serde_json 读字符串的其余细节(skip_to_escape 的快速扫描、UTF-8 校验)在 serde_json 的 Deserializer。

💡TIP
打个比方:从一本借来的书里摘句子

你(Visitor)请图书管理员(Deserializer)把书(输入)里的一句话给你。句子原样印在书页上时,管理员直接指给你看那一行(Reference::Borrowed,visit_borrowed_str),你可以在书归还之前('de)一直指着它。句子里有需要改写的地方(转义)时,管理员只能把改好的句子抄在一张便签上(scratch,Reference::Copied,visit_str),而这张便签下一句话还要用,你看完就得还。想要一直留着,就得自己再抄一份(to_owned)。

比方到这里为止:真实的管理员不会征求你的意见,书页还是便签完全由句子本身决定;而且只有 &str 这种「只要书页」的读者会因为拿到便签而失败。

&str 字段:借不成就报错

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>(self, v: &'a str) -> Result<Self::Value, E>
    where
        E: Error,
    {
        Ok(v) // so easy
    }

    fn visit_borrowed_bytes<E>(self, v: &'a [u8]) -> Result<Self::Value, E>
    where
        E: Error,
    {
        str::from_utf8(v).map_err(|_| Error::invalid_value(Unexpected::Bytes(v), &self))
    }
}

它没有覆盖 visit_str,所以格式一旦调了 visit_str,就走到默认实现,报 invalid_type;expecting 写的 a borrowed string 成了报错的后半句。这不是 serde 的疏忽:visit_str 拿到的 &str 活不过这次调用,StrVisitor 没有任何合法的办法把它变成 &'a str 返回。

src/bin/borrow_str.rs:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Post<'a> {
  title: &'a str,
}

fn main() {
  let plain = r#"{"title":"hello"}"#;
  let escaped = r#"{"title":"say \"hi\""}"#;

  let p: Post = serde_json::from_str(plain).unwrap();
  let inside = plain.as_bytes().as_ptr_range().contains(&p.title.as_ptr());
  println!("plain:   {:?}, points into input: {inside}", p);

  match serde_json::from_str::<Post>(escaped) {
    Ok(p) => println!("escaped: {:?}", p),
    Err(e) => println!("escaped: Err({e})"),
  }
}
plain:   Post { title: "hello" }, points into input: true
escaped: Err(invalid type: string "say \"hi\"", expected a borrowed string at line 1 column 21)
  • 没有转义时,title 的地址落在输入字符串里面,零拷贝。
  • 有转义时,同一个类型、同一个函数,运行期失败。报错里的字符串是解码后的值(Unexpected::Str 用 {:?} 打印,所以引号又被转义了一次);invalid type 和 expected ... 怎么拼起来的在 错误。

所以 &'de str 字段只适合「确定不会有转义」的输入,比如标识符、枚举名、数字串。会出现任意文本的字段,用 String 或下面的 Cow<'a, str>。

Cow:默认不借,加 borrow 才借

Cow 的通用实现 永远产出 Owned:

impl<'de, 'a, T> Deserialize<'de> for Cow<'a, T>
where
    T: ?Sized + ToOwned,
    T::Owned: Deserialize<'de>,
{
    #[inline]
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        T::Owned::deserialize(deserializer).map(Cow::Owned)
    }
}

这里的 'de 和 'a 没有任何关系,所以它不可能借。原因是这个 impl 对所有 T: ToOwned 都成立:对一个任意的 T,serde 不知道「从输入借一个 &'de T」是什么意思;只有 str 和 [u8] 有对应的 visit_borrowed_*。又因为 Rust 没有特化,不能再给 Cow<'a, str> 单独写一个借用的 impl。

借用版本因此放在 derive 里。字段写了 #[serde(borrow)]、类型在语法上像 Cow<'a, str> 时,attr.rs 把这个字段的 deserialize_with 设成 borrow_cow_str:

            // Cow<str> and Cow<[u8]> never borrow by default:
            //
            //     impl<'de, 'a, T: ?Sized> Deserialize<'de> for Cow<'a, T>
            //
            // 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) {
                // ...
                path.segments
                    .push(Ident::new("borrow_cow_str", span).into());
                // ...
                deserialize_with.set_if_none(expr);
            } else if is_cow(&field.ty, is_slice_u8) {
                // ... 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);
        }

borrow_cow_str 在 serde/src/private/de.rs 里,是 derive 运行时的一部分。它的 CowStrVisitor 把六个字符串 / 字节方法都实现了,借来的变 Borrowed,其余变 Owned:

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;

    #[cfg_attr(not(no_diagnostic_namespace), diagnostic::do_not_recommend)]
    impl<'a> Visitor<'a> for CowStrVisitor {
        type Value = Cow<'a, str>;
        // ...
        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
        where
            E: Error,
        {
            Ok(Cow::Owned(v.to_owned()))
        }

        fn visit_borrowed_str<E>(self, v: &'a str) -> Result<Self::Value, E>
        where
            E: Error,
        {
            Ok(Cow::Borrowed(v))
        }

        fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
        where
            E: Error,
        {
            Ok(Cow::Owned(v))
        }
        // ... visit_bytes / visit_borrowed_bytes / visit_byte_buf 先做 UTF-8 检查
    }

    deserializer.deserialize_str(CowStrVisitor).map(From::from)
}

和 StrVisitor 比,差别就是多了 visit_str 和 visit_string:借不成时退回拷贝,而不是报错。展开后的生成代码里,这个字段被包进一个局部的 __DeserializeWith 类型:

                        impl<'de: 'a, 'a> _serde::Deserialize<'de>
                        for __DeserializeWith<'de, 'a> {
                            // ...
                                _serde::__private229::Ok(__DeserializeWith {
                                    value: _serde::__private229::de::borrow_cow_str(
                                        __deserializer,
                                    )?,

src/bin/cow.rs 对比两种写法和两种输入:

use std::borrow::Cow;

use serde::Deserialize;

#[derive(Deserialize)]
struct Plain<'a> {
  title: Cow<'a, str>,
}

#[derive(Deserialize)]
struct Borrowing<'a> {
  #[serde(borrow)]
  title: Cow<'a, str>,
}

fn kind(c: &Cow<str>) -> &'static str {
  match c {
    Cow::Borrowed(_) => "Borrowed",
    Cow::Owned(_) => "Owned",
  }
}

fn main() {
  let plain = r#"{"title":"hello"}"#;
  let escaped = r#"{"title":"say \"hi\""}"#;
  for input in [plain, escaped] {
    let a: Plain = serde_json::from_str(input).unwrap();
    let b: Borrowing = serde_json::from_str(input).unwrap();
    println!("{input:<24} no borrow: {:<8} #[serde(borrow)]: {}", kind(&a.title), kind(&b.title));
  }
  // Cow<str> as a top-level value (not a derived field) uses the plain impl
  let top: Cow<str> = serde_json::from_str(r#""hello""#).unwrap();
  println!("top-level Cow<str>: {}", kind(&top));
}
{"title":"hello"}        no borrow: Owned    #[serde(borrow)]: Borrowed
{"title":"say \"hi\""}   no borrow: Owned    #[serde(borrow)]: Owned
top-level Cow<str>: Owned
  • 不加 borrow,没有转义的输入也拷贝。
  • 加了 borrow,没有转义时借,有转义时拷贝,两种输入都成功。这是 Cow 字段相对 &str 字段的全部好处。
  • 不经过 derive 的 Cow<str>(顶层值、Vec<Cow<str>> 的元素)走通用 impl,总是 Owned。

is_cow 是按语法判断的,它的注释自己列了误判:use std::borrow::Cow as Pig; 之后写 Pig<'a, str> 认不出来。另一个更常见的情况是 Option<Cow<'a, str>>:is_cow 只看最外层的路径名,Option 不是 Cow,于是不会换成 borrow_cow_str。

📝NOTE
没有蠢问题

问: Option<Cow<'a, str>> 字段加了 #[serde(borrow)],编译通过了,是不是就借了?

答: 没有。src/bin/cow_option.rs 用 {"title":"hello"} 跑,结果是 Option<Cow<str>> + borrow: Owned。展开看,生成的 impl 头确实变成了 impl<'de: 'a, 'a> _serde::Deserialize<'de> for Opt<'a>(borrow 把字段里的 'a 收进了约束),但字段本身没有换成 borrow_cow_str,Option<Cow> 走的是 Option 加 Cow 的通用 impl,永远 Owned。结果是付出了 'de: 'a 的约束,却没有得到借用。serde 自己不提供 Option<Cow> 的借用版本;serde_with 3.24.0 的 BorrowCow 可以:字段写 #[serde_as(as = "Option<BorrowCow>")] 加 #[serde(borrow)],src/bin/cow_option_with.rs 实测无转义的输入得到 Borrowed、有转义的得到 Owned。另一个办法是用 Option<&'a str>(is_implicitly_borrowed 认得 Option<&str>),代价是遇到转义报错。

#[serde(borrow = "'a + 'b")] 是给「字段类型带好几个生命周期,只想借其中一部分」用的。不写等号时,borrowable_lifetimes 收集字段类型里出现的所有生命周期;写了等号,就只用列出来的那几个(列了字段里没有的生命周期会报 field `x` does not have lifetime 'c)。这些生命周期的并集成为生成 impl 里 'de 的约束。下面的例子里,Pair<'a, 'b> 自己的 impl 是 impl<'de: 'a + 'b, 'a, 'b>(a: &'a str 隐式借,b: Cow<'b, str> 加了 borrow);外层只借 'a:

#[derive(Deserialize, Debug)]
struct Outer<'a, 'b> {
  #[serde(borrow = "'a")]
  pair: Pair<'a, 'b>,
}

生成的是 impl<'de: 'a, 'a, 'b> _serde::Deserialize<'de> for Outer<'a, 'b>,而 Pair 需要 'de: 'b,于是 rustc 报错:

error: lifetime may not live long enough
  --> src/bin/borrow_some.rs:14:3
   |
12 | #[derive(Deserialize, Debug)]
   |          ----------- lifetime `'de` defined here
13 | struct Outer<'a, 'b> {
   |                  -- lifetime `'b` defined here
14 |   #[serde(borrow = "'a")]
   |   ^ requires that `'de` must outlive `'b`
   |
   = help: consider adding the following bound: `'de: 'b`

error: could not compile `exp-serde-life` (bin "borrow_some") due to 2 previous errors

(「2 previous errors」是两条完全相同的诊断,终端只显示一次;--message-format=json 能看到两条。)改成 #[serde(borrow)] 就生成 impl<'de: 'a + 'b, 'a, 'b>,能编译。注意外层字段 pair 本身必须写 borrow:除了 &str / &[u8] / Option<&str> / Option<&[u8]>,derive 不会自动借别的类型里的生命周期。它怎么把这些生命周期加到 impl 的泛型上,在 泛型与 bound;属性的完整列表在 属性。

📝NOTE
冷知识:'de 和 #[serde(borrow)] 是 1.0.0 才有的

serde 0.9 的 trait 是 pub trait Deserialize: Sized(本机 registry 里的 serde 0.9.15,src/de/mod.rs 第 458 行),没有生命周期参数,Visitor 也没有 visit_borrowed_str,任何字符串都只能拷贝。PR #826(「Add a 'de lifetime to the deserialize traits」)先把 'de 穿过所有 trait,说明里写「This is the boring part of #492 - just plumbing 'de through all the relevant places」,并提到要把生命周期名约定为 'de,因为 forward_to_deserialize! 宏假定了这个名字。PR #837(「Derive for borrowed fields」)接着加了 derive 支持,说明里的例子就是今天的规则:「&str and &[u8] are always implicitly borrowed from the input」「other types require an attribute to opt into borrowing」「can narrow down which lifetimes should be borrowed」,以及「The union of all borrowed lifetimes becomes the bound on the 'de lifetime of the Deserialize impl」,并报告「measured some 10% improvements in realistic use cases」。两个 PR 都合进了 1.0.0,关掉的是 issue #492(「What's serde zero-copy story so far?」)。上面 attr.rs 里那段 Cow<str> and Cow<[u8]> never borrow by default 的注释,就是这套规则在今天的样子。

DeserializeOwned 与 from_reader

DeserializeOwned 只有两行:

pub trait DeserializeOwned: for<'de> Deserialize<'de> {}
impl<T> DeserializeOwned for T where T: for<'de> Deserialize<'de> {}

for<'de> 是高阶 trait bound(HRTB):T 必须对每一个 'de 都实现 Deserialize<'de>。String、u32、不含借用的 derive 类型都满足;&'a str 只对 'de: 'a 的那些 'de 实现,Post<'a> 也一样,所以不满足。

serde_json::from_reader 要的就是它:

pub fn from_reader<R, T>(rdr: R) -> Result<T>
where
    R: crate::io::Read,
    T: de::DeserializeOwned,

对比 from_slice 的 T: de::Deserialize<'a>('a 是输入切片的生命周期)。from_reader 的签名里根本没有一个生命周期可以填进 'de:数据是边读边进 scratch 的,IoRead 永远返回 Copied,没有哪段字节能活过下一次读取。三个入口最后都调 from_trait,它只要求 R: Read<'de> 和 T: Deserialize<'de>,区别全在入口的签名上。

给 from_reader 一个借用的类型,src/bin/reader_str.rs:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Post<'a> {
  title: &'a str,
}

fn main() {
  let bytes = br#"{"title":"hello"}"#;
  let p: Post = serde_json::from_reader(&bytes[..]).unwrap();
  println!("{p:?}");
}
error: implementation of `Deserialize` is not general enough
  --> src/bin/reader_str.rs:10:17
   |
10 |   let p: Post = serde_json::from_reader(&bytes[..]).unwrap();
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `Deserialize` is not general enough
   |
   = note: `Post<'_>` must implement `Deserialize<'0>`, for any lifetime `'0`...
   = note: ...but `Post<'_>` actually implements `Deserialize<'1>`, for some specific lifetime `'1`

error: could not compile `exp-serde-life` (bin "reader_str") due to 1 previous error

报错里没有出现 DeserializeOwned 这个名字,for<'de> 被展开成了两句 note:「for any lifetime '0」就是 HRTB,「for some specific lifetime '1」就是 'de: 'a 约束下的那个 impl。自己写 fn load<T: DeserializeOwned>(text: &str) -> T 再传 Post,得到的是一模一样的三行(src/bin/owned_bound.rs)。

类型根本没有实现 Deserialize 时,HRTB 换一种方式出现。src/bin/no_impl.rs 用一个没有 derive 的 struct Post { title: String } 调 from_reader,输出的开头:

error[E0277]: the trait bound `Post: serde::de::DeserializeOwned` is not satisfied
    --> src/bin/no_impl.rs:6:17
     |
   6 |   let p: Post = serde_json::from_reader(&b"{}"[..]).unwrap();
     |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
     |
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `Post`

第一行的措辞来自 DeserializeOwned 上的 diagnostic::on_unimplemented(message = "the trait bound {Self}: serde::de::DeserializeOwned is not satisfied"),help 里则是 HRTB 的原形。axum 的 Json<T> 要求 T: DeserializeOwned,所以在 handler 里写错时看到的也是这一组(handler 报 not satisfied,Json 提取器)。

反过来,不用 DeserializeOwned 而写 fn load<'a, T: Deserialize<'a>>(path: &str) -> T,在函数里读文件再 from_str(&text),会撞上另一面(src/bin/local_buf.rs):

error[E0597]: `text` does not live long enough
    --> src/bin/local_buf.rs:5:24
     |
   3 | fn load<'a, T: Deserialize<'a>>(path: &str) -> T {
     |         -- lifetime `'a` defined here
   4 |   let text = std::fs::read_to_string(path).unwrap();
     |       ---- binding `text` declared here
   5 |   serde_json::from_str(&text).unwrap()
     |   ---------------------^^^^^-
     |   |                    |
     |   |                    borrowed value does not live long enough
     |   argument requires that `text` is borrowed for `'a`
   6 | }
     | - `text` dropped here while still borrowed

'a 是调用者选的,可能很长;text 在函数结束时就没了。输入在函数内部产生、结果要带出函数时,只能要求 T 不借,也就是 DeserializeOwned。

📝NOTE
没有蠢问题

问: IoRead 内部明明也有一个缓冲,为什么不能从那里借?

答: 那个缓冲(scratch)每读一个字符串就 clear() 一次重新填。parse_str 的返回类型 Reference<'de, 's, str> 把这件事写进了类型:Copied 的生命周期是 's,也就是对 scratch 的这一次可变借用,下一次调用 parse_str 之前它就必须结束。要让 &str 活过整个反序列化,就得保证缓冲在整个过程中既不清空也不扩容(扩容会搬家),那等于先把整个输入读进内存,这正是 from_slice 的做法。from_reader 的文档还提醒:「counter to intuition, this function is usually slower than reading a file completely into memory and then applying from_str or from_slice on it」。

from_reader 的约束是在签名上加的,不是在 IoRead 上。下面这道题绕开签名,直接用 Deserializer::from_reader 构造。

❗IMPORTANT
动脑筋:绕开 from_reader 的签名,&str 能从 IoRead 里借出来吗?

M2 Max、macOS、rustc 1.99.0。IoRead 实现的是 impl<'de, R> Read<'de> for IoRead<R>,对任意 'de 都成立,所以下面这段能编译。猜两行各输出什么:

use serde::Deserialize;

fn show(label: &str, r: Result<&str, serde_json::Error>) {
  match r {
    Ok(s) => println!("{label}: Ok({s:?})"),
    Err(e) => println!("{label}: Err({e})"),
  }
}

fn main() {
  let bytes = br#""hello""#;
  let mut de = serde_json::Deserializer::from_reader(&bytes[..]);
  show("from_reader", <&str>::deserialize(&mut de));

  let mut de = serde_json::Deserializer::from_slice(&bytes[..]);
  show("from_slice ", <&str>::deserialize(&mut de));
}

动脑筋答案

src/bin/reader_manual.rs 的输出:

from_reader: Err(invalid type: string "hello", expected a borrowed string at line 1 column 7)
from_slice : Ok("hello")
  • 第一行:类型检查过了(编译器给 'de 随便选了一个生命周期),但 IoRead::parse_str 只会返回 Copied,deserialize_str 于是调 visit_str,StrVisitor 没有覆盖它,落到默认的 invalid_type。即使输入 "hello" 里没有任何转义,也借不到。
  • 第二行:同样的字节,SliceRead 走快路径返回 Borrowed,visit_borrowed_str 直接把输入里的切片交出去。

所以 from_reader 的 DeserializeOwned 不是为了内存安全(这里没有任何不安全的事情发生),而是把一个必然在运行期失败的组合提前到编译期拒绝。

小结

  • 'de 是输入的生命周期,Deserializer<'de>、Visitor<'de>、Deserialize<'de> 共用它;借用的类型写成 impl<'de: 'a, 'a> Deserialize<'de> for &'a str,derive 对 &'a str 字段生成同样的约束。
  • Visitor 不能要求借。格式按数据实际活多久选 visit_borrowed_str(活 'de)、visit_str(只活这次调用)或 visit_string(转交所有权);后两者默认都转到 visit_str。
  • serde_json 的 parse_str 返回 Reference::Borrowed(输入里的切片)或 Copied(scratch 里的解码结果):SliceRead / StrRead 没有转义时借,有转义时拷贝;IoRead 永远拷贝。
  • &'a str 的 StrVisitor 只实现借来的方法,遇到转义报 invalid type: string "...", expected a borrowed string。
  • Cow<'a, T> 的通用 impl 总是 Owned;字段加 #[serde(borrow)] 后 derive 换成 borrow_cow_str,能借就借、借不成就拷贝。is_cow 按语法认,Option<Cow<'a, str>> 加 borrow 只加约束、不借。
  • #[serde(borrow = "'a + 'b")] 只借列出的生命周期,它们的并集成为 'de 的约束;1.0.0 的 PR #826 / #837 引入了 'de 和这套规则。
  • DeserializeOwned = for<'de> Deserialize<'de>;from_reader 要它,因为签名里没有可借的生命周期。报错里它显示成「implementation of Deserialize is not general enough」或 for<'de> Deserialize<'de> is not implemented。

💬 关联讨论

发起讨论
加载中...