Skip to content

Day 6: 引用与借用

今天是《30天精通Rust语言》系列的第六天。我们将学习Rust中极其重要的概念——引用(References)和借用(Borrowing)。引用允许你在不获取值所有权的情况下使用值,这是Rust最强大也最独特的特性之一。

引用允许你暂时借用一个值的使用权,而不获取其所有权。用 & 创建不可变引用,用 &mut 创建可变引用。

fn main() {
let s = String::from("hello");
let len = calculate_length(&s);
println!("The length of '{}' is {}", s, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}

在这个例子中,&s 表示创建对 s 的引用并传递给函数。函数calculate_length并不拥有这个字符串,它只是借用它。

当需要修改借用的值时,使用可变引用:

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

注意:可变变量 s 才能创建可变引用,且需要显式声明 &mut。

Rust的借用规则确保了内存安全:

  1. 任意数量的不可变引用:可以同时拥有多个不可变引用。
  2. 只有一个可变引用:可变引用与不可变引用不能同时存在,且同一作用域只能有一个可变引用。
  3. 引用必须总是有效的:不能创建悬垂引用(dangling reference)。
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{}, {}, {}", r1, r2, r3);
}

3.2 规则验证:可变与不可变引用冲突

Section titled “3.2 规则验证:可变与不可变引用冲突”
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可变引用
let r2 = &s; // 不可变引用
// let r3 = &mut s; // 编译错误!不能同时存在可变和不可变引用
println!("{} and {}", r1, r2);
// r1和r2在此之后不再使用,可以创建可变引用
let r3 = &mut s; // 现在可以了
r3.push_str(", world");
}

悬垂引用是指指向一个已被释放内存的引用。Rust的借用规则确保编译器能检测并阻止悬垂引用的创建:

fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s // 编译错误!s将在函数结束时被drop,返回的引用无效
}

编译错误:

error[E0106]: missing lifetime specifier

Rust在编译时就拒绝了这种危险的代码,而不像C/C++那样在运行时产生未定义行为。

利用可变引用,函数可以修改外部变量:

fn append_elements(elements: &mut Vec<i32>) {
elements.push(1);
elements.push(2);
elements.push(3);
}
fn main() {
let mut v = Vec::new();
append_elements(&mut v);
println!("{:?}", v); // [1, 2, 3]
}

合理安排借用的作用域,可以同时满足借用规则:

fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1和r2在这里使用完毕
let r3 = &mut s; // 可以创建可变引用了
r3.push_str(", world");
println!("{}", r3);
}

Rust的借用检查器(Borrow Checker)是编译器的一部分,负责分析代码中的借用是否遵循规则。它通过追踪变量的引用和作用域来判断代码是否安全。

理解借用检查器的一个技巧:引用的作用域结束于最后一次使用该引用的地方,而非函数或代码块的末尾。

对象的方法可以借用对象,也可以获取对象的所有权:

struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// &self 不可变借用
fn area(&self) -> u32 {
self.width * self.height
}
// &mut self 可变借用
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
}
fn main() {
let mut rect = Rectangle { width: 10, height: 5 };
println!("Area: {}", rect.area()); // 50
rect.scale(2);
println!("Scaled: {}x{}", rect.width, rect.height); // 20x10
}

今天的实践任务:

  1. 编写函数接收两个 &String 参数,返回较长的字符串的引用。
  2. 编写程序验证:创建两个不可变引用后尝试创建可变引用,观察编译错误。
  3. 编写程序验证悬垂引用:尝试返回局部变量的引用,观察编译错误。
  4. 实现一个 Counter 结构体,提供 add(&mut self) 方法增加计数,验证可变借用的行为。
  5. 编写程序处理一个字符串向量,使用不可变引用遍历,可变引用修改特定元素。

问:为什么Rust要限制可变引用的数量?

答:同时存在多个可变引用可能导致数据竞争(data race)。Rust的设计目标是在编译时消除数据竞争,即使这意味着更严格的借用规则。

问:是否可以返回借用的引用而不是值?

答:可以,但需要使用生命周期注解(下一章内容),确保返回的引用与输入引用的生命周期关联。

今天我们深入学习了Rust的引用与借用:

  • 引用用 & 创建不可变借用,用 &mut 创建可变借用。
  • 借用规则:多个不可变引用 OR 一个可变引用(不能同时存在)。
  • 借用规则在编译时就消除悬垂引用和数据竞争。
  • 理解引用的作用域对于编写正确代码至关重要。

明天我们将学习结构体(Struct),了解如何创建自定义数据类型。加油!