所有权(Ownership)是Rust最独特的特性,它使得Rust能够在没有垃圾回收器(GC)的情况下保证内存安全。理解所有权是理解Rust的关键。
在深入了解所有权之前,需要理解栈(Stack)和堆(Heap)的区别:
栈(Stack):
堆(Heap):
所有权规则:
1. Rust中的每个值都有一个所有者(owner) 2. 值在任一时刻有且只有一个所有者 3. 当所有者离开作用域,值将被丢弃
作用域(Scope)是程序中一个项在有效性的范围:
{ // s 在这里无效,它尚未声明 let s = "hello"; // s 从这里开始有效 // 使用 s println!("{}", s); } // 作用域结束,s 不再有效 // println!("{}", s); // 错误!s已失效
关键点:
let s = "hello"; // 字符串字面量,硬编码进程序,不可变 let mut s = String::from("hello"); // String类型,可变的 s.push_str(", world!"); // 追加字符串
为什么String可变而字符串字面量不可变?
String的内存布局:
let s = String::from("hello");
内存中的表示:
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方法:
let s1 = String::from("hello"); let s2 = s1.clone(); // 深拷贝 println!("s1 = {}, s2 = {}", s1, s2); // 都有效
注意:clone操作可能很昂贵,Rust通过显式调用来表明这一点。
某些类型在赋值后仍然可用,因为它们实现了Copy trait:
let x = 5; let y = x; // Copy,不是Move println!("x = {}, y = {}", x, y); // 都有效
实现Copy trait的类型:
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 // 返回并转移所有权 }
为了避免所有权转移带来的不便,可以使用引用:
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; // 大问题!
规则总结:
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 // 转移所有权 }
字符串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字符边界。
let s = "Hello, world!"; // &str类型
字符串字面量被直接存储在二进制文件中,&str是指向该位置的slice。
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; // &[i32]类型,包含[2, 3]
生命周期是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 } }
预测以下代码的输出:
fn main() { let s = String::from("hello"); let mut s2 = s; s2.push_str(" world"); println!("{}", s2); // println!("{}", s); // 能编译吗? }
修复以下代码中的错误:
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); }
编写函数返回字符串中的第一个单词:
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); }
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的基本数据类型。