Skip to content

一、什么是引用?怎么借? ​

看代码:

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

    let len = calculate_length(&s1); // &s1 创建了一个指向 s1 的引用,但所有权没变

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

fn calculate_length(s: &String) -> usize { // s 是一个对 String 的引用
    s.len()
} // s 离开作用域,但它只是引用,不会释放所指向的数据

我们把 &s1 这个动作叫做借用,s 就是一个引用。你可以把引用想象成一本书的目录条目——它能让你看到书的内容,但条目本身并不是书。把书借给别人看,书还是你的。

引用默认是不可变的,你不能通过它去修改原值:

rust
fn change(s: &String) {
    s.push_str(", world"); // 编译错误!不能通过不可变引用修改
}

如果确实需要修改,就得用可变引用。

二、可变引用:借走还能改 ​

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

    change(&mut s); // 创建可变引用

    println!("{}", s); // 输出 "hello, world"
}

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

可变引用的规则非常严格,这正是 Rust 把数据竞争扼杀在编译期的关键。

规则一:同一作用域,对同一数据,只能有一个可变引用 ​

rust
let mut s = String::from("hello");

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

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

如果允许两个可变引用同时存在,就可能出现“你改我也改”的数据竞争。Rust 直接让这种代码编译不过。

注意:这个限制是“同一作用域”内,且引用必须没有失效。 如果第一个可变引用在第二个被创建前已经用完了,编译器是允许的:

rust
let mut s = String::from("hello");

let r1 = &mut s;
println!("{}", r1); // r1 最后一次使用在此,之后 r1 不再被使用

let r2 = &mut s;    // 编译器允许,因为 r1 已经“失效”
println!("{}", r2);

编译器会进行一种非词法作用域生命周期分析,通俗说就是:只要你后面不再用 r1,它就算提前“死”了,就可以再借给别人。

规则二:可变引用和不可变引用不能同时存在 ​

rust
let mut s = String::from("hello");

let r1 = &s;      // 不可变引用
let r2 = &s;      // 没问题,多个不可变引用可以共存
let r3 = &mut s;  // 编译错误!已经存在不可变引用,不能再有可变引用

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

反过来也一样,如果已经有了一个可变引用,就不能再有任何不可变引用。

为什么? 假设你有了一个不可变引用,你预期它指向的值不会变。如果这时再冒出一个可变引用并修改了数据,你的不可变引用就读到被污染的数据了。Rust 通过这个规则在编译期杜绝了“读写冲突”。

同样,如果不可变引用在可变引用创建前就已经不再使用,编译器也会放行:

rust
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // r1 和 r2 此后不再使用

let r3 = &mut s; // 允许
println!("{}", r3);

三、数据竞争:Rust 帮你关在门外的恶魔 ​

数据竞争是一种非常难复现和调试的 bug,它发生在三个条件同时满足时:

  1. 两个或以上指针同时访问同一数据。
  2. 至少有一个指针用于写入。
  3. 没有同步机制。

Rust 通过所有权和借用规则,让编译器在编译期就能证明你的代码没有数据竞争:

  • 同一时间只能有一个可变引用(解决了“同时有两个写入指针”的问题)。
  • 可变引用与不可变引用不能共存(解决了“一个写一个读”的问题)。

因此,在安全的 Rust 代码中,你永远不需要担心数据竞争。

四、悬垂引用:指向已释放内存的“鬼魂” ​

在 C/C++ 里,你可能写出这样的代码:函数返回一个局部变量的指针,等函数结束后指针就成了野指针。Rust 编译器保证你永远写不出悬垂引用。

rust
fn dangle() -> &String {       // 期望返回一个 String 引用
    let s = String::from("hello"); // s 在堆上分配

    &s // 返回 s 的引用
} // s 离开作用域,内存被释放!返回的引用指向已释放的内存

编译器会果断报错:

error[E0106]: missing lifetime specifier
...
this function's return type contains a borrowed value, but there is no value for it to be borrowed from

它直接指出:这个借用的值不存在于函数之外,你不能返回一个对局部变量的引用。

解决办法很简单:直接把所有权交出去,返回 String 而不是 &String。

rust
fn no_dangle() -> String {
    let s = String::from("hello");
    s // 所有权移出,调用者获得所有权
}

五、引用的本质与生命周期初探 ​

每一个引用背后都有一个生命周期,它是编译器用来跟踪引用作用域的工具。上面我们讲的“最后一次使用”其实就是编译器自动推断引用生命周期的结果。

简单的例子:

rust
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这段代码会编译失败,因为编译器不知道返回的引用是来自 x 还是 y,也就无法确定返回的引用能活多久。你需要显式标注生命周期参数,告诉编译器输入引用和输出引用之间的生存时间关系。

总结(帮你串联) ​

  • 引用让你在不获取所有权的情况下使用数据,分不可变引用 (&T) 和可变引用 (&mut T)。
  • 借用规则(编译期强制检查):
    • 同一时刻,要么任意多个不可变引用,要么恰好一个可变引用,两者不能共存。
    • 引用必须永远有效(不存在悬垂引用)。
  • 这些规则消灭了数据竞争和悬垂指针。
  • 编译器会进行非词法作用域分析,只要引用不再使用,就会提前结束借用,允许新的借用。

理解了这些,你就已经握住了 Rust 内存安全的核心钥匙。所有权决定了谁来释放内存,借用决定了谁能访问内存以及怎么访问。两者结合,就是一套零成本、完全编译期检查的内存管理方案。

Java · Golang · Rust