Skip to content

所有权

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

在开始之前,让我们先了解内存管理的背景:

  • C/C++:手动管理内存(malloc/free),容易出错
  • Java/Python/Go:垃圾回收(GC),有运行时开销
  • Rust:所有权系统,编译时检查,零运行时开销

理解栈和堆有助于理解所有权。

  • 后进先出(LIFO)
  • 分配和释放非常快
  • 存储大小固定的数据
  • 例如:整数、浮点数、布尔值、固定大小的数组
  • 存储大小可变的数据
  • 分配和释放相对较慢
  • 需要指针来访问
  • 例如:StringVec
fn main() {
let x = 5; // 存储在栈上
let s = String::from("hello"); // 数据存储在堆上,s 是指向堆的指针
}

Rust 的所有权系统基于三条规则:

  1. 每个值都有一个所有者(owner)
  2. 值在任一时刻只能有一个所有者
  3. 当所有者离开作用域,值被丢弃(drop)
fn main() {
{
let s = String::from("hello"); // s 进入作用域,成为 "hello" 的所有者
println!("{}", s);
} // s 离开作用域,"hello" 被丢弃,内存被释放
}

对于栈上的简单类型,赋值是复制:

fn main() {
let x = 5;
let y = x; // 复制,x 和 y 都是 5
println!("x = {}, y = {}", x, y); // 正常工作
}

对于堆上的数据,赋值是移动所有权:

fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移动到 s2
// println!("{}", s1); // 错误!s1 不再有效
println!("{}", s2); // 正常工作
}

为什么?看看内存中发生了什么:

s1 (移动后无效) s2
┌──────────────┐ ┌──────────────┐
│ ptr ────┼──× │ ptr ────┼──┐
│ len: 5 │ │ len: 5 │ │
│ capacity: 5 │ │ capacity: 5 │ │
└──────────────┘ └──────────────┘ │
堆 ▼
┌─────────────────┐
│ h │ e │ l │ l │ o │
└─────────────────┘

如果允许 s1s2 同时有效,当它们离开作用域时会尝试释放同一块内存——这就是”双重释放”错误。Rust 通过移动语义避免了这个问题。

如果确实需要深拷贝堆上的数据,使用 clone()

fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝
println!("s1 = {}, s2 = {}", s1, s2); // 都有效
}

clone() 会在堆上创建数据的完整副本,可能开销较大。

实现了 Copy trait 的类型在赋值时会复制而不是移动:

fn main() {
let x: i32 = 5;
let y = x; // 复制,因为 i32 实现了 Copy
println!("x = {}, y = {}", x, y);
}

实现 Copy 的类型包括:

  • 所有整数类型(i32u64 等)
  • 布尔类型 bool
  • 浮点类型(f32f64
  • 字符类型 char
  • 只包含 Copy 类型的元组,如 (i32, i32)

规则:如果一个类型实现了 Drop trait,就不能实现 Copy

将值传递给函数和赋值类似——会移动或复制:

fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权移动到函数
// println!("{}", s); // 错误!s 不再有效
let x = 5;
makes_copy(x); // x 被复制
println!("{}", x); // 正常工作
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,内存被释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
}

函数可以通过返回值转移所有权:

fn main() {
let s1 = gives_ownership(); // 获得所有权
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 移动进去,s3 获得返回的所有权
}
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string // 返回并移动所有权给调用者
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 返回并移动所有权给调用者
}

每次都转移所有权太麻烦了。引用允许你使用值但不获取所有权:

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 离开作用域,但它不拥有数据,所以什么都不会发生

创建引用的行为称为借用(borrowing)

s1 s(引用)
┌──────────────┐ ┌──────────────┐
│ ptr ────┼──┐ │ ptr ────┼──┐
│ len: 5 │ │ └──────────────┘ │
│ capacity: 5 │ │ │
└──────────────┘ │ │
▼ │
堆 └──────────────────────────┘
┌─────────────────┐
│ h │ e │ l │ l │ o │
└─────────────────┘

引用默认不可变。使用 &mut 创建可变引用:

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 的借用规则确保内存安全:

同一时间只能有一个可变引用:

fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // 错误!不能同时有两个可变引用
println!("{}", r1);
}

这防止了数据竞争。

规则 2:可变和不可变引用不能共存

Section titled “规则 2:可变和不可变引用不能共存”
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
// let r3 = &mut s; // 错误!已经有不可变引用了
println!("{}, {}", r1, r2);
}

引用的作用域从声明处开始,到最后一次使用结束:

fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 和 r2 在这之后不再使用,作用域结束
let r3 = &mut s; // 现在可以创建可变引用了
println!("{}", r3);
}

Rust 编译器保证不会有悬垂引用(指向已释放内存的引用):

fn main() {
// let reference_to_nothing = dangle(); // 编译错误
}
fn dangle() -> &String { // 错误!返回悬垂引用
let s = String::from("hello");
&s // 返回 s 的引用
} // s 在这里被释放,引用将指向无效内存!

解决方法是返回所有权:

fn no_dangle() -> String {
let s = String::from("hello");
s // 移动所有权给调用者
}

切片是对集合中部分元素的引用:

fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
// 简写
let hello = &s[..5]; // 从开头
let world = &s[6..]; // 到结尾
let whole = &s[..]; // 整个字符串
println!("{} {}", hello, world);
}

字符串切片的类型是 &str

let s = "Hello, world!"; // s 的类型是 &str

字符串字面量是指向二进制文件中某处的切片,这也是为什么它们是不可变的。

fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3],类型是 &[i32]
assert_eq!(slice, &[2, 3]);
}

以下代码无法编译,请修复它:

fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // 错误!
}

提示:有两种修复方法,尝试两种都实现。

编写一个函数 string_length,计算字符串长度但不获取所有权:

fn string_length(/* 参数 */) -> usize {
// 你的代码
}
fn main() {
let s = String::from("hello");
let len = string_length(/* 参数 */);
println!("'{}' 的长度是 {}", s, len); // s 仍然可用
}

编写一个函数 append_world,在字符串末尾添加 ”, world”:

fn append_world(/* 参数 */) {
// 你的代码
}
fn main() {
let mut s = String::from("hello");
append_world(/* 参数 */);
println!("{}", s); // 输出: hello, world
}

以下代码为什么无法编译?如何修复?

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

理解了所有权后,下一章我们将学习与之密切相关的概念——生命周期。