跳到主要内容

Rust 所有权

所有权是 Rust 最独特的特性,它让 Rust 在没有垃圾回收器的情况下保证内存安全。理解所有权是掌握 Rust 的关键。

什么是所有权?

所有权是 Rust 用来管理内存的一套规则。与其他语言不同:

  • C/C++:手动管理内存,容易出现内存泄漏和悬垂指针
  • Java/Python:垃圾回收器自动管理,有运行时开销
  • Rust:通过所有权系统在编译时管理内存,零运行时开销

所有权规则

Rust 的所有权遵循三条核心规则:

  1. Rust 中每个值都有一个所有者(owner)
  2. 同一时间只能有一个所有者
  3. 当所有者离开作用域,值会被丢弃

变量作用域

作用域是变量有效存在的范围:

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

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

解释

  • 变量在声明时开始有效
  • 变量在作用域结束时失效
  • 当变量失效时,Rust 会自动清理其资源

String 类型

为了更好地理解所有权,我们需要区分栈上数据和堆上数据:

fn main() {
// 栈上的数据:固定大小,复制成本低
let x = 5;
let y = x; // 复制,x 和 y 都有效
println!("x = {}, y = {}", x, y);

// 堆上的数据:String 类型
let s1 = String::from("hello");
let s2 = s1; // 移动,s1 不再有效

// println!("{}", s1); // 错误!s1 已失效
println!("{}", s2); // 正确
}

String 与字符串字面量的区别

fn main() {
// 字符串字面量:不可变,编译时已知大小,存储在二进制文件中
let s: &str = "hello";

// String:可变,运行时分配,存储在堆上
let mut s = String::from("hello");
s.push_str(", world!"); // 可以修改
println!("{}", s);
}

移动(Move)

当一个值被赋值给另一个变量时,所有权会被转移:

fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移动到 s2

// s1 在这里已经无效
// println!("{}", s1); // 编译错误!

println!("{}", s2); // 正确
}

为什么需要移动?

String 由三部分组成:

  1. 指向堆上数据的指针
  2. 长度(len)
  3. 容量(capacity)

如果只是复制指针,会导致两个指针指向同一块堆内存,当它们离开作用域时,会尝试两次释放同一块内存(双重释放),这是严重的内存错误。

Rust 通过移动语义避免了这个问题:转移所有权后,原变量失效。

克隆(Clone)

如果需要深度复制堆上的数据,可以使用 clone 方法:

fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深度复制

println!("s1 = {}, s2 = {}", s1, s2); // 两个都有效
}

解释

  • clone 会复制堆上的数据
  • 这是一个显式操作,代码中很容易看出
  • 对于性能敏感的场景,需要谨慎使用

复制(Copy)

对于存储在栈上的简单类型,复制成本低,Rust 会自动复制:

fn main() {
let x = 5;
let y = x; // x 的值被复制到 y

println!("x = {}, y = {}", x, y); // 两个都有效
}

实现 Copy trait 的类型

  • 所有整数类型(i32, u64, etc.)
  • 浮点类型(f32, f64)
  • 布尔类型(bool)
  • 字符类型(char)
  • 元组(当所有元素都是 Copy 类型时)
  • 不可变引用(&T)
fn main() {
// Copy 类型的例子
let a = 5;
let b = a; // 复制
println!("a = {}, b = {}", a, b);

let c = (1, 2.0, true); // (i32, f64, bool) 都是 Copy
let d = c;
println!("c = {:?}, d = {:?}", c, d);

// 非 Copy 类型的例子
let e = (1, String::from("hello")); // String 不是 Copy
let f = e; // 移动
// println!("{:?}", e); // 错误!
}

函数与所有权

传递值给函数

将值传递给函数也会发生所有权转移:

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

take_ownership(s); // s 的所有权移动到函数

// println!("{}", s); // 错误!s 已失效

let x = 5;
make_copy(x); // x 是 Copy 类型,复制到函数

println!("{}", x); // 正确,x 仍有效
}

fn take_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,被丢弃

fn make_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,但因为是 Copy,不会有特殊操作

返回值与所有权

函数返回值也会转移所有权:

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

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 = 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) // 返回 String 和长度
}

引用和借用

传递所有权虽然安全,但有时不方便。Rust 提供了引用来解决这个问题。

引用

引用允许使用值但不获取所有权:

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

let len = calculate_length(&s1); // 传递引用

println!("字符串 '{}' 的长度是 {}", s1, len); // s1 仍然有效
}

fn calculate_length(s: &String) -> usize { // 参数是引用
s.len()
} // s 离开作用域,但因为没有所有权,不会丢弃数据

解释

  • &String 是对 String 的引用
  • &s1 创建一个指向 s1 的引用
  • 引用不拥有值,离开作用域时不会丢弃数据
  • 这种行为称为"借用"(borrowing)

可变引用

默认引用是不可变的,如果需要修改,需要可变引用:

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 有两条引用规则,在编译时检查:

  1. 任意时刻,要么有一个可变引用,要么有任意数量的不可变引用
  2. 引用必须总是有效的
fn main() {
let mut s = String::from("hello");

// 规则 1:可变引用和不可变引用不能同时存在
let r1 = &s; // 不可变引用
let r2 = &s; // 不可变引用
// let r3 = &mut s; // 错误!已有不可变引用

println!("{} {}", r1, r2); // r1 和 r2 最后一次使用

let r3 = &mut s; // 正确!r1 和 r2 已不再使用
r3.push_str(" world");

println!("{}", r3);
}

非词法作用域生命周期(NLL)

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

let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// r1 和 r2 在这之后不再使用

let r3 = &mut s; // 正确!因为 r1 和 r2 已不再使用
println!("{}", r3);
}

悬垂引用

Rust 在编译时防止悬垂引用:

fn main() {
let reference_to_nothing = dangle();
println!("{}", reference_to_nothing);
}

// 错误示例
fn dangle() -> &String { // 返回 String 的引用
let s = String::from("hello");

&s // 返回 s 的引用
} // s 离开作用域被丢弃,返回的引用指向已释放的内存

// 正确做法
fn no_dangle() -> String {
let s = String::from("hello");
s // 返回 String,所有权转移给调用者
}

切片

切片是对集合的部分引用,不拥有所有权。

字符串切片

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

// 字符串切片
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"

// 简写
let hello = &s[..5]; // 从开头到索引 5
let world = &s[6..]; // 从索引 6 到结尾
let whole = &s[..]; // 整个字符串

println!("{} {}", hello, world);
}

字符串切片的类型是 &str

fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

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

let word = first_word(&s);

println!("第一个单词: {}", word);

// 字符串字面量本身就是切片
let literal: &str = "hello world";
let word = first_word(&literal.to_string());
}

字符串切片作为参数

// 更好的函数签名,接受 &str
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

fn main() {
// 可以接受 String 的切片
let s = String::from("hello world");
let word = first_word(&s);
println!("{}", word);

// 也可以接受字符串字面量
let literal = "hello world";
let word = first_word(literal);
println!("{}", word);
}

数组切片

fn main() {
let a = [1, 2, 3, 4, 5];

// 数组切片
let slice = &a[1..3]; // [2, 3]

println!("{:?}", slice);
}

所有权模式总结

fn main() {
// 1. 移动
let s1 = String::from("hello");
let s2 = s1; // s1 移动到 s2
// s1 不再有效

// 2. 克隆
let s3 = String::from("hello");
let s4 = s3.clone(); // 深度复制
// s3 和 s4 都有效

// 3. 复制(Copy 类型)
let x = 5;
let y = x; // 栈上复制
// x 和 y 都有效

// 4. 不可变引用
let s5 = String::from("hello");
let r1 = &s5; // 借用
// s5 仍有效,r1 可以读取

// 5. 可变引用
let mut s6 = String::from("hello");
let r2 = &mut s6; // 可变借用
r2.push_str(" world");
// s6 仍有效,但通过 r2 修改

// 6. 切片
let s7 = String::from("hello");
let slice = &s7[0..2]; // "he"
}

小结

本章我们学习了:

  1. 所有权规则:每个值有唯一所有者,离开作用域时释放
  2. 移动:赋值和函数传参会转移所有权
  3. 克隆:使用 clone 深度复制堆数据
  4. 复制:Copy 类型会自动复制
  5. 引用和借用&T&mut T
  6. 引用规则:可变引用和不可变引用不能同时存在
  7. 切片:对集合的部分引用 &str&[T]

练习

  1. 编写一个函数,接收 String 并返回第一个单词
  2. 解释为什么以下代码无法编译:
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s;
    println!("{} {}", r1, r2);
  3. 编写一个函数,交换两个可变引用指向的值
  4. 使用切片重构字符串处理函数