返回文章列表
技术2026-04-10

Rust 入门:所有权与借用

从栈与堆、所有权规则、移动语义到引用和借用,深入理解 Rust 最核心的所有权系统。

为什么 Rust 需要所有权系统

在 C/C++ 中,内存管理完全由程序员手动控制:mallocfree 必须配对,忘记释放会导致内存泄漏,过早释放会导致悬垂指针。在 Java/Go 中,垃圾回收器(GC)自动管理内存,但代价是运行时的 STW(Stop The World)延迟和更高的内存占用。

Rust 选择了第三条路:所有权系统。它在编译时通过一套严格的规则检查内存的正确使用,不需要 GC,也不需要手动管理内存。如果你能通过 Rust 编译器的检查,你的程序就不会有内存泄漏、悬垂指针、数据竞争等问题。

栈与堆:内存分配的基础

理解所有权必须先理解 Rust 中的内存模型:

// 栈上分配:大小固定,编译时已知
let x: i32 = 42;
let y: [i32; 5] = [1, 2, 3, 4, 5];

// 堆上分配:大小可变,运行时确定
let s = String::from("hello"); // String 在堆上,指针在栈上
let v = vec![1, 2, 3];         // Vec 在堆上,指针在栈上

关键理解:String 类型的变量在栈上只存储了三个字段(指针、长度、容量),实际字符数据存储在堆上。

所有权规则

Rust 的所有权系统有三条核心规则:

  1. 每个值有且只有一个所有者(Owner)
  2. 当所有者离开作用域时,值被自动释放(drop)
  3. 同一时刻,一个值只能有一个所有者
fn main() {
    let s1 = String::from("hello"); // s1 拥有 "hello"
    let s2 = s1;                     // 所有权从 s1 移动(move)到 s2
    // println!("{}", s1);           // 编译错误!s1 已失效
    println!("{}", s2);              // OK
} // s2 离开作用域,"hello" 被释放

这种"移动后原变量失效"的设计,是 Rust 防止双重释放(double free)的核心机制。它从根源上杜绝了 C++ 中常见的 use-after-move 问题。

Move 语义 vs Clone

不是所有类型的赋值都会发生移动:

// 实现了 Copy trait 的类型:赋值时自动复制
let x = 42;
let y = x;    // x 被复制,x 仍然有效
println!("{}", x); // OK

// 未实现 Copy trait 的类型:赋值时发生移动
let s1 = String::from("hello");
let s2 = s1;  // s1 被移动,s1 失效

// 需要深度复制时使用 .clone()
let s3 = s2.clone(); // s2 和 s3 各自拥有独立的 "hello"
println!("{}", s2);  // OK
println!("{}", s3);  // OK

实现 Copy trait 的类型包括:所有整数类型、布尔类型、浮点类型、字符类型、以及它们的元组(如果所有字段都实现了 Copy)。StringVecBox 等堆分配类型没有实现 Copy。

引用与借用

如果每次传递数据都要转移所有权,编程会变得非常痛苦。Rust 提供了"引用"机制,允许在不转移所有权的情况下访问数据:

fn calculate_length(s: &String) -> usize {
    // s 是对 String 的引用,不拥有所有权
    s.len()
} // s 离开作用域,但因为不拥有所有权,不会释放任何东西

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // 借出 s1 的引用
    println!("{} 的长度是 {}", s1, len); // s1 仍然有效
}

创建引用的行为称为"借用"(Borrowing)。就像你借一本书看,看完要还回去,你不能把它撕了。

可变引用与借用规则

借用分为两种:不可变引用(&T)和可变引用(&mut T)。两者有严格的共存规则:

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

    let r1 = &s;      // 不可变借用
    let r2 = &s;      // 可以同时有多个不可变借用
    // let r3 = &mut s; // 错误!不能同时有不可变和可变借用
    println!("{} {}", r1, r2);

    // r1 和 r2 在这里不再使用

    let r3 = &mut s;  // 现在可以可变借用了
    r3.push_str(" world");
    println!("{}", r3);
}

借用规则可以总结为:

这条规则在编译时防止了数据竞争——两个线程同时访问同一数据,其中至少一个是写操作,且没有同步机制。

生命周期基础

生命周期(Lifetime)是 Rust 编译器用来确保引用始终有效的机制。大多数情况下,生命周期是隐式的,编译器会自动推断。但在引用之间存在复杂关系时,需要手动标注:

// 返回值引用与两个参数引用有关,编译器需要知道它们的关系
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("short");
    let s2 = String::from("longer");
    let result = longest(&s1, &s2);
    println!("最长的字符串是 {}", result);
}

'a 是一个生命周期标注,它告诉编译器:返回值的生命周期与两个参数中较短的那个相同。这确保了返回值不会在参数被释放后仍然存在。

String vs &str

这是 Rust 初学者最困惑的概念之一:

fn main() {
    let s = String::from("hello world"); // String
    let slice: &str = &s[0..5];           // 字符串切片,借用 s 的一部分
    println!("{}", slice);               // "hello"

    let literal: &str = "hello";          // 字符串字面量,存储在程序的二进制中
    let owned = literal.to_string();      // 转换为 String,在堆上分配
}

函数参数的选择原则:如果想接收字符串且不需要所有权,使用 &str(它同时接受 &String&str)。

常见所有权模式

返回所有权

fn take_and_give(s: String) -> String {
    // 做一些处理 ...
    s // 返回所有权
}

借用后修改再返回

fn modify_and_return(s: &mut String) {
    s.push_str(" modified");
}

Option 模式

fn find_user(id: u64) -> Option<&User> {
    // 返回引用而不是所有权,避免不必要的克隆
}

总结

所有权是 Rust 最独特的特性,也是学习曲线最陡峭的部分。但一旦你理解了它的设计哲学——"在编译时证明内存安全,而不是在运行时检查"——你就能理解为什么 Rust 能同时做到高性能和内存安全。初学时不要求一次理解透彻,多写代码,多和编译器"搏斗",所有权终将成为你的第二天性。


返回文章列表
标签:Rust所有权系统编程