Skip to content

4. Rust 所有权

4.1 所有权

所有权特性让 Rust 无需垃圾回收,同时保证内存安全。所有权特性不会减慢程序的运行速度,因为所有权特性是在编译时检查的。

Rust 对数据存放在堆上和栈上有着严格的规定。栈上的数据大小固定,数据存放在堆上的大小是动态的。

所有权解决的问题:

  • 跟踪代码的哪些部分正在使用堆上的哪些数据
  • 最小化堆上的重复数据
  • 清理堆上不再使用的数据

所有权规则:

  • Rust 中的每一个值都有一个被称为其所有者的变量
  • 一次只能有一个所有者
  • 当所有者(变量)离开作用域,这个值将被丢弃

String 类型的数据存放在堆上,字符串也是可以被修改的:

rust
fn main() {
    let mut s = String::from("hello");
    s.push_str(", world!");
    println!("{}", s);
}

当变量生命周期结束时,Rust 会自动调用 drop() 函数来释放内存。

String 数据的结构(栈):

名称内容
ptr指向堆上数据的指针
len长度
capacity容量

Rust 使用移动(move)来处理堆上数据的所有权。当变量赋值给另一个变量时,所有权会从一个变量转移到另一个变量。

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1); // error: value borrowed here after move
}

在这个例子中,Rust 阻止我们在将 s1 移动给 s2 后继续使用 s1,这样可以避免"悬垂指针"问题。

Rust 为了内存安全:

  • 没有尝试复制被分配的内存
  • s1 被移动到 s2 后,s1 无效了
  • s1 离开作用域后,也不需要释放任何内存

使用 .clone() 方法对 String 进行深拷贝:

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1 = {}, s2 = {}", s1, s2); // 有效,s1 仍然可用
}

拥有 Copy trait 的类型没有所有权问题,而所有需要使用堆内存的类型都没有 Copy trait。

以下类型都实现了 Copy trait:

  • 所有整数类型,如 u32
  • 布尔类型 bool
  • 浮点类型,如 f64
  • 字符类型 char
  • 仅包含实现了 Copy 的类型的元组,如 (i32, i32)

当元组的所有元素都有 Copy trait 时,此元组也有 Copy trait。

函数的参数和返回值也涉及所有权:

rust
fn main() {
    let s = String::from("hello");  // s 进入作用域
    
    takes_ownership(s);             // s 的值移动到函数里
                                    // 所以到这里 s 不再有效
    
    let x = 5;                      // x 进入作用域
    
    makes_copy(x);                  // x 应该移动到函数里
                                    // 但 i32 是 Copy 的,所以在后面可以继续使用 x
    
    println!("x: {}", x);           // 这行正常工作
    // println!("s: {}", s);        // 这行会报错
} // 这里,x 离开作用域,然后是 s。但因为 s 的值已被移动,没有特殊操作发生。

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 离开作用域并调用 `drop` 方法,释放内存

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 离开作用域,没有特殊操作

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

rust
fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值移动给 s1
    
    let s2 = String::from("hello");     // s2 进入作用域
    
    let s3 = takes_and_gives_back(s2);  // s2 被移动到函数里,函数返回值移动给 s3
    
    println!("s1: {}, s3: {}", s1, s3);
    // println!("s2: {}", s2);          // 这行会报错,s2 已被移动
} // 这里,s3, s1 离开作用域并被丢弃,s2 已被移动,所以什么也不会发生

fn gives_ownership() -> String {
    let some_string = String::from("yours"); // some_string 进入作用域
    some_string                              // 返回 some_string,所有权转移给调用者
}

fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
    a_string  // 返回 a_string,所有权转移给调用者
}

4.2 引用和借用

每次传递所有权并返回所有权比较繁琐。Rust 提供了"引用"机制,允许我们使用值但不获取所有权。

rust
fn main() {
    let s1 = String::from("hello");
    
    // &s1 创建一个指向 s1 的引用,但没有获取所有权
    let len = calculate_length(&s1);
    
    println!("'{}'的长度是 {}.", s1, len); // s1 依然有效,因为只是借用了引用
}

fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
    s.len()
} // 这里,s 离开作用域,但它只是一个引用,不拥有所引用的值,所以没有任何特殊操作

将"创建一个引用"的行为称为"借用"(borrowing)。默认情况下,引用是不可变的。

可变引用

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
fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s; // 错误:不能同时有两个可变引用

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

这个限制的好处是可以防止数据竞争。数据竞争在以下三种行为同时发生时会出现:

  • 两个或更多指针同时访问同一数据
  • 至少有一个指针被用来写入数据
  • 没有同步数据访问的机制

不能在同一时间同时拥有一个可变引用和一个不可变引用:

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

    let r1 = &s;     // 没问题
    let r2 = &s;     // 没问题
    let r3 = &mut s; // 错误:不能在拥有不可变引用的同时拥有可变引用

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

引用的作用域从声明的地方开始一直持续到最后一次使用为止:

rust
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);
} // r3 作用域结束

悬垂引用

Rust 编译器确保引用永远不会变成悬垂引用:

rust
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串
    
    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃,其内存被释放
  // 危险!这会返回一个指向被释放内存的引用

上面的函数会编译错误,Rust 不允许悬垂引用存在。正确做法是直接返回 String:

rust
fn no_dangle() -> String {
    let s = String::from("hello");
    s // 所有权被移动出去,所以没有值被释放
}

4.3 切片(Slice)类型

切片让你可以引用集合中部分连续的元素序列,而不用引用整个集合。切片是一种不持有所有权的引用。

字符串切片是 String 中一部分值的引用:

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

    let hello = &s[0..5];
    let world = &s[6..11];
    
    println!("{} {}", hello, world); // 输出: hello world
}

创建切片的语法:

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

let slice1 = &s[0..2];  // 包括索引0但不包括索引2
let slice2 = &s[..2];   // 如果从0开始,可以省略
let slice3 = &s[3..];   // 如果到末尾,可以省略
let slice4 = &s[..];    // 整个字符串的切片

例子:编写一个函数,接收一个字符串,返回其中第一个单词:

rust
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 mut s = String::from("hello world");
    
    let word = first_word(&s);
    
    s.clear(); // 错误:不能清空 s,因为 word 是一个引用
    
    println!("第一个单词是: {}", word);
}

字符串字面值就是切片:

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

可以将函数参数改为接受切片类型,这样更加通用:

rust
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() {
    let my_string = String::from("hello world");
    
    // 可以传入 String 的切片
    let word = first_word(&my_string[..]);
    
    // 也可以传入字符串字面值的切片
    let word2 = first_word("hello world");
    
    println!("{} {}", word, word2);
}

其他切片类型:

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

4.4 所有权实践

Rust 的所有权系统通过编译时规则确保内存安全,而不需要垃圾回收。理解所有权、借用和生命周期是掌握 Rust 的关键。

所有权系统的优点:

  • 内存安全无需垃圾回收
  • 编译时检查避免运行时错误
  • 并发安全性更高

总结:

  • 每个值只有一个所有者
  • 当所有者离开作用域,值将被丢弃
  • 可以通过引用借用值而不获得所有权
  • 引用可以是可变的或不可变的,但有限制
  • 切片是集合一部分的引用

掌握这些概念是编写高效、安全的 Rust 程序的基础。