目录

第二章 所有权系统

2.1 什么是所有权

所有权(Ownership)是Rust最独特的特性,它使得Rust能够在没有垃圾回收器(GC)的情况下保证内存安全。理解所有权是理解Rust的关键。

栈与堆

在深入了解所有权之前,需要理解栈(Stack)和堆(Heap)的区别:

栈(Stack):

堆(Heap):

所有权规则:

1. Rust中的每个值都有一个所有者(owner)
2. 值在任一时刻有且只有一个所有者
3. 当所有者离开作用域,值将被丢弃

2.2 变量作用域

作用域(Scope)是程序中一个项在有效性的范围:

{                      // s 在这里无效,它尚未声明
    let s = "hello";   // s 从这里开始有效
 
    // 使用 s
    println!("{}", s);
}                      // 作用域结束,s 不再有效
 
// println!("{}", s);  // 错误!s已失效

关键点:

2.3 String类型与内存分配

字符串字面量 vs String

let s = "hello";  // 字符串字面量,硬编码进程序,不可变
 
let mut s = String::from("hello");  // String类型,可变的
s.push_str(", world!");  // 追加字符串

为什么String可变而字符串字面量不可变?

内存分配方式

String的内存布局:

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

内存中的表示:

2.4 所有权转移(Move)

变量与数据交互的方式

1. 移动(Move):

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

当s1赋值给s2时,String的数据被复制了,包括:

但堆上的数据没有被复制!为了避免双重释放(double free)错误,Rust让s1失效。

图示:

赋值前:

s1
+--------+      +---------+
| 指针   | ---> | hello   |
| 长度 5 |      +---------+
| 容量 5 |
+--------+

赋值后(s1失效):

s2
+--------+      +---------+
| 指针   | ---> | hello   |
| 长度 5 |      +---------+
| 容量 5 |
+--------+

克隆(Clone)

如果需要深拷贝堆数据,使用clone方法:

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

注意:clone操作可能很昂贵,Rust通过显式调用来表明这一点。

只在栈上的数据:Copy trait

某些类型在赋值后仍然可用,因为它们实现了Copy trait:

let x = 5;
let y = x;  // Copy,不是Move
 
println!("x = {}, y = {}", x, y);  // 都有效

实现Copy trait的类型:

2.5 所有权与函数

将值传递给函数

fn main() {
    let s = String::from("hello");  // s进入作用域
 
    takes_ownership(s);              // s的值移动到函数里
    // println!("{}", s);            // 错误!s不再有效
 
    let x = 5;                       // x进入作用域
 
    makes_copy(x);                   // x被Copy到函数里
    println!("x = {}", x);           // 正确!x仍然有效
}
 
fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}  // some_string离开作用域,调用drop,内存释放
 
fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
}  // some_integer离开作用域,无需特殊处理

返回值与作用域

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);
}  // s3离开作用域被丢弃,s2已移动(不发生),s1被丢弃
 
fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string  // 返回并转移所有权
}
 
fn takes_and_gives_back(a_string: String) -> String {
    a_string  // 返回并转移所有权
}

2.6 引用与借用

什么是引用

为了避免所有权转移带来的不便,可以使用引用:

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离开作用域,但它没有所有权,所以不释放内存

借用(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");
}

可变引用的限制:

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

好处: 在编译期防止数据竞争(data race)。

数据竞争条件:

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

混合引用规则

let mut s = String::from("hello");
 
let r1 = &s;      // 没问题
let r2 = &s;      // 没问题
let r3 = &mut s;  // 大问题!

规则总结:

2.7 悬垂引用(Dangling References)

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 no_dangle() -> String {
    let s = String::from("hello");
    s  // 转移所有权
}

2.8 Slice类型

字符串Slice

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

let s = String::from("hello world");
 
let hello = &s[0..5];   // 从0开始到5(不包括5)
let world = &s[6..11];  // 从6开始到11(不包括11)

语法:

注意: slice的索引必须在有效的UTF-8字符边界。

字符串字面量就是slice

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

字符串字面量被直接存储在二进制文件中,&str是指向该位置的slice。

数组Slice

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

2.9 生命周期(初步介绍)

生命周期是Rust的另一个重要概念,将在第十四章深入讲解。这里简要介绍其必要性:

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这段代码会报错,因为编译器不知道返回的引用与哪个参数相关。需要使用生命周期标注:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

练习题

练习题2.1:理解所有权转移

预测以下代码的输出:

fn main() {
    let s = String::from("hello");
    let mut s2 = s;
    s2.push_str(" world");
 
    println!("{}", s2);
    // println!("{}", s);  // 能编译吗?
}

练习题2.2:借用练习

修复以下代码中的错误:

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

修复方案:

fn main() {
    let mut s = String::from("hello");
 
    {
        let r1 = &s;
        println!("{}", r1);
    }  // r1在这里离开作用域
 
    let r2 = &mut s;  // 现在可以了
    println!("{}", r2);
}

练习题2.3:实现第一个单词提取

编写函数返回字符串中的第一个单词:

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");
    let word = first_word(&my_string);
    println!("第一个单词是:{}", word);
}

练习题2.4:计算字符串字节长度

fn string_length(s: &String) -> usize {
    s.len()
}
 
fn main() {
    let s = String::from("hello");
    let len = string_length(&s);
    println!("长度:{}", len);
}

本章小结

本章深入讲解了Rust的所有权系统:

所有权系统是Rust内存管理的核心,也是Rust能够提供内存安全保证的基础。掌握所有权系统是成为Rust程序员的关键一步。

在下一章中,我们将详细学习Rust的基本数据类型。