引用和借用

Rust 的引用(references)和借用(borrowing)是所有权系统的扩展部分,它们允许你访问数据而不转移所有权。这确保了内存安全,同时避免了不必要的拷贝。借用规则在编译时强制执行,防止数据竞争和无效引用(如悬垂指针)。引用用 & 表示,是指向值的指针,但 Rust 保证它们始终有效。

1. 引用和借用简介

  • 引用(&T):一个指向类型 T 的值的指针,不拥有值。
  • 借用:创建引用的过程。借用是临时的,作用域结束时结束。
  • 为什么使用?:避免移动所有权或昂贵拷贝,同时访问数据。
  • 类型
    • 不可变引用:&T – 读访问,不能修改。
    • 可变引用:&mut T – 读写访问,可以修改。
  • 解引用:用 * 访问引用的值,如 *ref

示例:基本引用

fn main() {
    let x = 5;
    let y = &x;  // y 是 x 的不可变引用

    println!("x: {}, y: {}", x, *y);  // 输出: x: 5, y: 5
    // *y = 10;  // 错误!不可变引用不能修改
}
  • 解释:y 借用 x,但不拥有。x 仍有效。引用是栈上的指针,指向 x 的位置。

2. 不可变借用

不可变借用允许多个同时存在,因为它们不修改数据。

示例:函数中的不可变借用

fn print_length(s: &String) {  // 借用 &String
    println!("长度: {}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_length(&s);  // 传递引用
    print_length(&s);  // 可以多次借用
    println!("原值: {}", s);  // s 仍拥有所有权
}
  • 解释:函数借用 s,不转移所有权。多个 & 借用 OK,因为是只读的。Rust 允许无限个不可变借用。

多引用示例

fn main() {
    let mut s = String::from("hello");  // mut 不是必需,但这里演示
    let r1 = &s;
    let r2 = &s;
    println!("r1: {}, r2: {}", r1, r2);  // 有效
}

3. 可变借用

可变借用允许修改,但同一时间只能有一个(独占访问)。

示例:可变借用

fn append_world(s: &mut String) {
    s.push_str(", world!");
}

fn main() {
    let mut s = String::from("hello");  // 必须 mut
    append_world(&mut s);
    println!("{}", s);  // 输出: hello, world!
}
  • 解释&mut 传递可变引用。借用期间,s 不能被其他方式访问:
    #![allow(unused)]
    fn main() {
    let mut s = String::from("hello");
    let r = &mut s;
    // println!("{}", s);  // 错误!不能在可变借用时访问 s
    // let r2 = &mut s;    // 错误!不能有第二个可变借用
    r.push_str("!");
    }

4. 借用规则

Rust 的借用检查器(borrow checker)强制这些规则:

  1. 任何值,在给定作用域内,可以有:
    • 一个可变引用,或
    • 任意多个不可变引用。 但不能同时两者。
  2. 引用必须始终有效(无悬垂引用)。
  3. 借用不能超过所有者的生命周期。

示例:借用冲突

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;      // 不可变
    let r2 = &s;      // 另一个不可变 OK
    // let r3 = &mut s;  // 错误!有不可变借用时不能可变借用
    println!("{}, {}", r1, r2);
}
  • 解释:规则防止数据竞争。如果允许同时 & 和 &mut,修改可能使不可变引用失效。

5. 悬垂引用(Dangling References)

Rust 防止返回局部变量的引用,因为所有者超出作用域会导致引用悬垂。

示例:悬垂引用错误

#![allow(unused)]
fn main() {
// fn dangle() -> &String {  // 错误!返回局部引用
//     let s = String::from("hello");
//     &s
// }  // s drop,引用无效
}
  • 解释:编译错误:missing lifetime specifier。Rust 要求指定生命周期(详见生命周期教程)。正确方式:返回所有权或用静态生命周期。

正确示例:静态引用

#![allow(unused)]
fn main() {
fn static_ref() -> &'static str {
    "hello"  // 字符串字面量是 'static
}
}

6. 引用与切片

切片是引用的子集,如数组或字符串的部分。

示例:字符串切片

fn first_word(s: &str) -> &str {  // &str 是 String 或 str 的借用
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

fn main() {
    let s = String::from("hello world");
    let word = first_word(&s);  // 借用
    println!("{}", word);  // 输出: hello
    // s.clear();  // 错误!word 借用期间不能修改 s
}
  • 解释:切片借用规则相同。修改 s 会使 word 失效,但借用 checker 防止它。

7. 引用与所有权的交互

  • 借用后不能移动:借用存在时,所有者不能被移动。
  • 解引用强制:某些操作需要 *,但方法调用隐式解引用(deref coercion)。
  • Deref trait:自定义类型可以实现 Deref 以像引用一样行为(如 smart pointers)。

示例:Deref 强制

fn main() {
    let s = String::from("hello");
    let r = &s;
    println!("{}", r.len());  // 隐式 *r.len()
}

8. 最佳实践和常见陷阱

  • 优先借用:避免 clone,除非必要。
  • 最小借用作用域:用 {} 限制借用,早释放锁。
  • mut 只在必要时:减少可变借用以允许更多不可变访问。
  • 常见错误
    • 借用冲突:在循环中借用集合同时修改(用索引或迭代器代替)。
    • 悬垂引用:返回函数局部引用(用所有权返回或生命周期)。
    • 未 mut 变量:借用 &mut 时,所有者必须 mut。
  • 与生命周期结合:复杂借用需生命周期注解(如 'a)。
  • 性能:引用是零成本,编译时检查无运行时开销。

练习建议

  1. 编写函数,接收 &Vec,返回最大值的引用。
  2. 创建 struct,用 &mut 修改其字段。
  3. 尝试切片数组,处理边界借用冲突。

如果需要更多示例、与生命周期的集成,或特定场景的调试,请提供细节!