所有权与借用

一句话理解

Rust 不用 GC,靠编译期的所有权规则决定每块内存何时释放。

  • 所有权解决”谁负责释放”
  • 借用解决”能不能临时用一下”
  • 违反规则的代码编译不过,而不是运行时崩溃

1. 为什么需要所有权

方案内存何时释放代价
GC(Java / Go / JS)运行时垃圾回收运行时开销、停顿、内存占用偏高
手动管理(C / C++)开发者调用 free / delete悬垂指针、double free、泄漏
所有权(Rust)编译期确定:所有者离开作用域即 drop需要适应借用检查器

Rust 的立场:把内存安全问题提前成编译错误,运行时零成本。代价是你要学会和借用检查器相处——这是 Rust 学习曲线最陡的一段,也是最值的一段。

2. 三条核心规则

  1. 每个值有且只有一个所有者(owner)
  2. 同一时刻:要么有多个不可变借用 &T,要么只有一个可变借用 &mut T,二者不能共存
  3. 所有者离开作用域,值被自动 drop
{
    let s = String::from("hello"); // s 是所有者
    println!("{}", s);
} // 作用域结束,s 在这里被释放,不需要手写 free

3. 移动、复制与克隆

let s1 = String::from("hello");
let s2 = s1;              // 所有权从 s1 移动(move)到 s2
// println!("{}", s1);    // ❌ E0382: borrow of moved value
println!("{}", s2);       // ✅

String 在栈上只有一个「指针 + 长度 + 容量」的胖指针,真实字节在堆上。如果 s1 = s2 只是浅拷贝,就会 double free——所以 Rust 干脆移动所有权,让 s1 失效。

而栈上的纯数据可以直接按位复制,这类类型实现了 Copy:

let x = 5;
let y = x;                // i32 实现了 Copy,x 仍然可用
println!("{} {}", x, y);  // ✅
概念含义典型类型
Copy按位复制,原值仍可用;不能与 Drop 共存i32、f64、bool、char、&T、[T; N](元素 Copy 时)
Clone显式调用 .clone() 做(可能是深)拷贝String、Vec<T>、HashMap
Move默认行为,所有权转移,原变量失效大多数拥有堆数据的类型

.clone() 不是万能解药

借用检查器报错时随手加 .clone() 能让代码编过,但可能悄悄引入大量无谓拷贝。先问:这里能不能改成传引用,或者干脆让所有权转移出去?

4. 借用:只借不拿

fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s;      // 不可变借用
    let r2 = &s;      // 可以有多个
    println!("{} {}", r1, r2);
 
    let r3 = &mut s;  // ✅ 前面的不可变借用已经不再使用(NLL)
    r3.push_str(", world");
    println!("{}", r3);
}

NLL(Non-Lexical Lifetimes):借用的有效期到”最后一次使用”为止,而不是到作用域结束。这是 Rust 2018 之后的改进,让很多老代码里的”多余作用域”不再必要。

let mut s = String::from("hello");
let r = &s;
println!("{}", r);
let m = &mut s;   // ✅ 上面那次不可变借用已经结束

而下面这段就是典型的 E0502:

let mut v = vec![1, 2, 3];
let first = &v[0];       // 不可变借用
v.push(4);               // ❌ 可变借用:push 可能重新分配,first 会悬垂
println!("{}", first);

为什么 push 之后不能用 first

Vec::push 在容量不足时会重新分配堆内存,原来的元素地址失效。借用检查器拦住你,正是因为它防止了一次真实的悬垂引用——这不是”编译器找麻烦”,是它在替你挡 bug。

5. 函数签名上怎么选

签名语义使用场景
fn f(s: String)拿走所有权(会 drop)需要在函数内销毁、或转移给别人
fn f(s: &str)只读借用大多数只读函数
fn f(s: &mut String)可写借用需要原地修改
fn f(s: String) -> String拿走再还回来链式转换
fn f(v: &Vec<T>)只读借用(不推荐)应写成 &[T]

经验法则:

  • 参数尽量用借用,别让调用方失去所有权
  • 返回新值而不是改参数,风格更函数式
  • 需要长期持有,才要求所有权

6. String / &str、Vec<T> / &[T]

拥有型借用型说明
String&str&str 可来自字符串字面量、String 切片、&String 解引用
Vec<T>&[T]切片是通用只读视图
PathBuf&Path同上,文件路径版
OsString&OsStr同上,非 UTF-8 路径版
fn total(nums: &[i32]) -> i32 {   // 接受数组、Vec、切片都行
    nums.iter().sum()
}
 
fn main() {
    let v = vec![1, 2, 3];
    let arr = [4, 5, 6];
    println!("{} {}", total(&v), total(&arr));
}

借用型让 API 更通用

&[T] 比 &Vec<T> 好,&str 比 &String 好。前者能接受更多输入,且不多一层间接寻址。

7. 常见编译错误对照表

错误码报错文本典型原因修法
E0382borrow of moved value用完所有权又用原变量传引用、提前 clone()、或调整顺序
E0502cannot borrow as mutable because also borrowed as immutable可变与不可变借用重叠缩短借用范围,把读取放在修改之前
E0499cannot borrow as mutable more than once同时两个可变借用用作用域分隔,或改用索引
E0505cannot move out because borrowed借用还活着就转移所有权先结束借用
E0596cannot borrow as mutable, not declared as mutable变量没加 mut加 mut,或重新设计
E0515cannot return reference to local variable返回了局部变量的引用返回拥有型值,或传入外部引用
E0716temporary value dropped while borrowed借用了临时值用 let 绑定住临时值

读报错的方式

rustc 的报错通常会直接给出「suggestion」,照着改八九不离十。遇到借用问题,先看它说哪个借用和哪一行冲突,再想能不能缩短借用范围——而不是立刻 clone。

8. 与 C++ RAII 的关系

Rust 的 Drop 和 C++ 的析构函数思路一致:资源获取即初始化,离开作用域即释放。区别在于:

  • C++ 允许你复制一个持有资源的对象(触发拷贝构造),也可能出现悬垂引用和 double free,需要开发者自己保证
  • Rust 在编译期强制唯一所有权,把这类错误变成类型错误
  • Rust 的 Drop 是确定的(不像 GC 靠回收时机),适合用来管文件句柄、锁、连接

9. 小结

关键判断

  1. 所有权管释放,借用管访问,二者由编译器静态检查
  2. 默认语义是 move,Copy 是例外,Clone 是显式深拷贝
  3. 遇到借用冲突,先想「能不能缩短借用范围 / 改成传引用」,最后才考虑 clone
  4. API 设计上优先用借用型(&str、&[T]),把所有权留在调用方

相关笔记