Rust 入门:所有权与借用
Rust 最难也最核心的概念是所有权(ownership),它让 Rust 无 GC 也内存安全。本文用大白话讲清所有权、借用、生命周期这三件事,以及新手最常见的"和编译器搏斗"场景,帮你跨过 Rust 的第一道也是最陡的门槛。
本文要点
- 主题聚焦:TypeScript、开源
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
写惯了 TypeScript / Python / Go 的开发者,第一次碰到 Rust 通常会被编译器按在地上摩擦。但摩擦完之后你会发现,这是一种全新的、值得的体验。而那道最陡的门槛,名字叫所有权。这篇文章就把所有权、借用、生命周期这三件事讲清楚,顺便聊几个新手必踩的坑。
一、Rust 为什么值得学
先说为什么值得花时间学一门陡到劝退的语言。三个理由:
第一,无 GC 的内存安全。 JavaScript 有 V8 的 GC 替你回收,Go 有 GC,C/C++ 得手动管理。Rust 走第三条路——所有权系统——在编译期就保证没有空指针、悬垂引用、二次释放,运行时零开销。你可以写底层代码,却不需要担心内存泄漏或 use-after-free。
第二,性能。 Rust 编译成原生机器码,没有运行时,没有 JIT 预热,性能和 C/C++ 一个梯队,但比它们安全得多。Discord、Cloudflare、字节跳动的关键路径都在用 Rust 重写。
第三,系统编程复兴。 Linux 内核已经接受 Rust,Windows 内核组件在用 Rust 重写,区块链、数据库、游戏引擎、嵌入式,Rust 都在快速渗透。学会 Rust,等于拿到一张通往底层的入场券。
门槛也是真的陡。但只要你跨过去,后面就豁然开朗。
二、所有权三规则
所有权是 Rust 独有的概念。别在别的语言里找对应——它就是新的。记住三条规则,你就掌握了 80%:
- 每个值有且只有一个所有者(owner)。
- 把值赋给另一个变量,或作为函数参数传入,默认是"移动"(move),不是拷贝。原变量从此失效。
- 当所有者离开作用域,值被自动释放。
看代码:
fn main() {
let s1 = String::from("hello"); // s1 是所有者
let s2 = s1; // 所有权移动到 s2,s1 失效
// println!("{}", s1); // 编译错误:s1 已经被 move 了
println!("{}", s2); // OK
} // s2 离开作用域,"hello" 的内存被释放
为什么这样设计?因为 Rust 要在编译期就知道谁负责释放这块内存。如果 s1 和 s2 都有效,那块内存就会被释放两次——这就是 C/C++ 里臭名昭著的 double-free。Rust 用 move 语义一刀切干净。
注意:简单标量类型(整数、浮点、布尔、char)实现了 Copy trait,赋值时是拷贝,不会失效:
let x = 5;
let y = x; // x 被拷贝到 y,x 仍然有效
println!("{} {}", x, y); // OK
函数传参同样是 move:
fn take(s: String) {
println!("got {}", s);
} // s 离开作用域,被释放
fn main() {
let s = String::from("hello");
take(s);
// println!("{}", s); // 编译错误:s 已经被 move 进函数了
}
想继续用?要么让函数把所有权还回来,要么用下一节的"借用"。
三、借用与引用
每次调用函数都得把所有权搬来搬去,太累。Rust 的解法是借用(borrow):你拿一个引用去看或者改那个值,但所有权不动。
引用分两种:
- 不可变引用
&T:可以同时存在多个,只读。 - 可变引用
&mut T:同一时刻只能有一个,可写。
fn main() {
let s = String::from("hello");
let r1 = &s; // 不可变借用
let r2 = &s; // 再来一个不可变借用,OK
println!("{} {}", r1, r2); // 多个不可变借用可以共存
let mut s2 = String::from("world");
let r3 = &mut s2; // 可变借用,独占
r3.push_str("!");
println!("{}", r3);
}
铁律来了:同一时刻,不可变借用和可变借用不能共存。要么大家只读,要么一个人独占写。下面这段编译不过:
let mut s = String::from("hello");
let r1 = &s; // 不可变借用
let r2 = &mut s; // 编译错误:已经有不可变借用了
为什么这么严?因为 Rust 要在编译期消灭数据竞争(data race)。如果一个人在写,另一个人在读,读到一半数据变了,这就是并发 bug 的温床。Rust 在编译期就把它掐死,而不是等到线上爆雷。
一个 NLL(Non-Lexical Lifetimes)的小细节:引用的作用域从它最后一次使用开始算结束,不是从变量离开花括号算。所以下面这段是合法的:
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // r1 在这里最后一次使用,之后"死了"
let r2 = &mut s; // r1 已死,所以这里 OK
println!("{}", r2);
这是 Rust 2018 edition 之后编译器变聪明的结果。早些年这种代码会被拒。
四、生命周期
借用不能凭空存在,它得有所指。生命周期(lifetime)就是编译器用来追踪"这个引用到底能活多久"的机制。
核心规则一句话:引用不能比被引用的值活得长。否则就是悬垂引用(dangling reference)。
fn main() {
let r;
{
let x = 5;
r = &x;
} // x 在这里被释放
// println!("{}", r); // 编译错误:r 指向的 x 已经没了
}
大多数时候编译器能自己推断生命周期,你不用管。但函数签名里,如果参数和返回值都是引用,编译器就不知道返回的引用到底跟哪个参数绑定。这时候你得显式标注:
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
'a 是个生命周期参数,告诉编译器:返回值的生命周期,至少和 s1、s2 中较短的那个一样长。调用方拿到返回的引用后,它的可用范围就被这个约束框死了。
新手第一次看到 <'a> 容易发怵。其实就当类型参数看——泛型是"什么类型都行",生命周期是"多长都行,但得满足约束"。一旦接受了这个设定,大部分生命周期错误就能读懂了。
五、新手最常和编译器搏斗的三个场景
写 Rust 的头几百小时,你会反复在三个坑里摔倒。提前知道它们能省你大量时间。
坑一:String vs &str
String 是堆上的、可变、拥有所有权的字符串;&str 是字符串切片,通常是借用。新手不知道该用哪个,经常把代码写成编译器红一片。
经验法则:函数参数优先用 &str,需要拥有所有权时才用 String。
fn first_word(s: &str) -> &str { // 用 &str 接收,函数更通用
// ...
s
}
fn main() {
let s = String::from("hello world");
let w = first_word(&s); // String 强转成 &str,OK
let literal = "hi";
let w2 = first_word(literal); // 字面量本身就是 &str,OK
}
结构体字段需要持有数据用 String,只是引用时用 &'a str(但需要标注生命周期)。
坑二:在循环或集合里被 move
let words = vec![String::from("a"), String::from("b")];
for w in words { // 这里 words 被 move 进迭代器
println!("{}", w);
}
// println!("{:?}", words); // 编译错误:words 已经被消费
你"只是想看看",结果集合被搬空了。想保留?用引用迭代:for w in &words,拿到的是 &String;要改用 &mut。这是新手最容易踩的坑,没有之一。
坑三:返回引用
fn dangle() -> &String { // 编译错误
let s = String::from("hi");
&s
} // s 在这里被释放,返回的引用悬空
编译器直接拒绝。两种解法:要么返回 String(把所有权交出去),要么让引用的生命周期绑定到外部传入的参数上(像前面的 longest)。
这三个坑看起来啰嗦,但其实都在教你同一件事:数据该归谁拥有,引用该活多久。想清楚这两点,代码自然就过了编译。
六、心态转变:编译器是队友,不是敌人
很多新手和 Rust 编译器的关系是"敌对"——编译器报了一屏红,你就开始猜:加个 clone()?加个 &?换个顺序试试?
这是错的姿势。
Rust 编译器是市面上最耐心的老师。它报的每一个错误,都指向一个真实的、可能让你的程序在生产环境崩溃的内存问题。随手 .clone() 是止痛药,不是治本——它掩盖了你对所有权设计没想清楚。
正确的心态:
- 读懂错误信息。 Rust 的错误提示是所有语言里最好的,会告诉你哪里、为什么、怎么改。
- 别急着
.clone()。 先问自己:这里到底应该谁拥有这块数据? - 接受"被拒绝是常态"。 前两周你会觉得自己不会编程,这是正常的,所有人都是这么过来的。
一旦和编译器达成默契,你会发现一个奇妙的现象:Rust 程序一旦编译通过,基本就跑得起来。那些"在我机器上能跑"的玄学 bug,在 Rust 里编译期就消掉了大半。这种"编译器替你兜底"的安全感,是任何动态语言都换不来的。
七、学习路径建议
最后给点具体的路径建议,免得你在错误的层次上浪费力气:
第一步: 把 The Rust Programming Language(社区称 The Book)的前四章(含所有权)读三遍,不要跳。每读完一段,自己写代码验证。光看是学不会所有权的,手必须动。
第二步: 不要急着写 Web 框架、写数据库。先在 Rustlings(官方练习集)上把几十道小题刷完,建立肌肉记忆。
第三步: 写一个稍大的 CLI 小工具——比如一个 todo list,或者一个简易 grep。在这个过程中你会被迫处理所有权、借用、生命周期、错误处理(Result)、trait,把它们串起来用。
第四步: 这时再去选你想做的领域。Web 就学 axum,异步就学 tokio,嵌入式去看 no_std。有了所有权打底,后面学什么都快。
最容易踩的反面教材是:一上来就想用 Rust 复刻你熟悉的 TypeScript 项目。你会在所有权上撞得头破血流,然后怪语言"难用"。Rust 不是更好的 TypeScript,它是另一种思维方式。先理解所有权,再谈别的。
小结
- 所有权三规则:每个值有唯一所有者、赋值默认是 move、作用域结束自动释放。这是 Rust 内存安全的根基。
- 借用是"借来用,所有权不动":多个
&T可以共存,&mut T独占,二者不可同时存在——编译期消灭数据竞争。 - 生命周期是编译器对"引用能活多久"的静态检查,引用不能比被引用者活得长;函数签名里要标注,是为了让调用方知道返回引用的有效范围。
- 新手三大坑:
Stringvs&str、循环里被 move、返回引用。背后都是同一个问题——数据归谁所有。 - 编译器是队友不是敌人,它报错是在帮你消灭潜在的内存 bug。读懂它,别和它对着干。
学 Rust 不是学一门语法,是学一种"谁拥有什么"的工程纪律。编译器骂你越狠,你将来的程序越稳。
Related