用户工具

站点工具


rust:第五章引用与借用

第五章 引用与借用

5.1 引用的概念

在Rust中,引用(Reference)允许你引用某个值而不获取其所有权。将创建一个引用的行为称为借用(Borrowing)

为什么需要引用

回顾所有权转移的问题:

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    // println!("{}", s1);  // 错误!s1已被移动
    println!("'{}'的长度是{}", s2, len);
}
 
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

这段代码很笨拙。使用引用可以优雅地解决:

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

图示:

&String s 指向 String s1
+--------+      +---------+
| 指针   | ---> | hello   |
| 长度 5 |      | 容量 5  |
+--------+      +---------+

5.2 引用规则

Rust对引用有严格的规则,在编译期确保内存安全。

基本规则:

1. 在任一给定时刻,只能满足下列条件之一:
   * 一个可变引用
   * 任意数量的不可变引用
2. 引用必须总是有效的(不能悬垂)

5.3 不可变引用

创建不可变引用

let s = String::from("hello");
let r = &s;  // 创建不可变引用
 
println!("{}", r);  // 通过引用访问值

可以同时存在多个不可变引用:

let s = String::from("hello");
 
let r1 = &s;
let r2 = &s;
let r3 = &s;
 
println!("{}, {}, {}", r1, r2, r3);  // 全部有效

不可变引用不能修改数据:

let s = String::from("hello");
let r = &s;
 
// r.push_str(", world");  // 错误!不可变引用不能修改数据

5.4 可变引用

创建可变引用

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

注意:

  • 变量本身必须是mut
  • 引用也必须是&mut
  • 函数参数类型必须是&mut T

可变引用的限制

同一作用域只能有一个可变引用:

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 = &mut s;
    println!("{}", r1);
}  // r1在这里离开作用域
 
let r2 = &mut s;  // 现在可以了
println!("{}", r2);

5.5 混合引用规则

不能同时存在可变和不可变引用:

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

原因: 不可变引用的用户不希望数据突然改变!

但可以在不可变引用不再使用后创建可变引用:

let mut s = String::from("hello");
 
let r1 = &s;
let r2 = &s;
println!("{} 和 {}", r1, r2);
// r1和r2在这里之后不再使用
 
let r3 = &mut s;  // 现在可以了
println!("{}", r3);

Rust编译器使用非词法生命周期(Non-Lexical Lifetimes, NLL),可以检测到r1和r2在println!后就不再使用。

5.6 悬垂引用(Dangling References)

悬垂指针(Dangling Pointer)是指向已经释放内存的指针。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  // 转移所有权
}

5.7 引用的本质

引用就是指针

引用本质上是受约束的指针:

  • 总是指向有效的内存
  • 编译器检查其生命周期
  • 保证遵循借用规则

引用的大小:

use std::mem;
 
fn main() {
    println!("&i32大小:{}", mem::size_of::<&i32>());
    println!("&String大小:{}", mem::size_of::<&String>());
    println!("&[i32]大小:{}", mem::size_of::<&[i32]>());
}
// 在64位系统上,引用都是8字节

自动解引用

Rust在某些情况下会自动解引用:

let s = String::from("hello");
let r = &s;
 
// 这两种写法等价
println!("长度:{}", r.len());   // 自动解引用
println!("长度:{}", (*r).len()); // 显式解引用

5.8 引用与函数参数

最佳实践

优先使用引用避免所有权转移:

// 好的做法:使用引用
fn print_length(s: &String) {
    println!("长度:{}", s.len());
}
 
// 不好的做法:转移所有权
fn print_length_bad(s: String) {
    println!("长度:{}", s.len());
}  // s在这里被释放,调用者不能再使用

API设计原则:

// 只读访问 -> &T
fn inspect(s: &String) {
    println!("{}", s);
}
 
// 需要修改 -> &mut T
fn append_world(s: &mut String) {
    s.push_str(" world");
}
 
// 需要获取所有权 -> T
fn take_ownership(s: String) {
    println!("{} 被获取", s);
}  // s在这里被释放

5.9 多重引用与嵌套引用

引用的引用

let x = 5;
let y = &x;      // &i32
let z = &y;      // &&i32
 
println!("z = {}", **z);  // 需要两次解引用

多重可变引用

let mut x = 5;
let y = &mut x;   // &mut i32
let z = &mut *y;  // 通过y重新借用
 
*z = 10;
println!("x = {}", x);

5.10 引用与模式匹配

匹配引用

let x = Some(5);
 
match &x {
    Some(val) => println!("值:{}", val),
    None => println!("无值"),
}

ref关键字

let x = 5;
let y = &x;
 
// 等价于
let ref z = x;
 
println!("z = {}", *z);

在模式匹配中使用:

let mut v = vec![1, 2, 3];
 
match v.get_mut(0) {
    Some(ref mut val) => *val = 10,
    None => (),
}
 
println!("{:?}", v);  // [10, 2, 3]

练习题

练习题5.1:交换两个数

fn swap(a: &mut i32, b: &mut i32) {
    let temp = *a;
    *a = *b;
    *b = temp;
}
 
fn main() {
    let mut x = 5;
    let mut y = 10;
 
    println!("交换前:x = {}, y = {}", x, y);
    swap(&mut x, &mut y);
    println!("交换后:x = {}, y = {}", x, y);
}

练习题5.2:字符串反转

fn reverse(s: &mut String) {
    let chars: Vec<char> = s.chars().collect();
    let reversed: String = chars.into_iter().rev().collect();
    *s = reversed;
}
 
fn main() {
    let mut s = String::from("hello");
    println!("原字符串:{}", s);
    reverse(&mut s);
    println!("反转后:{}", s);
}

练习题5.3:寻找最大值

fn find_max(arr: &[i32]) -> Option<&i32> {
    if arr.is_empty() {
        return None;
    }
 
    let mut max = &arr[0];
    for item in arr.iter() {
        if item > max {
            max = item;
        }
    }
    Some(max)
}
 
fn main() {
    let numbers = [3, 1, 4, 1, 5, 9, 2, 6];
 
    match find_max(&numbers) {
        Some(max) => println!("最大值是:{}", max),
        None => println!("数组为空"),
    }
}

练习题5.4:修复借用错误

下面代码有编译错误,请修复:

fn main() {
    let mut 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);
}

本章小结

本章深入讲解了Rust的引用与借用机制:

  • 引用(&T):借用值而不获取所有权
  • 可变引用(&mut T):可以修改借用的值
  • 借用规则
    • 任意数量的不可变引用 一个可变引用
    • 不能同时存在可变和不可变引用
  • 悬垂引用:Rust编译器保证引用总是有效
  • 非词法生命周期:编译器优化,更精确地追踪引用生命周期

理解引用与借用是掌握Rust的关键,这是Rust内存安全保证的核心机制。

rust/第五章引用与借用.txt · 最后更改: 127.0.0.1