CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Rust 语言入门/课时/所有权与借用
RawBlame
课时目录12 节

Rust 语言入门

  1. 01Rust 是什么
  2. 02基础语法
  3. 03所有权与借用
  4. 04生命周期
  5. 05结构体与枚举
  6. 06错误处理
  7. 07Trait 与泛型
  8. 08集合类型
  9. 09并发与 async
  10. 10宏与元编程
  11. 11Web 与生态
  12. 12综合练习

所有权与借用

ownership、borrowing、slices、stack vs heap。

40 分钟

所有权与借用

这一课你将能做到

  1. 说清所有权三条规则,判断任意一段代码里值的所有权归属
  2. 用 & / &mut 写借用代码,读懂并修复借用检查器报错
  3. 分清 String 与 &str 的使用场景,写出接收引用而非抢夺所有权的函数

为什么要学

所有权是 Rust 替代 GC 的核心机制:编译器靠它决定每块内存在哪里释放、引用是否安全。这也是 Java 转 Rust 的第一道坎——Java 里赋值是「共享引用」,随手传都行;Rust 里赋值默认是「所有权转移」,传出去自己就不能再用了。

掌握它的直接收益:读得懂编译器报错(E0382 是初学者出现频率最高的错误码),并且天然写出无内存泄漏、无悬垂指针、无数据竞争的代码。面试聊 Rust 绕不开它。

先建立图像

栈(连续、快、定长)          堆(灵活、慢、动态分配)
┌─────────────┐             ┌──────────────────┐
│ s: String    │──ptr──────→│ "judger" (堆内存) │
│ (ptr,len,cap)│             │  所有者:s        │
└─────────────┘             └──────────────────┘
  • 整型、布尔、浮点这类定长小值放栈上,赋值时直接按位拷贝(Copy 语义)
  • String、Vec 这类动态数据放堆上,栈上只留一个「胖指针」;赋值默认转移所有权(move 语义),而不是拷贝堆数据

三条规则,全部推论都从这来:

  1. 每个值有唯一所有者(owner)
  2. 同一时刻只能有一个所有者
  3. 所有者离开作用域,值被释放(drop)

跟着做

步骤 1:亲眼看一次 move

cargo new own-demo
cd own-demo
fn main() {
    let s1 = String::from("judger");
    let s2 = s1;              // 所有权从 s1 move 到 s2

    // println!("{s1}");      // ← 取消注释:error[E0382] s1 已失效
    println!("{s2}");
}

把注释解开跑一次,读编译器报错:value borrowed here after move。这就是 Rust 在阻止你访问一块「已经不属于你」的内存——Java 里这行代码能跑(两个引用指向同一对象),Rust 在编译期直接拒绝。

步骤 2:需要两份就 clone,纯数字走 Copy

fn main() {
    let s1 = String::from("judger");
    let s2 = s1.clone();      // 深拷贝堆数据,s1 依然有效
    println!("{s1} / {s2}");

    let x: i32 = 5;
    let y = x;                // i32 实现了 Copy trait,按位拷贝,x 仍可用
    println!("{x} + {y}");
}

规律:小而定长的类型(整数、浮点、bool、char、由它们组成的元组)赋值即拷贝;持有堆内存的类型(String、Vec、HashMap)赋值即 move。

步骤 3:用借用代替转移

fn byte_len(s: &String) -> usize {   // &String:借用,不夺走所有权
    s.len()
}

fn main() {
    let s = String::from("hello rust");
    let n = byte_len(&s);   // 传引用
    println!("{s} 的字节长度是 {n}");   // s 依然可用
}

& 借用就像「看一眼但不带走」:函数用完还回来。对照 Java,这相当于把参数声明为只读视图,但编译器强制执行——被借用的值在借用期内不能被释放或修改。

步骤 4:借用规则——一山不容二虎

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

    let r1 = &s;
    let r2 = &s;              // 多个不可变借用共存:读读不冲突
    println!("{r1} {r2}");

    let r3 = &mut s;          // 不可变借用用完后,可变借用合法
    r3.push('!');
    println!("{r3}");         // 同一时刻:要么多个 &,要么一个 &mut
}

把 println!("{r1} {r2}") 挪到 r3 之后,编译器立刻报 E0502:可变借用存在期间不允许任何其他借用。这条规则同时消灭了「一边遍历一边改」和「数据竞争」两类 bug。

步骤 5:编译器如何阻止悬垂引用

// fn dangle() -> &String {
//     let s = String::from("x");
//     &s          // ← 取消注释:error[E0106] 缺少生命周期标注 → 本质是悬垂引用
// }

fn own_it() -> String {   // 正解:把所有权交出去
    String::from("x")
}

fn main() {
    let s = own_it();
    println!("{s}");
}

C++ 返回局部变量引用是未定义行为,Java 靠 GC 延迟回收兜底;Rust 直接编译不过。dangle 函数真正的修复方式是返回 String 本身——把所有权交给调用方。

步骤 6:实战——统计最长单词,全程零拷贝

fn longest_word(text: &str) -> &str {
    text.split_whitespace()
        .max_by_key(|w| w.len())
        .unwrap_or("")
}

fn main() {
    let log = "compile error borrow check failed";
    let word = longest_word(log);   // 借用 log 的切片,不复制任何内容
    println!("最长单词:{word}");
}

&str 是字符串切片:指向原字符串某一段的只读视图。高频接口里传日志、传输出都用 &str,避免 String 来回复制——这正是 Rust 性能优势的来源之一。

讲清楚要点

  1. move 语义:堆数据的赋值/传参默认转移所有权,原变量立即失效。想要两份,clone() 显式付费。
  2. 借用规则:同一时刻,要么任意多个不可变借用 &T,要么恰好一个可变借用 &mut T,二者互斥。读写锁的语义被搬到编译期。
  3. 借用不能比所有者活得久:这是生命周期检查(第 4 课)的核心,悬垂引用由此绝迹。
  4. String vs &str:String 是堆上可增长的所有权字符串(对标 StringBuilder 但更常用);&str 是只读切片视图(更接近 final String 的引用但无 GC 依赖)。函数参数优先收 &str,调用方传 String 也能自动转。
  5. NLL(非词法作用域生命周期):借用的有效期按「最后一次使用」计算,不是按花括号,所以先 &s 后 &mut s 只要不重叠就合法。

你可能会懵的地方

  • 为什么 Rust 不像 Java 那样随便共享引用? 随便共享的前提是 GC 兜底加运行时锁。Rust 要做到无 GC 无锁下的内存安全,就必须让编译器知道「谁能读、谁能写、用到什么时候」,所有权就是这套信息的形式化。

  • 到处 clone 会不会很慢? 滥用 clone 会慢。正确姿势是函数收引用(&str、&Vec),只在真正需要独立副本时 clone。熟练后代码里 clone 的密度会自然降到个位数。

  • &mut 参数和 Java 的对象参数是一回事吗? 表面都是「函数内改外面看得见」,本质不同:Java 传的是共享引用,任何线程随时可改;Rust 的 &mut 是独占借用,编译期保证借用期间没有第二个访问者。

  • Copy 类型和 Clone 有什么区别? Copy 是隐式的位级拷贝(赋值即复制,原值仍可用),Clone 是显式调用(可能深拷贝、可能昂贵)。String 实现了 Clone 但没实现 Copy。

  • 函数返回 &str 但源数据是参数,编译器怎么知道不悬垂? 靠生命周期省略规则推导(第 4 课)。推不出来时编译器要求你显式标注,而不是放行。

错法对照

错法问题正解
move 后继续用原变量error[E0382] value used after moveclone 一份,或改为传引用
持有 &s 的同时拿 &mut serror[E0502] 可变借用不独占错开使用区间,或改用可变借用到底
函数返回局部变量的引用error[E0106]/E0515 悬垂返回所有权(String),或让调用方传入缓冲
循环里 let s2 = vec.remove(0) 后又用 vec 里的旧引用所有权被移走的元素不可再访问用索引访问 &vec[0] 或先 clone
所有函数参数都收 String调用方被迫复制参数收 &str,需要所有权时在函数内 to_string

卡点急救

  1. error[E0382] used after move:编译器消息里标了 move 发生点和使用点。三选一:clone、传引用、调整代码顺序让 move 放到最后一次使用之后。
  2. error[E0502] cannot borrow as mutable:找出前一个借用还活着的区间,把它的最后一次使用提前;或把读操作的结果先取出再修改。
  3. 报错信息太长看不懂:先看第一条 error,忽略 note/help 之外的内容;cargo check 比完整编译输出更清爽。实在卡住把报错前两行直接搜,Rust 社区对此覆盖极全。

面试常问

  • 所有权三条规则是什么?它们如何替代 GC? 唯一所有者、赋值即转移、离开作用域即 drop。编译器据此在编译期插入释放调用,确定性地回收内存,无 STW、无引用计数开销;安全性由借用检查兜底。

  • move 语义和 Java 的引用赋值有什么区别? Java 赋值共享同一对象,谁都能改,靠 GC 判断回收时机;Rust move 是所有权转移,原变量编译期失效,任何时刻数据有唯一主人,无需 GC 即可判定释放点。

  • 借用规则为什么能防止数据竞争? 数据竞争需要「多个指针同时访问 + 至少一个写 + 无同步」。Rust 的「多个读或一个写」互斥规则在编译期禁止了无同步的并发写与读写混用,把竞争从运行时事故变成编译错误。

自测三问

  1. let s2 = s1; 之后还能用 s1 吗?取决于什么?
  2. 为什么「多个 & 和一个 &mut 互斥」能同时消灭迭代中修改集合的 bug?
  3. 写函数接收一段用户名并返回其长度,参数类型该用 String、&String 还是 &str?为什么?

小结一句话

所有权把「这块内存归谁、谁在读写、活到什么时候」变成编译期可验证的数学题——borrow checker 不是敌人,是免费的并发审查员。

延伸

  • The Rust Book ch04:理解所有权
  • 下一课:生命周期——给引用的有效范围贴上编译期标签。
加载本节练习…
← 上一课基础语法下一课 →生命周期