本节摘要:给出所有权的三条铁律,用 E0382 案件讲清移动语义与栈上复制(Copy)的分界线,并给出"该移动还是该克隆"的判断流程。读完你能解释任意一段代码里每块内存的产权归属。
第三条无需任何运行时机制:作用域结束时的 drop 调用由编译器插入。第 1 章的"双重释放"卷宗由此根除——第二次释放的前提(两个所有者)在法律上就不成立。
let a = 5; let b = a; // 整数实现了 Copy,按位复制,a 仍可用 println!("{} {}", a, b); // 合法 let s1 = String::from("证物袋"); let s2 = s1; // String 持有堆缓冲,这里是移动 // println!("{}", s1); // E0382:value borrowed after move
分界线画在"是否拥有堆或外部资源"上:
| 类型 | 行为 | 原因 |
|---|---|---|
| 整数、浮点、bool、char | Copy,赋值后原值仍活 | 完全在栈上,复制廉价 |
| 元组与定长数组(元素全 Copy) | Copy | 递归成立 |
| String、Vec、Box | Move,原值失效 | 持有堆指针,复制指针会造成双释放 |
真要两份,显式克隆:let s2 = s1.clone();。克隆是一次新的堆分配,写出来就是要你看见成本。

fn archive(file: String) { // 所有权移入函数 println!("归档:{}", file); } // file 在此释放 let doc = String::from("卷宗173"); archive(doc); // println!("{}", doc); // E0382:产权已移交
传值即移交、调用即释放——这让"谁负责释放"永远没有争议,代价是需要拿回来的话得靠返回值交还,或者用下一节的借用。
💡 关键直觉:把变量想成房产证而非地址标签——证只有一张,转手后旧主无权处置。
铁律的边界情形最先在元组身上出现——一个值可以整体移交,也可以只移交其中一个字段,这就是部分移动(partial move)。
fn main() { let case = (String::from("卷宗"), 2024); let (title, _) = case; // 解构:title 拿走 String 产权 // let whole = case; // E0382:case 已部分移动,整体不可再移交 println!("编号 {}", case.1); // 但未移动的字段仍可读,产权可拆 println!("标题 {}", title); }
规则读起来拗口,法理却一致:产权按字段逐个登记。移动了 case.0,case.0 作废而 case.1 完好;正因存在"部分作废"状态,整体赋值或整体借用被禁止,编译器不想推断你到底要用哪一半。函数返回多值时的常见手感是反向的——返回元组再解构接收,产权从被调方整体移入调用方,没有任何借用悬空。
Copy 与 Drop 互斥,这不是实现细节而是逻辑必然:Copy 承诺按位复制后新旧两份等同、离开作用域都无事可做;Drop 则声明离开作用域要执行善后。两者同时成立就意味着善后逻辑要执行两次,等于给双重释放开门。所以凡是需要析构动作的类型(持文件句柄、锁、缓冲)必然只能移动。
struct Seal { stamp: String } impl Drop for Seal { fn drop(&mut self) { println!("封条销毁:{}", self.stamp); // 善后逻辑 } } fn main() { let s = Seal { stamp: String::from("甲") }; let _t = s; // 移动,s 作废 // println!("{}", s.stamp); // E0382:善后责任已随产权转移 } // 这里对 _t 执行一次 drop,恰好一次
若想提前销毁而不等到作用域结束,标准姿势是 drop(s); 显式调用——注意这是普通函数调用,不是 Drop::drop,直接调后者会编译失败,E0040 会出面制止。
| 现场特征 | 判决 | 理由 |
|---|---|---|
| 值随后还要用,且修改互不影响 | clone | 真需要两份独立产权 |
| 值随后只读 | 改传引用 &T | 产权不动,零成本 |
| 值用完即弃、函数要长期持有 | 移动 | 产权随数据走,责任清晰 |
| 小且全在栈上 | 赋值即 Copy | 编译器自动处理 |
判据的核心是问一句"调用方之后还用不用"。用,则借用或克隆;不用,则大方移交。频繁 clone 出现在热路径上是性能审阅的红旗,而出现在边界转换处(解析输入、序列化输出)则完全正当——把成本写在明处,正是显式克隆的设计意图。
fn file_case() -> String { let a = String::from("X"); let b = a; b } fn main() { let c = file_case(); let d = c; // println!("{}", c.len()); println!("{}", d.len()); }
问:注释处能否通过编译?答案是 E0382——c 的产权已移交 d。再问:这个 String 从诞生到销毁被移动了几次、最终在哪里 drop?能不加思索答出"两次移动、在 main 结束时随 d 释放",本节即可结案。
上节自测的完整答案:函数内 a 移动给 b 是第一次移动,b 作为返回值移出给调用方是第二次,main 里 c 移动给 d 是第三次——共三次移动,String 最终随 d 在 main 结束时 drop,堆缓冲恰好释放一次。全程没有任何克隆,产权链却始终唯一。把这段话对照源码逐句指出对应行,就是"产权思维"的入门考试;日常读开源代码时偶尔做一次这种追踪,是保持手感成本最低的训练。