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++ | 手动 | 忘记释放 → 内存泄漏;重复释放 → 悬垂指针 |
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的区别
