3.1 所有权三铁律:产权的确立与转移


3.1 所有权三铁律:产权的确立与转移

本节摘要:给出所有权的三条铁律,用 E0382 案件讲清移动语义与栈上复制(Copy)的分界线,并给出"该移动还是该克隆"的判断流程。读完你能解释任意一段代码里每块内存的产权归属。

三条铁律

  1. Rust 里每个值都有一个唯一的所有者(变量);
  2. 同一时刻,值只能被一个所有者持有;
  3. 所有者离开作用域,值被释放——确定性析构,无需 GC

第三条无需任何运行时机制:作用域结束时的 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:产权已移交

传值即移交、调用即释放——这让"谁负责释放"永远没有争议,代价是需要拿回来的话得靠返回值交还,或者用下一节的借用。

要点回顾

  • 释放跟随所有权,作用域结束即 drop,无 GC 无泄漏(常规路径下);
  • Copy 与 Move 的分界是"是否持有堆资源",记这一条胜过背类型表;
  • clone 是显式的成本声明,出现在代码里就是性能审阅点。

💡 关键直觉:把变量想成房产证而非地址标签——证只有一张,转手后旧主无权处置。

部分移动:产权可以拆分移交

铁律的边界情形最先在元组身上出现——一个值可以整体移交,也可以只移交其中一个字段,这就是部分移动(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.0case.0 作废而 case.1 完好;正因存在"部分作废"状态,整体赋值或整体借用被禁止,编译器不想推断你到底要用哪一半。函数返回多值时的常见手感是反向的——返回元组再解构接收,产权从被调方整体移入调用方,没有任何借用悬空。

实现 Drop 的类型没有 Copy 资格

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,堆缓冲恰好释放一次。全程没有任何克隆,产权链却始终唯一。把这段话对照源码逐句指出对应行,就是"产权思维"的入门考试;日常读开源代码时偶尔做一次这种追踪,是保持手感成本最低的训练。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U