
0 引子:为什么解构不是"语法糖"?
在 Rust 中写下
rust
let Point { x, y } = p;
时,我们不仅得到了两个新绑定,还重新划分了所有权:
x与y的 所有权 从p中被移动出来;- 如果
p还有未解构的字段,它们会在同一作用域继续存活; - 编译器在 MIR 层为每个字段插入精确的 drop flag ,保证析构恰好一次。
因此,解构 = 所有权的编译期重新布线 。
本文将:
- 从 语言规则 到 MIR 生成 拆解这一过程;
- 用 3 个递进案例 展示 "拆" 与 "留" 的零成本艺术;
- 用 200 行真实代码 实现一个 零拷贝 JSON 解析器 ,把解构写进 宏规则。
1 规则速览:解构如何与所有权交互?
| 解构形式 | 所有权语义 | 剩余字段状态 |
|---|---|---|
let Struct { a, b } = s |
整体 move,字段 a、b 获得所有权 |
无剩余(s 失效) |
let Struct { a, .. } = s |
仅 a move,.. 字段原地析构 |
s 部分失效 |
let [x, y] = arr |
固定长度数组整体 move | 无剩余 |
let [x, rest @ ..] = slice |
x move,rest 成为新切片 |
原切片失效 |
let (a, b) = tuple |
元组整体 move | 无剩余 |
所有规则都在 借用检查器 内完成;剩余字段的 drop 由 字段级 drop flags 实现,LLVM 可常量折叠,零运行时开销。
2 初级:结构体字段解构
2.1 代码
rust
#[derive(Debug)]
struct Config {
db_url: String,
port: u16,
debug: bool,
}
fn start_server(mut cfg: Config) {
// 只关心 db_url,其余字段留在 cfg
let Config { db_url, .. } = cfg;
println!("connecting to {}", db_url);
// cfg.port / cfg.debug 仍可用
}
2.2 MIR 视角
bb0:
_2 = move (_1.0); // db_url
drop(_1.1); // port
drop(_1.2); // debug
_1(cfg)被 部分 move;- 剩余字段 立刻 drop,无额外运行时表。
3 中级:数组 & 切片解构
3.1 固定长度数组
rust
fn parse_rgb([r, g, b]: [u8; 3]) -> u32 {
((r as u32) << 16) | ((g as u32) << 8) | b as u32
}
- 整体 move,无剩余字段。
3.2 可变长度切片
rust
fn parse_csv(line: &str) -> (&str, &[&str]) {
let mut parts = line.split(',');
let head = parts.next().unwrap();
let tail = parts.collect::<Vec<_>>();
(head, &tail[..]) // ❌ tail 在函数作用域外悬垂
}
修复:使用 collect 转移所有权,或 返回 Vec:
rust
fn parse_csv(line: &str) -> (&str, Vec<&str>) {
let mut parts = line.split(',');
let head = parts.next().unwrap();
let tail = parts.collect();
(head, tail)
}
4 高级:把解构写进宏------零拷贝 JSON AST
目标:
- 解析 JSON → 不复制字符串
- AST 节点全部用 不可变借用 (
&'input str)- 用 宏 把「字段级解构」写进 DSL
4.1 数据结构
rust
#[derive(Debug)]
enum Json<'a> {
Null,
Bool(bool),
Num(&'a str),
Str(&'a str),
Arr(Vec<Json<'a>>),
Obj(Vec<(&'a str, Json<'a>)>),
}
4.2 解析器骨架
rust
struct Parser<'a> {
src: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn parse(&mut self) -> Json<'a> {
self.skip_ws();
match self.peek() {
Some(b'n') => { self.consume("null"); Json::Null }
Some(b't') => { self.consume("true"); Json::Bool(true) }
Some(b'f') => { self.consume("false"); Json::Bool(false) }
Some(b'"') => Json::Str(self.parse_str()),
Some(b'[') => self.parse_arr(),
Some(b'{') => self.parse_obj(),
_ => Json::Num(self.parse_num()),
}
}
fn parse_str(&mut self) -> &'a str {
let start = self.pos + 1;
self.bump(); // skip '"'
while self.peek() != Some(b'"') { self.bump(); }
let end = self.pos;
self.bump(); // skip closing '"'
&self.src[start..end]
}
fn parse_num(&mut self) -> &'a str {
let start = self.pos;
while self.peek().map_or(false, |c| c.is_ascii_digit()) { self.bump(); }
&self.src[start..self.pos]
}
fn parse_arr(&mut self) -> Json<'a> {
self.bump(); // '['
self.skip_ws();
let mut out = Vec::new();
while self.peek() != Some(b']') {
out.push(self.parse());
self.skip_ws();
if self.peek() == Some(b',') { self.bump(); self.skip_ws(); }
}
self.bump(); // ']'
Json::Arr(out)
}
fn parse_obj(&mut self) -> Json<'a> {
self.bump(); // '{'
self.skip_ws();
let mut out = Vec::new();
while self.peek() != Some(b'}') {
let key = self.parse_str();
self.skip_ws();
assert_eq!(self.bump(), Some(b':'));
self.skip_ws();
let val = self.parse();
out.push((key, val));
self.skip_ws();
if self.peek() == Some(b',') { self.bump(); self.skip_ws(); }
}
self.bump(); // '}'
Json::Obj(out)
}
fn peek(&self) -> Option<u8> { self.src.as_bytes().get(self.pos).copied() }
fn bump(&mut self) -> Option<u8> { let c = self.peek(); self.pos += 1; c }
fn skip_ws(&mut self) { while self.peek().map_or(false, u8::is_ascii_whitespace) { self.bump(); } }
}
4.3 宏:把解构写进模式
rust
macro_rules! destructure {
($struct:ident { $($field:ident),+ } = $expr:expr => $body:block) => {{
let $struct { $($field),+ , .. } = $expr;
$body
}};
}
fn demo_macro(tok: Json<'_>) {
destructure!(Json::Obj { entries } = tok => {
for (k, v) in entries {
println!("key={}, val={:?}", k, v);
}
});
}
5 常见坑与编译器诊断
| 代码 | 错误 | 原因 |
|---|---|---|
let a = s.x; use(s) |
E0382 | s 部分 move |
let [x, y] = slice |
E0509 | 固定长度 vs 动态长度 |
match x { Some(y) => y, _ => x } |
使用 mem::take |
6 性能观察:drop flags 被优化为常量
rust
#[derive(Debug)]
struct Loud(i32);
impl Drop for Loud {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn demo() {
let s = (Loud(1), Loud(2), Loud(3));
let (_, b, _) = s; // 仅 b 保留
}
输出:
drop 1
drop 3
1与3在解构点立刻 drop,无额外运行时检查- 汇编级与手写析构 等价
7 结语:解构 = 所有权的零成本重塑
- 字段级 :
a.x的零成本拆解 - 模式级 :
let Struct { x, .. } = s的编译期线性化 - 宏级 :
destructure!的 DSL 化
当你下一次写解析器、异步运行时、或零拷贝协议时,
请记住:解构不是语法糖,而是所有权的编译期重写。
