learning-rust/part3/所有权概念.md

441 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Rust 所有权Ownership概念详解
> 所有权是 Rust 最独特的特性,它让 Rust 无需垃圾回收器就能保证内存安全。
## 目录
- [1. 什么是所有权](#1-什么是所有权)
- [2. 所有权规则](#2-所有权规则)
- [3. 变量作用域](#3-变量作用域)
- [4. String 类型与内存分配](#4-string-类型与内存分配)
- [5. 移动Move](#5-移动move)
- [6. 克隆Clone](#6-克隆clone)
- [7. Copy 特征](#7-copy-特征)
- [8. 所有权与函数](#8-所有权与函数)
- [9. 返回值与作用域](#9-返回值与作用域)
- [10. 引用与借用](#10-引用与借用)
- [11. 可变引用](#11-可变引用)
- [12. 悬垂引用](#12-悬垂引用)
- [13. 切片类型](#13-切片类型)
- [14. 总结](#14-总结)
---
## 1. 什么是所有权
所有权是一套用于管理内存的规则。在大部分编程语言中,内存管理通过以下方式处理:
- **手动管理**(如 C/C++):程序员显式分配和释放内存
- **垃圾回收**(如 Java/Go运行时自动回收不再使用的内存
- **所有权系统**Rust通过编译时检查的一套规则管理内存
### 为什么需要所有权?
- 无需垃圾回收,性能更高
- 无需手动管理内存,更安全
- 在编译期消除内存错误
---
## 2. 所有权规则
三条核心规则:
1. Rust 中的**每一个值**都有一个**所有者**owner
2. 值在任一时刻**有且仅有一个**所有者
3. 当所有者离开作用域,值会被**自动释放**(调用 `drop`
---
## 3. 变量作用域
```rust
fn main() {
// s 在这里无效,尚未声明
let s = "hello"; // s 从此处开始有效
// 可以使用 s
println!("{}", s);
} // 此作用域结束s 不再有效
```
---
## 4. String 类型与内存分配
字符串字面量 `"hello"` 是不可变的,编译时已知,存储在二进制文件中。对于在运行时可变的字符串,使用 `String` 类型。
```rust
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 追加字符串
println!("{}", s); // 输出: hello, world!
}
```
### 内存分配与释放
`String` 类型的数据存储在**堆**上:
- 栈:存储指针、长度、容量(固定大小,编译时已知)
- 堆:存储实际的字符串内容(编译时大小未知,运行时动态分配)
```text
栈 堆
┌──────────┐ ┌──────────┐
│ ptr │────────>│ h e l l o│
│ len: 5 │ │ w o r l d│
│ cap: 5 │ └──────────┘
└──────────┘
```
当变量离开作用域时Rust 自动调用 `drop` 函数,将堆内存归还给操作系统。
---
## 5. 移动Move
```rust
let x = 5;
let y = x; // x 的值被复制到 y栈上数据Copy 类型)
println!("{}", x); // ✅ 正常工作x 仍可用
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权**移动**到了 s2
// println!("{}", s1); // ❌ 编译错误s1 已失效
println!("{}", s2); // ✅ 正常工作
```
### 为什么需要移动?
如果 Rust 对堆数据也做浅拷贝,当 `s1``s2` 同时离开作用域时,它们都会尝试释放同一块堆内存,导致**二次释放**错误。
Rust 通过**移动**解决了这个问题:赋值后旧变量失效,只有一个所有者负责释放内存。
```text
移动前: 移动后:
s1 → [堆内存] s2 → [堆内存]
s1 已失效 ❌
```
---
## 6. 克隆Clone
如果确实需要深度复制堆数据,使用 `clone` 方法:
```rust
let s1 = String::from("hello");
let s2 = s1.clone(); // 在堆上创建一份完整副本
println!("s1 = {}, s2 = {}", s1, s2); // ✅ 两者都可用
```
```text
克隆后:
s1 → [堆内存1: "hello"]
s2 → [堆内存2: "hello"]
```
- `clone` 会执行**深拷贝**,包括堆数据
- 代价较高,仅在确实需要时使用
---
## 7. Copy 特征
对于**完全存储在栈上**的数据类型Rust 提供了 `Copy` 特征:
```rust
let x = 5;
let y = x; // x 被复制,而非移动
println!("{}", x); // ✅ 仍可使用
```
### 具有 Copy 的类型
- 所有整数类型:`u32`、`i64` 等
- 所有浮点类型:`f32`、`f64`
- 布尔类型:`bool`
- 字符类型:`char`
- 元组(仅当所有元素都实现了 Copy`(i32, bool)` ✅,`(i32, String)` ❌
### 不具有 Copy 的类型
- `String`
- `Vec<T>`
- 任何实现了 `Drop` trait 的类型
---
## 8. 所有权与函数
将值传递给函数时,会发生**移动**或**复制**,规则与赋值相同:
```rust
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权移动到函数内
// println!("{}", s); // ❌ 编译错误s 已失效
let x = 5;
makes_copy(x); // x 被复制到函数内i32 是 Copy 类型)
println!("{}", x); // ✅ x 仍然可用
} // x 离开作用域,然后 s 离开作用域(但 s 已被移动,不会发生什么)
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域drop 被调用,内存释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,无特殊操作(栈数据)
```
---
## 9. 返回值与作用域
函数的返回值也可以**转移所有权**
```rust
fn main() {
let s1 = gives_ownership(); // 返回值将所有权移给 s1
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 移入函数,函数返回后又移给 s3
// println!("{}", s2); // ❌ s2 已失效
println!("s1 = {}, s3 = {}", s1, s3);
} // s1、s3 离开作用域并被释放s2 已被移动,不触发释放
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string // 返回 some_string所有权移出
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 返回 a_string所有权移出
}
```
### 元组返回多个值
```rust
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("'{}' 的长度是 {}", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // 返回字符串和它的长度
}
```
这种写法有些繁琐Rust 提供了**引用**来简化。
---
## 10. 引用与借用
**引用**允许在不获取所有权的情况下使用值。
```rust
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传递引用,不转移所有权
println!("'{}' 的长度是 {}", s1, len); // ✅ s1 仍可使用
}
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // s 离开作用域,但由于它不拥有值的所有权,不会调用 drop
```
```text
s1 → [堆内存: "hello"]
&s1引用指向 s1 的值)
```
### 引用的规则
1. 引用**不获取**所有权
2. 创建引用的行为称为**借用**borrowing
3. 借用者离开作用域时,不会释放底层数据
---
## 11. 可变引用
默认情况下,引用是**不可变的**。要修改借用的值,需要使用**可变引用**
```rust
fn main() {
let mut s = String::from("hello");
change(&mut s); // 传递可变引用
println!("{}", s); // 输出hello, world!
}
fn change(some_string: &mut String) {
some_string.push_str(", world!");
}
```
### 可变引用的限制
**在任意给定时间,只能拥有以下两者之一:**
- 一个可变引用
- 任意数量的不可变引用
```rust
let mut s = String::from("hello");
// ✅ 不可变引用可以同时存在多个
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// ✅ 不可变引用不再使用后,可以创建可变引用
let r3 = &mut s;
println!("{}", r3);
```
```rust
let mut s = String::from("hello");
let r1 = &s; // 不可变借用
let r2 = &s; // 不可变借用
let r3 = &mut s; // ❌ 编译错误!不能同时存在不可变和可变引用
println!("{}, {}, {}", r1, r2, r3);
```
```rust
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ❌ 编译错误!不能同时存在两个可变引用
println!("{}, {}", r1, r2);
```
**这样设计的好处:在编译时防止数据竞争!**
> 数据竞争发生的三个条件:
> 1. 两个或多个指针同时访问同一数据
> 2. 至少有一个指针用于写入
> 3. 没有同步数据访问的机制
Rust 的规则直接在编译期消灭了数据竞争。
---
## 12. 悬垂引用
**悬垂引用**是指向已被释放内存的指针。Rust 编译器保证永远不会产生悬垂引用:
```rust
fn main() {
let reference_to_nothing = dangle(); // ❌ 编译错误!
}
fn dangle() -> &String {
let s = String::from("hello");
&s // 返回 s 的引用
} // s 离开作用域,内存被释放;但返回的引用仍指向已释放的内存
```
编译器错误信息:
```
error[E0106]: missing lifetime specifier
|
| fn dangle() -> &String {
| ^ expected named lifetime parameter
```
正确的做法是直接返回 `String`,将所有权移出去:
```rust
fn no_dangle() -> String {
let s = String::from("hello");
s // 所有权移出
}
```
---
## 13. 切片类型
**切片**是一种不获取所有权的引用,允许引用集合中一段连续的元素。
### 字符串切片
```rust
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
```
语法简写:
```rust
let s = String::from("hello world");
let hello = &s[..5]; // 从 0 开始
let world = &s[6..]; // 到末尾
let whole = &s[..]; // 整个字符串
```
### 字符串字面量就是切片
```rust
let s = "Hello, world!"; // s 的类型是 &str即字符串切片
```
### 其他切片
```rust
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 类型是 &[i32],包含 [2, 3]
assert_eq!(slice, &[2, 3]);
```
---
## 14. 总结
| 概念 | 说明 |
|------|------|
| **所有权** | 每个值有且仅有一个所有者;所有者离开作用域时值被释放 |
| **移动** | 赋值/传参时转移所有权,原变量失效 |
| **克隆** | 深拷贝堆数据,两个变量独立 |
| **Copy** | 栈上数据的自动复制(整数、布尔等基础类型) |
| **引用** | 不获取所有权的指针,允许多个存在 |
| **可变引用** | 可修改数据的引用,同时只能有一个 |
| **切片** | 对集合局部元素的引用 |
| **借用规则** | 任意时刻:要么一个可变引用,要么多个不可变引用 |
### 借用检查器保证了
- **内存安全**:无悬垂指针、无二次释放、无使用后释放
- **线程安全**:无数据竞争
- **零运行时开销**:所有检查在编译时完成
> 所有权系统可能会让初学者感到"Rust 编译器在跟你作对",但请记住:**编译器不是在刁难你,而是在保护你写出的每一行代码都是内存安全的。** 当你熟悉所有权之后,这些规则会变得自然且合理。