Rust学习文档(二)

目录

02 所有权与借用

2.1 什么是所有权(Ownership)

Rust 为什么需要所有权

所有权三条铁律

作用域(Scope)

String 类型与栈/堆

移动(Move)—— 核心机制

克隆(Clone)—— 深拷贝

Copy Trait —— 栈上拷贝

函数传参与所有权转移

函数返回值与所有权转移

2.2 引用与借用(References & Borrowing)

问题:所有权转移导致使用不便

解决方案:引用(Reference)

可变引用(Mutable Reference)

可变引用的限制 —— 同一时刻只能有一个

不可变引用 vs 可变引用的互斥

悬垂引用(Dangling References)—— Rust 在编译期防止

引用规则总结

2.3 切片(Slices)

字符串切片

字符串切片的危险性 —— UTF-8 边界

函数参数中的字符串切片

数组切片

切片在内存中的表示

2.4 所有权决策树(速查)

学习目标检验


02 所有权与借用

2.1 什么是所有权(Ownership)

Rust 为什么需要所有权

语言

内存管理方式

问题

C/C++

手动malloc/free

忘记释放 → 内存泄漏;重复释放 → 悬垂指针

Java/Go

垃圾回收(GC)

运行时开销;GC 暂停影响延迟

Rust

所有权系统(编译期检查)

零运行时开销,编译器保证安全

所有权三条铁律

1. Rust 中每一个值都有一个被称为其 所有者(owner)的变量
2. 一次只能有一个所有者
3. 当所有者离开作用域时,这个值将被丢弃(drop)

作用域(Scope)

{ let s = String::from("hello"); // s 从这里开始有效 // 使用 s } // 作用域结束,s 被丢弃(drop),内存自动释放

String 类型与栈/堆

let s1 = String::from("hello"); // 堆上分配,可变长度 let s2 = "hello"; // 栈上(&str 切片类型),不可变 // String 由三部分组成(存储在栈上): // - 指向堆数据的指针(ptr) // - 长度(len) // - 容量(capacity)

移动(Move)—— 核心机制

let s1 = String::from("hello"); let s2 = s1; // s1 的所有权"移动"到 s2 // println!("{s1}"); // 编译错误!s1 已失效 println!("{s2}"); // OK // 原因:如果 s1 和 s2 都指向同一块堆内存, // 作用域结束时两者都会尝试释放 → 双重释放 → 内存不安全 // Rust 选择在移动时使原变量失效来避免此问题

克隆(Clone)—— 深拷贝

let s1 = String::from("hello"); let s2 = s1.clone(); // 深拷贝,堆数据独立 println!("s1 = {s1}, s2 = {s2}"); // 两者都有效

Copy Trait —— 栈上拷贝

let x = 5; let y = x; // i32 实现了 Copy trait,直接复制值(栈拷贝) println!("x = {x}, y = {y}"); // 两者都有效 // 实现 Copy trait 的类型(全部存储在栈上): // - 所有整数类型(i8, u32, usize 等) // - 所有浮点类型(f32, f64) // - 布尔 bool // - 字符 char // - 元组(仅当所有元素都是 Copy 时,如 (i32, f64))

函数传参与所有权转移

fn main() { let s = String::from("hello"); takes_ownership(s); // s 已移动,不可再使用 let x = 5; makes_copy(x); // x 仍可用(i32 是 Copy 类型) } fn takes_ownership(some_string: String) { println!("{some_string}"); } // some_string 离开作用域,drop 被调用,内存释放 fn makes_copy(some_integer: i32) { println!("{some_integer}"); } // some_integer 离开作用域,无特殊操作

函数返回值与所有权转移

fn gives_ownership() -> String { let some_string = String::from("yours"); some_string // 所有权移动给调用者 } fn takes_and_gives_back(a_string: String) -> String { a_string // 原样返回所有权 } fn main() { let s1 = gives_ownership(); let s2 = String::from("hello"); let s3 = takes_and_gives_back(s2); // s2 已移动到函数内,不可再用;s3 拿到了返回的所有权 }

2.2 引用与借用(References & Borrowing)

问题:所有权转移导致使用不便

fn calculate_length(s: String) -> (String, usize) { let length = s.len(); (s, length) // 必须同时返回所有权,才能继续使用 } fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("'{}' 的长度是 {}", s2, len); }

解决方案:引用(Reference)

fn calculate_length(s: &String) -> usize { // 借用 String(不获取所有权) s.len() } fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // 创建指向 s1 的引用 println!("'{}' 的长度是 {}", s1, len); // s1 仍然有效! }

术语

含义

借用Borrowing

创建一个引用的行为

借用者

拿到引用的函数/变量

所有者

原始变量,始终拥有所有权

可变引用(Mutable Reference)

fn main() { let mut s = String::from("hello"); change(&mut s); println!("{s}"); // "hello, world" } fn change(s: &mut String) { s.push_str(", world"); }

可变引用的限制 —— 同一时刻只能有一个

let mut s = String::from("hello"); let r1 = &mut s; // let r2 = &mut s; // 编译错误!不能同时有两个可变引用 // println!("{r1} {r2}"); // 好处:在编译期防止数据竞争(data race) // 数据竞争的三个条件(Rust 在编译期全部排除): // 1. 两个或更多指针同时访问同一数据 // 2. 至少有一个指针在写入 // 3. 没有同步机制

不可变引用 vs 可变引用的互斥

let mut s = String::from("hello"); let r1 = &s; // 不可变引用 OK let r2 = &s; // 多个不可变引用 OK println!("{r1} and {r2}"); // r1, r2 的最后一次使用结束于此 let r3 = &mut s; // 可变引用 OK(r1/r2 已不再使用) println!("{r3}"); // 关键:NLL(Non-Lexical Lifetimes,非词法生命周期) // 作用域结束 ≠ 引用失效,引用在最后一次使用后就失效了

悬垂引用(Dangling References)—— Rust 在编译期防止

// 这段代码在 Rust 中无法编译(其他语言允许,是常见 bug 源) fn dangle() -> &String { let s = String::from("hello"); &s // 返回指向局部变量的引用 } // s 在此被 drop,引用变成悬垂指针 // 编译器报错:missing lifetime specifier // 正确写法:直接返回所有权 fn no_dangle() -> String { let s = String::from("hello"); s // 所有权移动出去,不会有悬垂引用 }

引用规则总结

1. 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用
2. 引用必须始终有效(不能悬垂)

2.3 切片(Slices)

字符串切片

let s = String::from("hello world"); let hello = &s[0..5]; // "hello" let world = &s[6..11]; // "world" let whole = &s[..]; // 等价于 &s[0..s.len()] let half = &s[..5]; // 等价于 &s[0..5] // 切片类型:&str(字符串字面量也是 &str) let s: &str = "hello"; // 类型是 &str(不可变)

字符串切片的危险性 —— UTF-8 边界

let s = String::from("你好"); // let h = &s[0..1]; // 运行时 panic!'你' 占 3 字节,不是 1 字节 // 正确做法: let h = &s[0..3]; // "你"(3 字节)

函数参数中的字符串切片

// 接受 &String(仅持有 String 引用时可用) fn first_word(s: &String) -> &str { ... } // 接受 &str(&String 和字符串字面量都能用!更通用) fn first_word(s: &str) -> &str { ... } fn main() { let s = String::from("hello world"); let word = first_word(&s); // 传入 &String,自动解引用为 &str let word2 = first_word("literal"); // 传入 &str 字面量,也 OK }

最佳实践:函数参数优先使用&str而非&String,更通用。

数组切片

let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; // &[i32] 类型,值为 [2, 3]

切片在内存中的表示

String:
┌─────────┬─────┬───────────┐
│ ptr │ len │ capacity │ ← 栈上
├─────────┼─────┼───────────┤
│ │ │ │
│ ┌───────────────────┐ │
│ │ h e l l o \0 │ │ ← 堆上
│ └───────────────────┘ │
└─────────┼─────┼───────────┘
&str 切片:
┌─────┬─────┐
│ ptr │ len │ ← 栈上(没有 capacity!)
└─────┴─────┘
└──→ 指向 String 堆数据的一部分

2.4 所有权决策树(速查)

需要传递数据给函数吗?
├── 函数需要修改数据?
│ └── 是 → 传 &mut T(可变引用)
└── 函数只需要读取?
├── 数据较小(Copy 类型)→ 传值(自动复制)
└── 数据较大 → 传 &T(不可变引用)
需要从函数返回数据吗?
├── 在函数内创建的数据 → 直接返回(所有权转移)
└── 调用方仍需持有 → 先 clone 再传
不确定该用什么?
→ 默认用引用(&T),编译器会告诉你何时需要所有权

学习目标检验

  • 能清晰解释所有权三条铁律
  • 能区分"移动"和"复制",知道哪些类型是 Copy
  • 能解释为什么 Rust 要禁止悬垂引用
  • 能写出正确的可变引用和不可变引用组合
  • 理解 NLL(非词法生命周期)的含义
  • 能正确使用字符串切片&str作为函数参数
  • 理解Stringvs&str的区别