====== 第二章 所有权系统 ======
===== 2.1 什么是所有权 =====
所有权(Ownership)是Rust最独特的特性,它使得Rust能够在没有垃圾回收器(GC)的情况下保证内存安全。理解所有权是理解Rust的关键。
==== 栈与堆 ====
在深入了解所有权之前,需要理解栈(Stack)和堆(Heap)的区别:
**栈(Stack):**
* 后进先出(LIFO)的数据结构
* 存储已知固定大小的数据
* 入栈和出栈速度快
* 数据必须拥有已知且固定的大小
**堆(Heap):**
* 用于存储编译时大小未知或可能变化的数据
* 在堆上分配内存称为分配(allocating)
* 访问堆数据比栈慢,需要指针寻址
* 灵活性更高,但管理更复杂
**所有权规则:**
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:在堆上分配,编译时大小未知,可以存储在运行时修改的文本
==== 内存分配方式 ====
**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的类型:**
* 所有整数类型(i32, u64等)
* 布尔类型(bool)
* 浮点类型(f32, f64)
* 字符类型(char)
* 仅包含Copy类型的元组(如(i32, i32),但(i32, String)不行)
===== 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)
**语法:**
* [开始索引..结束索引]:从开始到结束(不包括结束)
* [..2]:从0开始到2
* [3..]:从3开始到结尾
* [..]:整个字符串
**注意:** 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的所有权系统:
* **所有权规则**:每个值有且只有一个所有者,所有者离开作用域值被丢弃
* **Move语义**:赋值时所有权转移,原变量失效
* **Copy trait**:基本类型实现Copy,赋值时复制而非移动
* **借用**:通过引用访问数据而不获取所有权
* **可变引用**:同一作用域只能有一个可变引用
* **生命周期**:编译器通过生命周期确保引用总是有效
所有权系统是Rust内存管理的核心,也是Rust能够提供内存安全保证的基础。掌握所有权系统是成为Rust程序员的关键一步。
在下一章中,我们将详细学习Rust的基本数据类型。