#TypeScript#开源

Rust 入门:所有权与借用

Rust 最难也最核心的概念是所有权(ownership),它让 Rust 无 GC 也内存安全。本文用大白话讲清所有权、借用、生命周期这三件事,以及新手最常见的"和编译器搏斗"场景,帮你跨过 Rust 的第一道也是最陡的门槛。

本文要点

  • 主题聚焦:TypeScript、开源
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年6月16日·更新于 2026年7月23日

写惯了 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%:

  1. 每个值有且只有一个所有者(owner)。
  2. 把值赋给另一个变量,或作为函数参数传入,默认是"移动"(move),不是拷贝。原变量从此失效。
  3. 当所有者离开作用域,值被自动释放。

看代码:

rust
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,赋值时是拷贝,不会失效:

rust
let x = 5;
let y = x;            // x 被拷贝到 y,x 仍然有效
println!("{} {}", x, y); // OK

函数传参同样是 move:

rust
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:同一时刻只能有一个,可写。
rust
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);
}

铁律来了:同一时刻,不可变借用和可变借用不能共存。要么大家只读,要么一个人独占写。下面这段编译不过:

rust
let mut s = String::from("hello");
let r1 = &s;           // 不可变借用
let r2 = &mut s;       // 编译错误:已经有不可变借用了

为什么这么严?因为 Rust 要在编译期消灭数据竞争(data race)。如果一个人在写,另一个人在读,读到一半数据变了,这就是并发 bug 的温床。Rust 在编译期就把它掐死,而不是等到线上爆雷。

一个 NLL(Non-Lexical Lifetimes)的小细节:引用的作用域从它最后一次使用开始算结束,不是从变量离开花括号算。所以下面这段是合法的:

rust
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)。

rust
fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    } // x 在这里被释放
    // println!("{}", r); // 编译错误:r 指向的 x 已经没了
}

大多数时候编译器能自己推断生命周期,你不用管。但函数签名里,如果参数和返回值都是引用,编译器就不知道返回的引用到底跟哪个参数绑定。这时候你得显式标注:

rust
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。

rust
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

rust
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。这是新手最容易踩的坑,没有之一。

坑三:返回引用

rust
fn dangle() -> &String { // 编译错误
    let s = String::from("hi");
    &s
} // s 在这里被释放,返回的引用悬空

编译器直接拒绝。两种解法:要么返回 String(把所有权交出去),要么让引用的生命周期绑定到外部传入的参数上(像前面的 longest)。

这三个坑看起来啰嗦,但其实都在教你同一件事:数据该归谁拥有,引用该活多久。想清楚这两点,代码自然就过了编译。

六、心态转变:编译器是队友,不是敌人

很多新手和 Rust 编译器的关系是"敌对"——编译器报了一屏红,你就开始猜:加个 clone()?加个 &?换个顺序试试?

这是错的姿势。

Rust 编译器是市面上最耐心的老师。它报的每一个错误,都指向一个真实的、可能让你的程序在生产环境崩溃的内存问题。随手 .clone() 是止痛药,不是治本——它掩盖了你对所有权设计没想清楚。

正确的心态:

  1. 读懂错误信息。 Rust 的错误提示是所有语言里最好的,会告诉你哪里、为什么、怎么改。
  2. 别急着 .clone()。 先问自己:这里到底应该谁拥有这块数据?
  3. 接受"被拒绝是常态"。 前两周你会觉得自己不会编程,这是正常的,所有人都是这么过来的。

一旦和编译器达成默契,你会发现一个奇妙的现象: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 独占,二者不可同时存在——编译期消灭数据竞争。
  • 生命周期是编译器对"引用能活多久"的静态检查,引用不能比被引用者活得长;函数签名里要标注,是为了让调用方知道返回引用的有效范围。
  • 新手三大坑:String vs &str、循环里被 move、返回引用。背后都是同一个问题——数据归谁所有。
  • 编译器是队友不是敌人,它报错是在帮你消灭潜在的内存 bug。读懂它,别和它对着干。

学 Rust 不是学一门语法,是学一种"谁拥有什么"的工程纪律。编译器骂你越狠,你将来的程序越稳。

Related

相关推荐