Rust:把内存安全写进类型系统

它的核心承诺,以及代价

Rust 只承诺三件事,但这三件事改变了整个系统编程领域:

  1. 内存安全——没有悬垂指针、没有双重释放、没有缓冲区溢出,而且不靠 GC。
  2. 线程安全——"无数据竞争"在编译期被证明(不是靠运行时检测)。
  3. 零开销抽象——高层写法编译后和手写 C 一样快。

这三件事以前被认为是不可兼得的:安全要么靠 GC(性能代价),要么靠人(错误率高),要么靠运行时检查(开销)。Rust 的答案是让编译器在编译期做所有权分析。这就是所有人第一次写 Rust 都被借用检查器(borrow checker)反复教训的原因——它不是苛刻,它在替你算清"这块内存归谁"。

出身:一个私人项目如何变成国家级工具

2006 年,Mozilla 的工程师 Graydon Hoare 开始写一个私人项目,起因很朴素——他住的那栋楼电梯坏了,因为电梯软件崩溃(C++ 写的、内存不安全)。他觉得"系统软件不该这么写"。

Mozilla 2010 年正式接手,2015 年 5 月发布 1.0。之后发生的事很有意思:Mozilla 自己的 Servo 引擎被搁置,但 Rust 反而被别的地方大面积采纳——AWS(Firecracker)、Microsoft(Windows 内核的部分组件、Azure)、Google(Android 的 Rust 支持)、Linux 内核(2022 年 6.1 开始接受 Rust 代码)、Cloudflare(Pingora 替换 Nginx 的 C 模块)。

2021 年 Rust 基金会成立,成员包括 AWS、Google、Microsoft、Huawei、Mozilla,2024 年又加入了一批。这已经不是一个社区项目了,是基础设施级别的工具。

三块基石,逐个看完

1. 所有权(Ownership)

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;                 // 移动(move),不是拷贝
    // println!("{}", s1);       // ❌ 编译错误:s1 已被移走
    println!("{}", s2);          // ✅
}

规则只有三条:

  • 每个值有且只有一个所有者。
  • 所有者离开作用域时,值被释放(drop)。
  • 值可以移动或借用,但不能凭空多出一个所有者。

这解决了 C++ 里最经典的悬垂指针和 double free:如果没有第二个所有者,就不可能"两个人同时以为该由自己释放"。

2. 借用(Borrowing)

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

借用检查的核心规则:同一时刻,要么有任意多个不可变借用(&T),要么有恰好一个可变借用(&mut T),不能兼得。

这条规则同时挡住了两类 bug:读的时候有人改(数据竞争)、改的时候有人读(迭代器失效)。写 C++ 的人应该都遇到过 for (auto& x : vec) vec.push_back(...) 这种崩溃——Rust 编译器会直接拒绝。

'a 是生命周期标注,意思是"返回的引用活得和输入一样久"。它不改变任何运行时行为,纯粹是给编译器的说明。初学阶段最容易误解这一点。

3. 类型系统承载安全:Option、Result、?

fn read_port(s: &str) -> Result<u16, String> {
    let n: u16 = s.parse().map_err(|e| format!("parse failed: {e}"))?;
    if n == 0 { return Err("port must be > 0".into()); }
    Ok(n)
}

Rust 没有 null,用 Option<T> 表示可空;没有异常,用 Result<T, E> 表示可失败。? 是语法糖:匹配到 Err 就提前返回,匹配到 Ok 就取出值。

好处是你不可能忘记处理错误——Result 是 #[must_use] 的,忽略会有编译警告;match 必须穷尽,Option 不处理 None 分支编译器不放行。这和 Go 的 if err != nil 目标相同,但形式更强。

4. Trait:比接口更强一层的抽象

trait Summarize {
    fn summarize(&self) -> String;
    fn preview(&self) -> String {              // 默认实现
        format!("{}...", &self.summarize()[..20.min(self.summarize().len())])
    }
}

impl Summarize for String {
    fn summarize(&self) -> String { self.clone() }
}

Trait 同时承担了三个角色:接口(动态分发 dyn Trait)、泛型约束(静态分发,单态化)、运算符重载(Add、Index 都是 trait)。这是 Rust 抽象能力的核心,也是学习曲线最陡的地方——一个 impl Trait for Trait 加上泛型嵌套,读起来像解方程。

真实性能:为什么它快

  • 无 GC:没有停顿,延迟可预测。这对金融、实时音视频、嵌入式是刚需。
  • 单态化:泛型在编译期展开成具体类型,和手写代码一样快(代价是编译慢、二进制大)。
  • 零成本抽象:Iterator 链式调用 map/filter/collect 编译后通常和手写循环等价,甚至更快(因为没了边界检查)。
  • 内存布局可控:#[repr(C)]、Vec 的容量规划、Box/Rc/Arc 的选择都很明确。

一组常见量级(不同硬件差异大,仅示意):

场景 Rust 相对 C/C++
JSON 解析(serde_json) 大致相当或略慢
正则(regex crate) 相当,部分场景更快(线性时间保证)
HTTP 服务(axum + tokio) 与 Go 相当或更快,内存低得多
启动时间 极小(无运行时初始化)

生态:已经足够用了

  • Web / 服务端:axum、actix-web、poem,异步运行时 tokio 是事实标准。
  • 序列化:serde 是行业标杆,性能极好且 API 设计优雅。
  • 数据库:sqlx(编译期检查 SQL)、diesel、SeaORM。
  • CLI:clap 写参数解析体验极好。
  • 数据处理:Polars(Rust 内核,Python 版的引擎就是它)、DataFusion。
  • 前端工具链:SWC、Rolldown、rustc 自己——JS 世界的构建工具正在被 Rust 重写一遍。
  • Python 扩展:PyO3 让写 C 扩展变得像写普通 Rust。

一段生产级异步服务骨架:

use axum::{routing::get, Router, Json};
use serde::Serialize;

#[derive(Serialize)]
struct Health { ok: bool, version: &'static str }

async fn health() -> Json<Health> {
    Json(Health { ok: true, version: env!("CARGO_PKG_VERSION") })
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/health", get(health));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

学习曲线的真相

社区常说"Rust 难",准确的说法是:语法不难,思维模型要重建。

真正的坎在三个地方:

  1. 借用检查器的错误信息虽然友好,但会连着报一串。新手常常一个错误修好引出五个。经验是:先想清楚"谁拥有这块数据、活多久",再写代码,而不是写完再修。
  2. 异步生态的双色函数(async 和同步不能自由混用),Send/Sync 约束在跨 .await 时会突然报错,Pin 的概念很抽象。
  3. 生命周期标注在结构体里会传染:一个带 &'a str 字段的结构体,所有用到它的地方都要带生命周期参数。实战中通常直接上 String 或 Arc<str> 绕开。

一个现实建议:先用 Rust 写 CLI 工具或数据处理脚本,别一上手就写大型异步服务。前者没有生命周期地狱,能专心熟悉所有权;熟悉之后再进 tokio。

不该用它的地方

  • 原型开发和业务快速迭代。编译时间 + 借用检查的心智负担,会让"先跑起来再改"的节奏失效。这类活儿 Python/Go/TS 更合适。
  • GUI 桌面应用。egui、iced 能用,但生态和成熟度远不如 Qt/Electron。
  • 机器学习训练。生态几乎空白,训练用 PyTorch,推理侧可以用 candle/burn,但那不是主战场。
  • 团队里没人写过系统语言。Rust 的收益建立在"团队真能驾驭"之上,否则会退化成跟编译器搏斗。

结论:它改变的不是一门语言的排名,而是行业的默认

Rust 最深远的影响可能不是"多了一门能写服务的语言",而是它证明了"内存安全可以零成本"这件事是可行的。此后 C++ 社区推进了 Core Guidelines 和安全子集,美国政府机构开始建议新项目避免使用内存不安全的语言,Linux 内核接受了 Rust。整个系统编程领域的默认假设被推动了。

对个人来说,学 Rust 的价值往往不在"用它写产品",而在"它逼你想清楚内存和生命周期的每一笔账"。这笔账想清楚了,写 C++、写 Go、甚至写 JS 都会更清楚自己在做什么。