Rust 学习笔记 - Day 6: 引用与借用

0 阅读8分钟

今日主题:References and Borrowing

第一层:原理层(Why & How)

为什么需要借用?

Rust 的所有权系统。所有权规则很严格:

  • 每个值有且只有一个所有者
  • 当所有者离开作用域,值被丢弃

这带来一个问题:如果我们想多次使用一个值,但又不想转移所有权怎么办?

// 问题:s 被 move 到 calculate_length,之后无法再使用
let s = String::from("hello");
let len = calculate_length(s);  // s 被 move
println!("{}", s);              // ❌ 编译错误!s 已失效

解决方案:借用(Borrowing)——允许你使用值,但不获取所有权。

引用的本质

引用(Reference)是一个指向值的指针,但不拥有该值。

栈内存                    堆内存
┌─────────────┐          ┌─────────────┐
│   s         │─────────►│  "hello"    │
│  (String)   │          │             │
└─────────────┘          └─────────────┘
       │
       │ 引用 &s
       ▼
┌─────────────┐
│   &s        │──────┐
│  (&String)  │      │
└─────────────┘      └─────► 只读访问,不拥有所有权

借用规则(Borrowing Rules)

Rust 编译器在编译时强制执行以下规则:

  1. 任何时刻,要么有一个可变引用,要么有任意数量的不可变引用
  2. 引用必须总是有效的(不能指向已释放的内存)
借用规则示意图:

同一时间:
✅ &T + &T + &T      多个不可变引用
✅ &mut T            一个可变引用
❌ &mut T + &mut T   多个可变引用(数据竞争)
❌ &mut T + &T       可变 + 不可变(读写冲突)

为什么这些规则能保证安全?

数据竞争(Data Race) 的条件:

  1. 两个或多个指针同时访问同一数据
  2. 至少有一个是写操作
  3. 没有同步机制

Rust 的借用规则在编译期就阻止了数据竞争的可能性:

  • 如果有 &mut T,保证没有其他引用存在 → 独占访问,安全
  • 如果只有 &T,保证没有可变引用 → 只读访问,安全

生命周期与引用有效性

Rust 通过**生命周期(Lifetime)**确保引用总是指向有效数据:

{
    let r;                      // r 的生命周期开始
    {
        let x = 5;              // x 的生命周期开始
        r = &x;                 // r 借用 x
    }                           // x 被释放,r 变成悬垂引用
    println!("{}", r);          // ❌ 编译错误!r 指向无效内存
}                               // r 的生命周期结束

编译器会检查引用的生命周期不超过被引用值的生命周期。


第二层:实战层(What & Do)

不可变引用

fn main() {
    let s = String::from("hello");
    
    // 创建不可变引用
    let len = calculate_length(&s);
    
    // s 仍然有效,因为只是借用,不是 move
    println!("字符串 '{}' 的长度是 {}", s, len);
}

// 参数类型 &String 表示接受 String 的引用
fn calculate_length(s: &String) -> usize {
    s.len()  // 通过引用访问值,不需要解引用
}

关键点

  • &s 创建引用
  • &String 是引用类型
  • 使用引用时不需要 *s,Rust 自动解引用

可变引用

fn main() {
    let mut s = String::from("hello");
    
    // 创建可变引用
    change(&mut s);
    
    println!("{}", s);  // 输出: hello, world
}

// &mut String 表示可变引用
fn change(s: &mut String) {
    s.push_str(", world");  // 可以修改引用的值
}

关键点

  • &mut s 创建可变引用
  • 原变量必须是 mut
  • 可变引用允许修改值

多重不可变引用

fn main() {
    let s = String::from("hello");
    
    // 多个不可变引用是允许的
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    
    println!("{}, {}, {}", r1, r2, r3);
}

可变引用的限制

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

混合引用的限制

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("!");
    println!("{}", r3);
}

悬垂引用(Dangling References)

// ❌ 错误示例
fn dangle() -> &String {  // 返回引用
    let s = String::from("hello");
    &s  // 返回 s 的引用
}       // s 在这里离开作用域,内存被释放
        // 返回的引用指向无效内存!

// ✅ 正确做法:返回所有权
fn no_dangle() -> String {
    let s = String::from("hello");
    s   // 返回 String,所有权转移
}

引用的作用域

fn main() {
    let mut s = String::from("hello");
    
    {
        let r1 = &mut s;    // r1 的作用域开始
        r1.push_str(" world");
    }                       // r1 在这里结束
    
    // r1 已经结束,可以创建新的引用
    let r2 = &mut s;
    println!("{}", r2);
}

切片(Slice)——特殊的引用

切片是对集合的部分引用:

fn main() {
    let s = String::from("hello world");
    
    // 字符串切片
    let hello = &s[0..5];   // 从索引 0 到 4
    let world = &s[6..11];  // 从索引 6 到 10
    
    println!("{} {}", hello, world);
    
    // 数组切片
    let arr = [1, 2, 3, 4, 5];
    let slice = &arr[1..3];  // [2, 3]
    
    println!("{:?}", slice);
}

// 接受字符串切片(更通用)
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[..]
}

为什么 &str&String 更好?

  • &str 可以接收 String 和字符串字面量
  • 更灵活,更通用

第三层:最佳实践(Production Ready)

1. 优先使用不可变引用

// ✅ 好:只在需要修改时使用可变引用
fn process_data(data: &Vec<i32>) -> i32 {
    data.iter().sum()
}

// ❌ 坏:不必要的可变引用
fn process_data_bad(data: &mut Vec<i32>) -> i32 {
    data.iter().sum()
}

2. 使用切片类型增加灵活性

// ✅ 好:接受字符串切片
fn parse_config(content: &str) -> Config {
    // 可以接收 &String 和 &str
}

// ❌ 坏:只接受 String 引用
fn parse_config_bad(content: &String) -> Config {
    // 不能接收字符串字面量
}

3. 避免过长的借用

// ❌ 坏:借用时间过长
let data = get_data();
let ref1 = &data;
let ref2 = &data;
// ... 很多代码 ...
// ref1 和 ref2 一直存活,阻止了可变借用

// ✅ 好:限制借用范围
let result = {
    let data = get_data();
    let refs = (&data, &data);
    process(refs)
};  // 借用在这里结束
// 现在可以修改 data 了

4. 使用结构体组织相关引用

// ✅ 好:用结构体封装相关数据
struct ParsedData<'a> {
    header: &'a str,
    body: &'a str,
    footer: &'a str,
}

fn parse_document(content: &str) -> ParsedData {
    ParsedData {
        header: &content[0..10],
        body: &content[10..100],
        footer: &content[100..],
    }
}

5. 避免返回局部变量的引用

// ❌ 坏:返回局部变量的引用
fn get_string() -> &String {
    let s = String::from("hello");
    &s  // s 会被释放,返回悬垂引用
}

// ✅ 好:返回所有权
fn get_string() -> String {
    let s = String::from("hello");
    s
}

// ✅ 好:返回静态生命周期的引用
fn get_static_str() -> &'static str {
    "hello"  // 字符串字面量是 'static
}

6. 使用 Cow(Clone on Write)优化

use std::borrow::Cow;

// ✅ 好:避免不必要的克隆
fn process_string(s: Cow<str>) -> String {
    if s.contains("special") {
        // 需要修改时才克隆
        s.into_owned().replace("special", "normal")
    } else {
        // 不需要修改,不克隆
        s.into_owned()
    }
}

// 使用
let owned = String::from("hello");
process_string(Cow::Owned(owned));

let borrowed = "hello";
process_string(Cow::Borrowed(borrowed));

7. 文档注释说明借用关系

/// 解析配置文件
/// 
/// # Arguments
/// * `content` - 配置文件内容,函数执行期间必须保持有效
/// 
/// # Returns
/// 返回配置结构体,不包含对 `content` 的引用
pub fn parse_config(content: &str) -> Config {
    // ...
}

第四层:问题诊断(Troubleshooting)

问题 1:cannot borrow as mutable more than once

错误信息

error[E0499]: cannot borrow `s` as mutable more than once at a time

代码示例

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;  // ❌ 错误!

解决

let mut s = String::from("hello");
{
    let r1 = &mut s;
    // 使用 r1
}
let r2 = &mut s;  // ✅ r1 已经结束,可以创建 r2

问题 2:cannot borrow as mutable because it is also borrowed as immutable

错误信息

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

代码示例

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

解决

let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);  // 先使用完不可变引用

let r2 = &mut s;      // ✅ 现在可以创建可变引用了
r2.push_str(" world");

问题 3:does not live long enough(悬垂引用)

错误信息

error[E0597]: `s` does not live long enough

代码示例

fn get_ref() -> &String {
    let s = String::from("hello");
    &s  // ❌ s 会被释放
}

解决

// ✅ 返回所有权
fn get_string() -> String {
    let s = String::from("hello");
    s  // 所有权转移
}

// ✅ 或者返回 'static 引用
fn get_static_str() -> &'static str {
    "hello"
}

问题 4:slice indices are out of order

错误信息

error: slice indices are out of order

代码示例

let s = String::from("hello");
let slice = &s[3..1];  // ❌ 起始 > 结束

解决

let slice = &s[1..3];  // ✅ 起始 <= 结束

问题 5:mismatched types(生命周期不匹配)

错误信息

error[E0623]: lifetime mismatch

代码示例

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 }
}

第五层:权威引用(References)


今日总结

  1. 借用允许使用值而不获取所有权,通过 &T(不可变)和 &mut T(可变)创建
  2. 借用规则:同时只能有一个可变引用或任意数量的不可变引用
  3. 引用必须有效,编译器通过生命周期检查确保不返回悬垂引用
  4. 切片 &str&[T] 是对集合的部分引用,比完整引用更灵活
  5. 最佳实践:优先使用不可变引用、使用切片类型、限制借用范围、避免返回局部引用