Skip to content

所有权与借用入门

1. 这是什么

所有权(ownership)是 Rust 最核心的语言机制之一。
它决定一段数据在某个时刻“归谁负责”,以及这段数据什么时候可以被读取、修改、转移和释放。

借用(borrowing)则是在不拿走所有权的前提下,临时去访问这段数据的方式。

一句话理解:

  • 所有权回答的是:这份值现在归谁管
  • 借用回答的是:我能不能先看一下,或者暂时改一下

Rust 之所以能在没有 GC 的情况下仍然保证内存安全,很大程度上就是因为它把这些规则放到了编译期检查。

2. 为什么重要

很多语言里,初学者写代码时很少主动思考“值的归属”。
但在 Rust 里,如果不理解所有权,后面的移动、借用、生命周期、trait 设计几乎都会觉得别扭。

理解这个机制后,你会明白:

  • 为什么一个值赋给另一个变量后,旧变量可能不能再用
  • 为什么同一时刻不能同时存在“可变借用”和“不可变借用”冲突
  • 为什么 Rust 能在编译期拦住很多悬垂引用和数据竞争问题

所以它不是一个孤立知识点,而是整门语言的底层规则。

3. 先建立直觉

先看一个最简单的例子:

rust
fn main() {
    let s = String::from("hello");
    let t = s;

    println!("{}", t);
}

这段代码里:

  • s 原本拥有这段字符串数据
  • let t = s; 之后,所有权被移动给了 t
  • 此时 s 不再有效

如果你再写:

rust
println!("{}", s);

编译器就会报错。

这说明 Rust 不是简单地“复制一下变量名”,而是非常认真地跟踪值的归属。

可以把它想象成:

  • 一个箱子里装着资源
  • 某一时刻只能有一个人负责这个箱子
  • 你可以把箱子交给别人
  • 也可以让别人暂时看看里面有什么
  • 但规则必须明确,不能同时乱抢

4. 核心内容

4.1 所有权三条基本规则

理解所有权,先记住三条核心规则:

  1. Rust 中每个值都有一个 owner
  2. 同一时刻一个值只能有一个 owner
  3. owner 离开作用域时,值会被自动释放

例如:

rust
fn main() {
    {
        let s = String::from("hello");
        println!("{}", s);
    }

    // 这里 s 已经离开作用域,内存会被自动回收
}

这就是 Rust 不依赖 GC 也能安全管理资源的基础。

4.2 Copy 与 Move

不是所有赋值都会触发“所有权转移”的那种紧张感。
Rust 会区分 Copy 类型Move 类型

Copy 类型

像整数、布尔值、字符这类小而简单的值,通常会直接复制:

rust
fn main() {
    let x = 10;
    let y = x;

    println!("{}", x);
    println!("{}", y);
}

这里 x 还能继续使用,因为 i32 是 Copy 类型。

Move 类型

StringVec<T> 这类拥有堆内存资源的类型,默认更常见的是 move:

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}", s2);
}

这里 s1 的所有权已经转移给 s2

4.3 克隆不是移动

如果你确实想保留两份独立数据,可以显式调用 clone()

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("{}", s1);
    println!("{}", s2);
}

这时:

  • s1 还有效
  • s2 是一份新的拷贝
  • 代价也比简单复制更高,因为它可能涉及堆内存分配

所以要建立一个意识:

  • = 不等于深拷贝
  • 需要复制真实数据时要显式说出来

4.4 借用:不拿走所有权地访问值

如果只是想读某个值,通常不需要拿走它的所有权,而是借用:

rust
fn print_len(s: &String) {
    println!("len = {}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_len(&s);
    println!("{}", s);
}

这里:

  • &s 表示对 s 的不可变借用
  • print_len 只是“看一下”这段字符串
  • 所有权仍然在 main 里的 s 手上

这是写 Rust 时最常见的模式之一。

4.5 可变借用

如果你想修改值,就要用可变借用:

rust
fn append_world(s: &mut String) {
    s.push_str(" world");
}

fn main() {
    let mut s = String::from("hello");
    append_world(&mut s);
    println!("{}", s);
}

这里需要同时满足两件事:

  • 变量本身要是 mut
  • 借用时也要写 &mut

4.6 借用规则为什么这么严格

Rust 最关键的借用规则之一是:

  • 同一时刻可以有多个不可变借用
  • 或者只能有一个可变借用
  • 但不能两种同时冲突存在

例如:

rust
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} {}", r1, r2);

    let r3 = &mut s;
    r3.push_str("!");
}

这段是可以的,因为不可变借用 r1r2 在使用完之后,后面才出现可变借用 r3

但下面这种通常就不行:

rust
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &mut s;

    println!("{} {}", r1, r2);
}

因为这意味着:

  • 一边有人在只读观察这份数据
  • 另一边又有人准备修改它

编译器会阻止这种潜在冲突。

4.7 函数参数里的所有权

函数调用时也经常发生所有权移动:

rust
fn takes_string(s: String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("hello");
    takes_string(s);
}

这里 takes_string 拿走了 s 的所有权。调用后,main 里的 s 就不能再用了。

如果只是想读取它,更常见写法是:

rust
fn takes_str(s: &str) {
    println!("{}", s);
}

fn main() {
    let s = String::from("hello");
    takes_str(&s);
    println!("{}", s);
}

这也是为什么 Rust 代码里经常能看到 &str&T&mut T 这样的参数形式。

5. 常见误区

5.1 误区一:所有权就是语法限制

不是。
它本质上是在用编译期规则换运行期安全。

你表面上觉得是“语法限制很多”,但底层得到的是:

  • 更明确的资源边界
  • 更少的悬垂引用
  • 更少的数据竞争
  • 更可预测的性能

5.2 误区二:能 clone 就一直 clone

clone() 很有用,但不能把它当成“编译不过就暴力绕过去”的万能按钮。

如果一遇到借用报错就 clone:

  • 代码可能掩盖真实数据流
  • 性能可能变差
  • 你会始终学不会所有权模型

更好的做法是先问:

  • 这里真的需要复制数据吗
  • 能不能借用
  • 能不能调整函数签名

5.3 误区三:可变借用越多越灵活

Rust 不是鼓励“到处改状态”,而是鼓励你明确状态变化发生在哪里。
可变借用越多,数据流越难推理。

很多时候,先用不可变借用思考问题,代码反而更清晰。

6. 一个更实用的判断思路

写 Rust 时如果卡住了,可以按这几个问题排查:

  1. 这个值现在到底归谁所有
  2. 我这里只是读取,还是要修改
  3. 我需要拿走所有权,还是借用就够了
  4. 这里是不是不小心让不可变借用和可变借用重叠了
  5. 我加 clone() 是真的需要,还是只是为了绕过错误

很多初学者报错看不懂,其实最后都能归到这几类问题上。

7. 学习建议

学习所有权时,不要只背规则,要反复做这种转换练习:

  • 先写一个会 move 的版本
  • 再改成借用版本
  • 再改成可变借用版本
  • 再观察哪些地方需要 String,哪些地方只需要 &str

只要这种转换做多了,脑子里就会慢慢形成“值的流动图”。

8. 自测标准

  • 能解释 owner、move、borrow 分别是什么意思
  • 能区分 Copy 类型和会 move 的类型
  • 能解释为什么 String 赋值后原变量常常失效
  • 能写出不可变借用和可变借用的基本函数签名
  • 遇到借用错误时,能先从“值归谁”和“借用是否冲突”开始排查