借用与引用

借用(Borrowing)是 Rust 中在不获取所有权的情况下访问数据的机制。

引用

引用允许你引用某个值而不取得其所有权:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);  // 借用 s1

    println!("'{}' 的长度是 {}", s1, len);  // ✅ s1 仍然有效
}

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s 离开作用域,但不释放数据(因为没有所有权)

&s1 创建一个指向 s1 的引用,但不拥有它。

Loading diagram...

引用不可变

默认情况下,引用是不可变的

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    // some_string.push_str(", world");  // ❌ 编译错误:不能修改引用的值
}

可变引用

使用 &mut 创建可变引用

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");  // ✅ 可以修改
}

可变引用的限制

同一时间,同一作用域内,只能有一个可变引用

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    // let r2 = &mut s;  // ❌ 编译错误:不能同时有两个可变引用

    println!("{}", r1);
}

好处:防止数据竞争(data race)。

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

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;      // ✅ 不可变引用
    let r2 = &s;      // ✅ 多个不可变引用是允许的
    // let r3 = &mut s;  // ❌ 不能在存在不可变引用时创建可变引用

    println!("{} and {}", r1, r2);
}

借用检查器可视化

借用检查器可视化

1let mut s = String::from("hello");
2let r1 = &s;
3let r2 = &s;
4let r3 = &mut s; // ❌ 错误
合法借用
创建可变变量 s,当前无借用
s
无借用
当前借用者:
借用规则
  • ✓ 可以有多个不可变引用(&T)
  • ✗ 不可变引用存在时,不能有可变引用(&mut T)
  • ✓ 只能有一个可变引用,且无其他引用

引用的作用域

引用的作用域从声明开始,持续到最后一次使用

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // r1 和 r2 的作用域在这里结束(最后一次使用)

    let r3 = &mut s;  // ✅ 可以创建可变引用
    println!("{}", r3);
}

这个特性叫做非词法作用域生命周期(Non-Lexical Lifetimes, NLL)。

悬垂引用

Rust 编译器保证引用永远不会成为悬垂引用(dangling reference):

fn main() {
    let reference_to_nothing = dangle();
}

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

修复方法:返回所有权而不是引用:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {  // ✅ 正确
    let s = String::from("hello");
    s  // 移动所有权
}

借用规则总结

Loading diagram...

三大规则

  1. 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用
  2. 引用必须总是有效的(不能悬垂)
  3. 引用的生命周期不能超过被引用数据的生命周期

引用与解引用

使用 * 解引用:

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);  // 解引用 y 获取值
}

自动解引用

Rust 在某些情况下会自动解引用(如方法调用):

fn main() {
    let s = String::from("hello");
    let len = s.len();  // 自动解引用

    let r = &s;
    let len = r.len();  // 自动解引用 r,调用 String 的 len 方法
}

多重引用

可以创建引用的引用:

fn main() {
    let x = 5;
    let y = &x;      // &i32
    let z = &y;      // &&i32

    assert_eq!(5, **z);  // 两次解引用
}

切片(Slice)

切片是一种特殊的引用,引用集合的连续序列:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];    // "hello"
    let world = &s[6..11];   // "world"

    // 简写
    let hello = &s[..5];     // 等同于 &s[0..5]
    let world = &s[6..];     // 等同于 &s[6..11]
    let whole = &s[..];      // 整个字符串

    println!("{}", hello);
}

字符串字面量就是切片

fn main() {
    let s: &str = "Hello, world!";  // &str 是字符串切片类型
}

数组切片

fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice: &[i32] = &a[1..3];  // [2, 3]
    assert_eq!(slice, &[2, 3]);
}

实践示例

示例 1:安全的字符串分割

fn first_word(s: &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 mut s = String::from("hello world");
    let word = first_word(&s);  // 不可变借用

    // s.clear();  // ❌ 编译错误:不能在存在不可变借用时修改
    println!("第一个单词: {}", word);
}

示例 2:可变引用传递

fn append_exclamation(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut greeting = String::from("Hello");
    append_exclamation(&mut greeting);
    println!("{}", greeting);  // "Hello!"
}

引用的内存表示

fn main() {
    let x: i32 = 42;
    let r: &i32 = &x;

    println!("x 的地址: {:p}", &x);
    println!("r 的值(地址): {:p}", r);
    println!("r 解引用的值: {}", *r);
}

内存布局

栈内存:
┌──────────────┐
│  x: 42       │  ← i32,4 字节
├──────────────┤
│  r: 0x...    │  ← 引用(指针),8 字节(64 位系统)
│  │           │
│  └──> 指向 x
└──────────────┘

智能指针 vs 引用

特性引用 &T智能指针 Box<T>
所有权
解引用*r*b
实现简单指针堆分配 + Drop
大小指针大小指针大小(指向堆)
fn main() {
    let x = 5;
    let r = &x;       // 引用
    let b = Box::new(x);  // 智能指针(堆分配)

    println!("r: {}", *r);
    println!("b: {}", *b);
}

实践建议

✅ 推荐做法

fn main() {
    let s = String::from("hello");

    // 1. 优先使用不可变引用
    print_string(&s);

    // 2. 只在需要时使用可变引用
    let mut s = s;
    modify_string(&mut s);

    // 3. 使用切片而不是索引
    let word = first_word(&s);  // 返回 &str
}

fn print_string(s: &String) {
    println!("{}", s);
}

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

fn first_word(s: &String) -> &str {
    // 返回切片而不是索引
    &s[..]
}

❌ 避免做法

fn main() {
    let mut s = String::from("hello");

    // ❌ 创建不必要的可变引用
    let r1 = &mut s;
    let r2 = &mut s;  // 编译错误

    // ❌ 返回局部变量的引用
    let bad_ref = create_dangling();
}

fn create_dangling() -> &String {  // ❌ 悬垂引用
    let s = String::from("hello");
    &s  // s 在函数结束时被释放
}

练习题

练习 1:修复借用错误

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

练习 2:实现字符串反转

使用引用实现字符串反转:

fn reverse_string(s: &mut String) {
    // 实现反转
}

fn main() {
    let mut s = String::from("hello");
    reverse_string(&mut s);
    println!("{}", s);  // "olleh"
}
查看答案
fn reverse_string(s: &mut String) {
    *s = s.chars().rev().collect();
}

fn main() {
    let mut s = String::from("hello");
    reverse_string(&mut s);
    println!("{}", s);  // "olleh"
}

小结

  • 引用允许访问数据而不获取所有权
  • &T 是不可变引用,&mut T 是可变引用
  • ✅ 同一时间只能有一个可变引用多个不可变引用
  • ✅ 引用必须总是有效,不会悬垂
  • 切片是特殊的引用类型
  • ✅ Rust 的借用检查器在编译时保证内存安全

下一步,我们将学习生命周期