讲解

把值传给函数就要交出所有权,用完还得还回来——太啰嗦。引用(reference)解决了这个问题:&s 创建对 s 的引用,函数通过引用「借用」值,所有权纹丝不动。形如 fn len(s: &String) -> usize 的签名读作「借一个 String 用用,看完就还」。默认的引用是只读的,借来不能改。

想改要用可变引用 &mut s,函数签名 fn append(s: &mut String)。但可变引用附带一条铁律:同一作用域、同一时刻,对同一个值要么存在任意多个不可变引用,要么存在且只存在一个可变引用——两者不能共存。这条规则在编译期消灭了数据竞争:没有人会正在读的同时另一个人在改。注意「同一时刻」按引用的最后一次使用计算(非词法生命周期,NLL),先用完不可变引用再创建可变引用是合法的。

借用检查器还堵死了悬垂引用:函数返回指向局部变量的引用必然编译失败,因为局部变量在函数结束时已释放,引用会指向无效内存。编译器要求所有引用始终指向有效数据——其他语言里的 use-after-free 这类线上事故,在 Rust 里连编译都过不去。

示例

不可变引用:借用而不占有:

fn calculate_length(s: &String) -> usize {
    s.len()
} // s 是引用,不拥有值,离开作用域什么也不释放

fn main() {
    let text = String::from("借用一下");
    let len = calculate_length(&text);
    // text 所有权没动,继续正常使用
    println!("「{text}」的长度是 {len} 字节");
    assert_eq!(len, 12);
}

可变引用:借来修改:

fn add_suffix(s: &mut String) {
    s.push_str(",世界");
}

fn main() {
    let mut greeting = String::from("你好");
    add_suffix(&mut greeting);

    // NLL:不可变借用在最后一次使用后结束,之后可以再借可变的
    let r1 = &greeting;
    println!("r1 看到:{r1}");
    let r2 = &mut greeting;
    r2.push_str("!");
    println!("r2 改完:{r2}");

    assert_eq!(greeting, "你好,世界!");
}

同时持有两个可变引用会被编译器拦下(构建脚本验证这段确实编译失败):

// compile-fail: 同时存在两个可变引用
fn main() {
    let mut s = String::from("数据");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{r1} {r2}");
}

报错大意(E0499,第二次可变借用越界):

error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> main.rs:4:14
  |
3 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
4 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
5 |     println!("{r1} {r2}");
  |               ---- first borrow later used here

常见坑

  • & 和 &mut 同时存活:let a = &s; let b = &mut s; println!("{a}") 编译失败——读者和作者不能同时在场。先用完读者,再请作者。
  • 以为规则是「词法作用域」:NLL 按最后一次使用计算借用有效期,所以 { let r = &mut s; use(r); } 之后再借是合法的,不必强行用小括号圈作用域。
  • 函数返回局部变量的引用:fn f() -> &String { let s = ...; &s } 是悬垂引用,编译必挂;返回 owned 值(String)才是出路,生命周期章节再细说。
  • 给 Copy 类型传引用:传 &i32 给函数是画蛇添足——i32 本来就复制,直接按值传更简洁。

小结

& 借用只读、&mut 借用可改;铁律:多读或单写,不可兼得;悬垂引用编译期根除。引用让所有权留在原地,是 Rust 函数的默认沟通方式。下一章看引用的近亲:切片。