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

11 KiB
Raw Permalink Blame History

Rust 所有权Ownership概念详解

所有权是 Rust 最独特的特性,它让 Rust 无需垃圾回收器就能保证内存安全。

目录


1. 什么是所有权

所有权是一套用于管理内存的规则。在大部分编程语言中,内存管理通过以下方式处理:

  • 手动管理(如 C/C++):程序员显式分配和释放内存
  • 垃圾回收(如 Java/Go运行时自动回收不再使用的内存
  • 所有权系统Rust通过编译时检查的一套规则管理内存

为什么需要所有权?

  • 无需垃圾回收,性能更高
  • 无需手动管理内存,更安全
  • 在编译期消除内存错误

2. 所有权规则

三条核心规则:

  1. Rust 中的每一个值都有一个所有者owner
  2. 值在任一时刻有且仅有一个所有者
  3. 当所有者离开作用域,值会被自动释放(调用 drop

3. 变量作用域

fn main() {
    // s 在这里无效,尚未声明
    let s = "hello";  // s 从此处开始有效

    // 可以使用 s
    println!("{}", s);
}  // 此作用域结束s 不再有效

4. String 类型与内存分配

字符串字面量 "hello" 是不可变的,编译时已知,存储在二进制文件中。对于在运行时可变的字符串,使用 String 类型。

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

    s.push_str(", world!"); // push_str() 追加字符串

    println!("{}", s); // 输出: hello, world!
}

内存分配与释放

String 类型的数据存储在上:

  • 栈:存储指针、长度、容量(固定大小,编译时已知)
  • 堆:存储实际的字符串内容(编译时大小未知,运行时动态分配)
         栈                     堆
    ┌──────────┐         ┌──────────┐
    │  ptr     │────────>│  h e l l o│
    │  len: 5  │         │  w o r l d│
    │  cap: 5  │         └──────────┘
    └──────────┘

当变量离开作用域时Rust 自动调用 drop 函数,将堆内存归还给操作系统。


5. 移动Move

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 对堆数据也做浅拷贝,当 s1s2 同时离开作用域时,它们都会尝试释放同一块堆内存,导致二次释放错误。

Rust 通过移动解决了这个问题:赋值后旧变量失效,只有一个所有者负责释放内存。

  移动前:                     移动后:
  s1 → [堆内存]              s2 → [堆内存]
                              s1  已失效 ❌

6. 克隆Clone

如果确实需要深度复制堆数据,使用 clone 方法:

let s1 = String::from("hello");
let s2 = s1.clone();  // 在堆上创建一份完整副本

println!("s1 = {}, s2 = {}", s1, s2); // ✅ 两者都可用
  克隆后:
  s1 → [堆内存1: "hello"]
  s2 → [堆内存2: "hello"]
  • clone 会执行深拷贝,包括堆数据
  • 代价较高,仅在确实需要时使用

7. Copy 特征

对于完全存储在栈上的数据类型Rust 提供了 Copy 特征:

let x = 5;
let y = x;      // x 被复制,而非移动
println!("{}", x); // ✅ 仍可使用

具有 Copy 的类型

  • 所有整数类型:u32i64
  • 所有浮点类型:f32f64
  • 布尔类型:bool
  • 字符类型:char
  • 元组(仅当所有元素都实现了 Copy(i32, bool) (i32, String)

不具有 Copy 的类型

  • String
  • Vec<T>
  • 任何实现了 Drop trait 的类型

8. 所有权与函数

将值传递给函数时,会发生移动复制,规则与赋值相同:

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. 返回值与作用域

函数的返回值也可以转移所有权

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所有权移出
}

元组返回多个值

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. 引用与借用

引用允许在不获取所有权的情况下使用值。

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
  s1    →    [堆内存: "hello"]
  ↑
  &s1引用指向 s1 的值)

引用的规则

  1. 引用不获取所有权
  2. 创建引用的行为称为借用borrowing
  3. 借用者离开作用域时,不会释放底层数据

11. 可变引用

默认情况下,引用是不可变的。要修改借用的值,需要使用可变引用

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!");
}

可变引用的限制

在任意给定时间,只能拥有以下两者之一:

  • 一个可变引用
  • 任意数量的不可变引用
let mut s = String::from("hello");

// ✅ 不可变引用可以同时存在多个
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);

// ✅ 不可变引用不再使用后,可以创建可变引用
let r3 = &mut s;
println!("{}", r3);
let mut s = String::from("hello");

let r1 = &s;         // 不可变借用
let r2 = &s;         // 不可变借用
let r3 = &mut s;     // ❌ 编译错误!不能同时存在不可变和可变引用

println!("{}, {}, {}", r1, r2, r3);
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;     // ❌ 编译错误!不能同时存在两个可变引用

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

这样设计的好处:在编译时防止数据竞争!

数据竞争发生的三个条件:

  1. 两个或多个指针同时访问同一数据
  2. 至少有一个指针用于写入
  3. 没有同步数据访问的机制

Rust 的规则直接在编译期消灭了数据竞争。


12. 悬垂引用

悬垂引用是指向已被释放内存的指针。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,将所有权移出去:

fn no_dangle() -> String {
    let s = String::from("hello");
    s  // 所有权移出
}

13. 切片类型

切片是一种不获取所有权的引用,允许引用集合中一段连续的元素。

字符串切片

let s = String::from("hello world");

let hello = &s[0..5];   // "hello"
let world = &s[6..11];  // "world"

语法简写:

let s = String::from("hello world");

let hello = &s[..5];    // 从 0 开始
let world = &s[6..];    // 到末尾
let whole = &s[..];     // 整个字符串

字符串字面量就是切片

let s = "Hello, world!";  // s 的类型是 &str即字符串切片

其他切片

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];  // 类型是 &[i32],包含 [2, 3]

assert_eq!(slice, &[2, 3]);

14. 总结

概念 说明
所有权 每个值有且仅有一个所有者;所有者离开作用域时值被释放
移动 赋值/传参时转移所有权,原变量失效
克隆 深拷贝堆数据,两个变量独立
Copy 栈上数据的自动复制(整数、布尔等基础类型)
引用 不获取所有权的指针,允许多个存在
可变引用 可修改数据的引用,同时只能有一个
切片 对集合局部元素的引用
借用规则 任意时刻:要么一个可变引用,要么多个不可变引用

借用检查器保证了

  • 内存安全:无悬垂指针、无二次释放、无使用后释放
  • 线程安全:无数据竞争
  • 零运行时开销:所有检查在编译时完成

所有权系统可能会让初学者感到"Rust 编译器在跟你作对",但请记住:编译器不是在刁难你,而是在保护你写出的每一行代码都是内存安全的。 当你熟悉所有权之后,这些规则会变得自然且合理。