enum 的表示与缓冲
数据模型里的 enum 变体有四种形状(unit、newtype、tuple、struct),序列化一侧各有一个 serialize_*_variant,反序列化一侧统一走 deserialize_enum,都带着变体名和变体下标。JSON 没有 enum,怎么写成 JSON 是 derive 的选择:默认是外部标签,#[serde(tag)]、#[serde(tag, content)]、#[serde(untagged)] 换成另外三种。后三种在反序列化时有一个共同的麻烦:读到决定性的信息(标签,或者「哪个变体能读通」)之前,不知道后面的内容该交给哪个 Visitor。serde 的办法是先把内容原样读进一棵 Content 树,等知道了再从树上重放一遍。这就是缓冲主线。
这一部分五章,前三章各讲一种表示,后两章讲缓冲本身和同样依赖缓冲的 flatten:
- 外部标签:
{"Variant": 内容}或"Variant";生成的visit_enum先用EnumAccess取出变体标识,再按变体形状调VariantAccess的一个方法,内容直接从格式读,不缓冲;rename/alias/#[serde(other)]怎么落到变体标识上。 - 内部标签与相邻标签:
{"t": "Variant", ...字段}把标签和字段放在同一个对象里,TaggedContentVisitor边读边找标签,其余键值全部缓冲;{"t": ..., "c": ...}标签在前时内容直接读,在后时内容先缓冲。 - untagged:没有标签,先
deserialize_any成Content,再按变体顺序逐个用ContentRefDeserializer试,第一个成功的赢,失败时只剩一句data did not match any variant。 - Content:缓冲的代价:
Content<'de>存了什么、丢了什么(类型提示、u128),为什么非自描述格式用不了缓冲。 - flatten:结构体里的
#[serde(flatten)]把未认出的键值缓冲起来,再交给被展开的字段重放;序列化一侧改用serialize_map。
四种表示
src/bin/reprs.rs(M2 Max、macOS、rustc 1.99.0,serde = "=1.0.229"、serde_json = "=1.0.154")用一个宏生成四个只差属性的 enum:
#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: i32,
y: i32,
}
macro_rules! shape {
($name:ident $(, $($attr:tt)+)?) => {
#[derive(Serialize, Deserialize, Debug)]
$(#[serde($($attr)+)])?
enum $name {
Dot,
At(Point),
Rect { w: u32, h: u32 },
}
// ...
};
}
shape!(External);
shape!(Internal, tag = "t");
shape!(Adjacent, tag = "t", content = "c");
shape!(Untagged, untagged);
fn trace<T: for<'de> Deserialize<'de> + std::fmt::Debug>(input: &str) {
println!("-- {input}");
let mut de = serde_json::Deserializer::from_str(input);
let v = T::deserialize(LogDe(&mut de));
println!("=> {v:?}");
}
main 先对 [Dot, At(Point { x: 1, y: 2 }), Rect { w: 3, h: 4 }] 各调一次 serde_json::to_string,再把一个 Rect 交给套了打印包装 LogDe 的 serde_json Deserializer,只打印最外面一层调用(LogDe 抄自一次往返的实验,加了一个 set_max_depth(1) 开关,只打印前一层):
external: ["Dot",{"At":{"x":1,"y":2}},{"Rect":{"w":3,"h":4}}]
internal: [{"t":"Dot"},{"t":"At","x":1,"y":2},{"t":"Rect","w":3,"h":4}]
adjacent: [{"t":"Dot"},{"t":"At","c":{"x":1,"y":2}},{"t":"Rect","c":{"w":3,"h":4}}]
untagged: [null,{"x":1,"y":2},{"w":3,"h":4}]
-- {"Rect":{"w":3,"h":4}}
deserialize_enum("External", ["Dot", "At", "Rect"])
=> Ok(Rect { w: 3, h: 4 })
-- {"t":"Rect","w":3,"h":4}
deserialize_any
=> Ok(Rect { w: 3, h: 4 })
-- {"t":"Rect","c":{"w":3,"h":4}}
deserialize_struct("Adjacent", ["t", "c"])
=> Ok(Rect { w: 3, h: 4 })
-- {"c":{"w":3,"h":4},"t":"Rect"}
deserialize_struct("Adjacent", ["t", "c"])
=> Ok(Rect { w: 3, h: 4 })
-- {"w":3,"h":4}
deserialize_any
=> Ok(Rect { w: 3, h: 4 })
newtype 变体包的是结构体 Point,因为内部标签要把标签键塞进内容的那个对象里:包一个 f64 的 Num(f64) 序列化时报 cannot serialize tagged newtype variant E::Num containing a float,tuple 变体则在编译时就被拒绝(#[serde(tag = "...")] cannot be used with tuple variants),所以这里都没有放。
谁需要缓冲
反序列化时有没有缓冲,看「字段的值是从哪个 Deserializer 读出来的」就知道。src/bin/who_reads.rs 把 Rect 的 w 换成一个打印自己收到的 Deserializer 类型名的 Probe(u32):
impl<'de> Deserialize<'de> for Probe {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
println!(" w read from {}", std::any::type_name::<D>());
u32::deserialize(d).map(Probe)
}
}
external {"Rect":{"w":3,"h":4}}
w read from &mut serde_json::de::Deserializer<serde_json::read::StrRead<'_>>
=> Rect { w: Probe(3), h: 4 }
internal {"t":"Rect","w":3,"h":4}
w read from serde::private::de::content::ContentDeserializer<'_, serde_json::error::Error>
=> Rect { w: Probe(3), h: 4 }
adjacent {"t":"Rect","c":{"w":3,"h":4}}
w read from &mut serde_json::de::Deserializer<serde_json::read::StrRead<'_>>
=> Rect { w: Probe(3), h: 4 }
adjacent {"c":{"w":3,"h":4},"t":"Rect"}
w read from serde::private::de::content::ContentDeserializer<'_, serde_json::error::Error>
=> Rect { w: Probe(3), h: 4 }
untagged {"w":3,"h":4}
w read from serde::private::de::content::ContentRefDeserializer<'_, '_, serde_json::error::Error>
=> Rect { w: Probe(3), h: 4 }
收到 serde_json 自己的 Deserializer,说明值是边解析边交出来的;收到 ContentDeserializer / ContentRefDeserializer,说明它已经先被读进了 Content,现在是从树上重放(生成代码里写作 _serde::__private229::de::...,type_name 打印的是定义处的路径 serde::private::de::content)。汇总:
| 表示 | 属性 | Rect { w: 3, h: 4 } 的 JSON | 生成代码的第一个调用 | 缓冲 |
|---|---|---|---|---|
| 外部标签 | 默认 | {"Rect":{"w":3,"h":4}} | deserialize_enum | 不缓冲 |
| 内部标签 | tag = "t" | {"t":"Rect","w":3,"h":4} | deserialize_any | 除标签外全部缓冲 |
| 相邻标签 | tag = "t", content = "c" | {"t":"Rect","c":{"w":3,"h":4}} | deserialize_struct | 标签在前不缓冲,内容在前缓冲内容 |
| untagged | untagged | {"w":3,"h":4} | deserialize_any(经 ContentVisitor) | 整个值缓冲 |
「第一个调用」一列就是上面 reprs.rs 打印的那一行;外部标签往下的完整调用序列在外部标签一章。
flowchart TB
de["Deserialize::deserialize"] --> ext{哪种表示}
ext -->|外部标签| denum["deserialize_enum"]
ext -->|"相邻,标签在前"| dstruct["deserialize_struct"]
ext -->|内部标签| buf["读进 Content"]
ext -->|untagged| buf
ext -->|"相邻,内容在前"| buf
denum --> direct["值直接从格式读"]
dstruct --> direct
buf --> replay["ContentDeserializer 重放"]
缓冲的代价(多一次分配和遍历、丢失格式的类型提示、非自描述格式不支持、untagged 的报错变短)在 Content:缓冲的代价。序列化一侧四种表示都不缓冲,各调哪个 serialize_* 在生成的 Serialize。