只借不占:Rust 的引用与借用

只借不占:Rust 的引用与借用

用 & 只借数据而不转移所有权,讲清可变与不可变引用的互斥规则

如果你已经动手试过上一篇的例子,很快就会撞上第一堵墙:把 String 传给一个函数,函数用完,你的变量就没了------所有权跟着参数一起移走了。

rust 复制代码
fn main() {
    let s = String::from("hello");
    let len = calculate_length(s);   // s 的所有权被移走
    println!("{} {}", s, len);       // 编译错误:s 已被移动
}

fn calculate_length(s: String) -> usize {
    s.len()
}

长度倒是算出来了,可 s 也没了。总不能每次传参都"送出去再要回来"吧。Rust 的答案是:不把值送进去,只把值的地址借给对方。这就是引用(reference)与借用(borrowing)。

借,而不是拿走

创建引用用 &

rust 复制代码
fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s);  // 借出 &s,s 的所有权不动
    println!("{} {}", s, len);       // s 还能正常用
}

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s 是借用,离开作用域时不释放任何东西

&s 读作"s 的引用",它本质上就是一个地址------像指针一样,你可以顺着它找到数据,但数据本身仍然归 s 所有。Book 把创建引用的动作叫借用 ,类比很贴切:东西是别人的,你借来用,用完要还,你始终不拥有它。所以 calculate_length 结束时,参数 s 只是个"借来的地址",不会触发 drop,堆上的内存依然完好地属于 main 里的 s

这里就出现了和 Java/Python 的第一个分岔。Java 里 List<String> b = a; 之后,ba 是同一个对象的两个引用,谁都能改;Python 里 b = a 之后,b.append(x) 会直接改变 a 看到的内容。它们都是自由共享 的引用------语言不限制你有多少个、谁能写。Rust 的 & 也允许共享,但它带一条默认限制:通过 & 只能读,不能改

rust 复制代码
let s = String::from("hello");
let r = &s;
r.push_str(", world");  // 编译错误:r 是 &String,不可变借用

变量默认不可变,引用也默认不可变------这跟 Python 里 b = a 后随手 b.append 的习惯正好相反。想改借来的数据?要显式升级成可变引用 &mut,而且代价是独占:

rust 复制代码
fn change(s: &mut String) {
    s.push_str(", world");
}

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

要么一个可变,要么多个不可变

可变引用只有一条限制,但它是 Rust 最著名的规则:同一时刻,对同一份数据,要么有一个可变引用,要么有任意多个不可变引用,不能同时存在

rust 复制代码
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;  // 编译错误 E0499:不能同时把 s 借用两次可变

不可变引用和可变引用混用也一样:

rust 复制代码
let r1 = &s;
let r2 = &s;      // 多个只读,没问题
let r3 = &mut s;  // 编译错误 E0502:s 正被不可变借用中

如果你习惯了 Java 或 Python,这条规则第一眼像无理取闹------为什么不能开两个句柄?它背后的理由值得认真听:这是把多线程里最危险的一类 bug 在编译期就判了死刑。

为什么能防数据竞争

数据竞争(data race)在 C/C++/Java 里是运行时才炸的雷。Book 给的定义是三个条件同时满足:两个或更多指针同时访问同一份数据;其中至少一个在写;没有任何同步机制。三个条件凑齐,结果就是未定义行为------读到的可能是旧值、新值或两者拼凑出来的垃圾。

Rust 的互斥规则恰好让这三个条件永远凑不齐。两个指针同时访问?可以有------多个 & 引用能同时存在,但它们全是只读的,"至少一个在写"就不成立。想写?可以------&mut 引用独占,其他同时刻的访问全部被编译器拒绝。至于同步机制,根本不需要了,因为"并发读写同一块内存"这个场景在编译期就不存在。Book 的结论很硬:数据竞争导致未定义行为,运行时极难排查;Rust 用拒绝编译来预防它。

这就是为什么 Rust 社区说"编译通过 = 没有数据竞争"。Java 里你得记得给共享变量加 synchronized,忘了就是一场事故;Rust 是编译器替你把门------忘了?代码根本编译不过。

有个细节值得现在就知道:引用的"存活期"到它最后一次被使用 为止,而不是到作用域结束。所以下面这段是合法的------两个 &s 用完(println! 之后)就不再算数,此时再创建 &mut s 没有重叠:

rust 复制代码
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}");
// r1、r2 到这里就"死"了
let r3 = &mut s;  // 没问题,借用期没有重叠

引用必须始终有效

第二条规则更简单:引用必须始终有效------一个引用指向的数据,不能比引用先死。

C 里这叫悬垂指针(dangling pointer):内存释放了,指针还攥在手里,下一次解引用就是访问已归还的内存。Rust 在编译期就掐死这个场景:

rust 复制代码
fn dangle() -> &String {   // 想返回一个引用
    let s = String::from("hello");
    &s                       // 但 s 是函数内部创建的
}                            // s 在这里被 drop,引用指向已释放的内存
// 编译错误:返回的引用指向已被释放的值

dangle 无法编译。原因很直白:s 是函数内部创建的,函数一结束 s 就被释放,返回的 &s 就成了悬垂引用------Book 的原话是"这不行!Rust 不会允许"。正确的修法是让所有权跟着走,把 s 本身返回回去,谁调用谁拥有:

rust 复制代码
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // 所有权移出函数,内存跟着走,不会悬垂
}

两条规则,一个目的

这一篇其实只讲了两句话:引用不拥有值,所以借了要还,还得守规矩。规矩是------同一时刻要么一个可变引用、要么多个不可变引用;引用指向的数据必须活得比引用久。前者在编译期消灭数据竞争,后者在编译期消灭悬垂引用。你在 Java/Python 里靠锁和小心谨慎维护的内存安全,Rust 用类型系统直接写死成了规则。

不过你可能已经好奇:编译器凭什么知道"数据活得比引用久"?它靠的是一套叫**生命周期(lifetime)**的分析------下一篇就讲它。好消息是,日常代码里绝大多数时候你不需要写任何生命周期标注,编译器自己就能推断。


参考来源

相关推荐
FreeTinker1 小时前
Java文件服务器的技术选型与实现路径:从嵌入式工具到企业级系统
java·服务器·开发语言
2501_906565121 小时前
小创 · 智能助手
开发语言·c#
benchmark_cc1 小时前
1000只ETF的5分钟K线如何批量获取?QuantDash分页策略与高性能Python实践
开发语言·人工智能·爬虫·python·算法·quantdash·量化数据源
Coodor1 小时前
如何在web浏览器使用js操作CPU卡
开发语言·前端·javascript·cpu卡
我星期八休息1 小时前
Linux I/O多路转接—epoll
java·linux·运维·服务器·开发语言·jvm·算法
大模型码小白2 小时前
AI 提示词专栏:使用系统指令(System Prompt)实现全局约束
java·大数据·运维·开发语言·人工智能·python·prompt
HugoStudio_SWAN2 小时前
C++ CMD 互动动画:按键触发爆炸效果
开发语言·c++·学习·程序人生
互联网中的一颗神经元2 小时前
10. Large 对象分配:直接路径
开发语言·golang