Rust 的承诺:不是没有复杂性,而是把复杂性放到你能看见的地方

内容概览

  1. 开场:Rust 的承诺到底是什么

    Rust 不只是"更安全的 C++",它真正承诺的是:让程序员把意图写进类型和函数签名里。

  2. 从一个简单例子开始:为什么 String 不能随便传两次

    通过 i64String 的区别解释 Copy、移动语义和显式 clone

  3. JavaScript 和 Go 的困境:函数会不会改我的数据,很难从签名看出来

    对比对象、指针、浅拷贝、深拷贝,以及运行时检查的局限。

  4. C/C++ 的 const 为什么不够可靠
    const 可以表达约束,但语言本身允许绕开它,安全边界不够清晰。

  5. Rust 的核心设计:所有权、共享引用、独占引用

    Rust 用 T&T&mut T 把"拿走、只读、可写"三种关系写进代码。

  6. Unsafe Rust:不是随便危险,而是明确标记"编译器不再替你检查"
    unsafe 是边界,不是逃避责任的通行证。

  7. Aliasing XOR Mutability:要么共享,要么修改,不能同时发生

    解释 Rust 借用检查器最核心的约束。

  8. 结尾:Rust 的承诺不是让写代码更轻松,而是让错误更早暴露

本内容是对The promise of Rust的翻译与整理,有一定删减。


Rust 的承诺:不是没有复杂性,而是把复杂性放到你能看见的地方

很多人第一次接触 Rust,会被它的所有权、借用、生命周期劝退。

为什么一个字符串传给函数之后就不能再用了?

为什么读数据要写 &T,改数据要写 &mut T

为什么明明我知道这段代码没问题,编译器却说不行?

fasterthanlime 的文章《The promise of Rust》讲的正是这个问题:Rust 最吓人的地方,恰恰也是它最有价值的地方。

Rust 的承诺不是"写起来永远简单",也不是"编译器永远懂你"。它真正承诺的是:

尽可能让程序的内存安全、可变性和资源所有权,在编译期被检查出来。

这篇文章我们就顺着原文的思路,聊聊 Rust 到底承诺了什么。


一、从一个最普通的函数开始

先看一个简单函数:

rust 复制代码
fn show(n: i64) {
    println!("n = {n}");
}

fn main() {
    let n = 42;
    show(n);
    show(n);
}

这段代码完全没问题。i64 是一个整数,复制它的成本很低,Rust 允许它被直接复制。也就是说,第一次 show(n) 并不会让 n 消失。

但如果把 i64 换成 String

rust 复制代码
fn show(s: String) {
    println!("s = {s}");
}

fn main() {
    let s = String::from("hello");
    show(s);
    show(s);
}

第二次调用就会报错。

原因是:String 拥有堆上的内存。把它传给 show 时,所有权被移动进函数里。第一次调用后,main 里的 s 已经不再拥有那块字符串数据,所以不能再用一次。

这就是 Rust 的移动语义。

对于整数这种类型,复制只是寄存器之间搬一下值。

对于 String,复制可能意味着堆分配、内存拷贝、allocator 调度,成本不再微不足道。

所以 Rust 不会偷偷帮你复制。

如果你确实想复制,需要明确写出来:

rust 复制代码
show(s.clone());
show(s);

这就是 Rust 的一个基本态度:昂贵的事情不要隐式发生。


二、如果函数只是看一眼,就不要拿走所有权

如果 show 只是打印字符串,并不需要拥有它,那更合理的写法是借用:

rust 复制代码
fn show(s: &String) {
    println!("s = {s}");
}

fn main() {
    let s = String::from("hello");
    show(&s);
    show(&s);
}

这里的 &String 表示共享引用,也就是"我只是借来看一下"。

这个区别非常关键:

rust 复制代码
fn consume(s: String)   // 拿走所有权
fn read(s: &String)     // 只读借用
fn update(s: &mut String) // 可变借用

在 Rust 里,函数签名不是装饰品。

它直接告诉你:这个函数会不会拿走你的数据,会不会修改你的数据。

这就是 Rust 相比很多语言更"诚实"的地方。


三、JavaScript 的问题:对象到底会不会被改,很难从函数签名看出来

在 JavaScript 里,原始类型通常按值传递:

js 复制代码
function inc(x) {
  x += 1;
}

let x = 0;
inc(x);
console.log(x); // 还是 0

但对象不是这样:

js 复制代码
function inc(o) {
  o.value += 1;
}

let o = { value: 0 };
inc(o);
console.log(o.value); // 变成 1

问题在于:从函数签名 inc(o) 本身,你看不出它会不会修改对象。

如果你不希望原对象被改,可能会选择 clone:

js 复制代码
inc({ ...o });

但这只是浅拷贝。如果对象里面还有对象,内部那层仍然可能被改。

于是你可能会做深拷贝,比如很多人用过的:

js 复制代码
JSON.parse(JSON.stringify(o))

这又会带来新问题:循环引用怎么办?日期、Map、自定义类型怎么办?性能怎么办?

原文的重点不是说 JavaScript 不好,而是指出一个事实:

在 JavaScript 里,很多"会不会修改数据"的约束无法可靠地表达在函数签名里,只能靠约定、文档、测试和运行时行为。


四、Go 的问题:指针能表达可变性,但不能表达"只读指针"

Go 里,结构体默认按值传递:

go 复制代码
type Obj struct {
    value int
}

func inc(o Obj) {
    o.value += 1
}

这里修改的是副本,不会影响原值。

如果要修改原对象,就传指针:

go 复制代码
func inc(o *Obj) {
    o.value += 1
}

这比 JavaScript 明确一些。看到 *Obj,你知道函数拿到了地址。

但问题也在这里:

如果一个函数接收 *Obj,它可能只是读取,也可能修改。调用方无法从类型上要求"你只能读,不能改"。

如果你想防止它修改,只能拷贝一份再传进去。

但和 JavaScript 一样,拷贝也有浅拷贝、深拷贝、循环引用、特殊类型这些问题。

原文用这个对比引出 Rust 的核心优势:

Rust 不只是区分"值"和"指针",它还能区分"共享引用"和"独占引用"。


五、C/C++ 的 const:看起来有约束,但边界不够硬

C 和 C++ 有 const。这似乎可以表达"我不会修改它"。

比如 C++ 里:

cpp 复制代码
void read(const S& s) {
    // 看起来不能修改 s
}

但 C/C++ 的问题是,语言允许你通过类型转换绕过 const

这意味着 const 更像是一种约定,而不是绝对可靠的安全边界。

在大型代码库里,你不能指望每个调用链、每个第三方库、每个宏、每个 unsafe-ish 技巧都遵守这层约定。

原文里有一句很强烈的判断:C 和 C++ 本质上过于宽松。你可以继续用工具、规范、审查、静态分析去补洞,但语言本身没有把安全边界封死。

这也是 Rust 想改变的地方。


六、Rust 的表达方式:拿走、只读、可写

Rust 用三种常见参数形式表达三种不同意图。

1. T:拿走所有权

rust 复制代码
struct Conn;

fn close(conn: Conn) {
    // 关闭连接,释放资源
}

fn main() {
    let conn = Conn;
    close(conn);
    close(conn); // 编译错误
}

如果 close 接收的是 Conn,就表示调用者把连接交出去了。

交出去之后,不能再用。

这可以防止"双重关闭"这类资源管理错误。

2. &T:共享引用,只读

rust 复制代码
struct Conn {
    name: String,
}

fn name(conn: &Conn) -> &str {
    &conn.name
}

这里函数只是读取连接名。

它不能修改 conn.name,编译器会阻止。

更重要的是,这种不可变性是传递的。

就算字段里还有字段,也不能通过共享引用一路钻进去修改内部数据。

3. &mut T:独占引用,可以修改

rust 复制代码
fn rename(conn: &mut Conn) {
    conn.name = String::from("new-name");
}

如果函数要修改数据,签名必须写 &mut Conn

调用方也必须显式传入:

rust 复制代码
let mut conn = Conn {
    name: String::from("old-name"),
};

rename(&mut conn);

这里有两个显式动作:

  • let mut conn:这个绑定允许被修改
  • &mut conn:把它作为可变引用借出去

Rust 不会让"可变性"悄悄发生。


七、Unsafe Rust:不是没有危险,而是危险被圈起来了

Rust 并不是完全禁止底层操作。

你仍然可以使用裸指针,也可以写 unsafe

但 Rust 的关键区别是:危险区域必须被显式标记。

比如你可以把共享引用转成裸指针,再转成可变裸指针。

但当你真的要解引用裸指针并写入时,编译器会要求你进入 unsafe 块。

rust 复制代码
unsafe {
    // 编译器不再替你证明这里安全
}

这并不表示 unsafe 里面的代码一定错。

它表示:这里有些事情编译器无法自动验证,程序员必须自己保证不破坏 Rust 的内存安全规则。

原文还提到 Miri。Miri 是 Rust 官方项目中的解释器工具,可以帮助发现某些未定义行为。它不能替代审查,但能让很多隐藏问题更早暴露。

这就是 Rust 的工程哲学:

不是假装底层复杂性不存在,而是把它隔离到明确边界里。


八、Rust 的核心规则:Aliasing XOR Mutability

原文后半部分讲了一个非常重要的概念:Aliasing XOR Mutability,简称 AXM。

可以简单理解成:

要么多个地方共享读取同一份数据,要么只有一个地方能修改它。不能既共享又修改。

多个共享引用是允许的:

rust 复制代码
let conn = Conn;
let a = &conn;
let b = &conn;
let c = &conn;

但多个可变引用不允许:

rust 复制代码
let mut conn = Conn;

let a = &mut conn;
let b = &mut conn; // 编译错误

为什么这么严格?

因为如果多个地方都能同时修改同一份数据,尤其是在多线程环境里,就可能出现数据竞争。数据竞争不是"业务逻辑 bug"那么简单,它可能导致内存破坏,甚至成为安全漏洞。

Rust 的借用检查器就是在维护这条规则。

当然,借用检查器并不完美。

有些程序在逻辑上是安全的,但编译器无法证明,于是会拒绝它。

这也是 Rust 学习曲线陡峭的原因之一:你不只是在写程序,还在学习如何把程序结构写成编译器能理解的形状。


九、当借用检查器太严格怎么办

Rust 不是只给你一条路。

如果你确实需要更灵活的共享和修改关系,可以把部分检查推迟到运行时。

单线程里常见组合是:

rust 复制代码
Rc<RefCell<T>>

多线程里常见组合是:

rust 复制代码
Arc<Mutex<T>>

RefCell 允许在运行时检查借用规则。
Mutex 允许多线程场景下通过加锁来保护数据。

这不是"绕过 Rust",而是用 Rust 提供的抽象选择不同的成本模型:

  • 编译期检查:更严格,零运行时成本
  • 运行时检查:更灵活,但有额外成本
  • unsafe:最灵活,但需要人工保证安全

Rust 并没有消灭复杂性。

它只是让你明确选择把复杂性放在哪里。


十、Rust 的承诺到底是什么

Rust 的承诺不是:

  • 写代码更短
  • 永远不用思考内存
  • 编译器永远接受你认为正确的代码
  • 所有复杂性自动消失

Rust 的承诺更接近于:

  • 资源所有权可以被类型系统表达
  • 函数能不能修改参数可以从签名看出来
  • 很多内存错误会在编译期暴露
  • 不安全代码有明确边界
  • 程序结构会被迫更诚实

这也是为什么很多人刚开始写 Rust 会觉得痛苦,但一旦"开窍",又很难回到以前的模式。

因为你会开始习惯这样的确定性:

rust 复制代码
fn read(x: &T)

只读。

rust 复制代码
fn write(x: &mut T)

可写,但必须独占。

rust 复制代码
fn consume(x: T)

拿走,之后不能再用。

这种表达力,就是 Rust 最吸引人的地方。


结语:Rust 让你更早面对真实问题

JavaScript 和 Go 往往把很多问题留到运行时。

C 和 C++ 允许你表达很多东西,但也允许你轻易绕过安全边界。

Rust 则选择在编译期尽可能多地拒绝不确定性。

这会让开发过程更慢一点,也会让初学者更容易受挫。

但它换来的是另一种确定感:当代码通过编译时,你知道很多低层风险已经被系统性排除。

Rust 的承诺,不是让编程变得轻松。

而是让那些原本隐藏在运行时、测试覆盖之外、代码审查盲区里的问题,尽可能提前出现在你面前。

当类型系统从敌人变成队友时,你写出来的程序不只是"能跑",而是更接近"结构上不容易错"。

这就是 Rust 的承诺。

相关推荐
fliter2 小时前
Rust 模块和文件不是一回事:一次讲清 `mod`、`use`、`pub use`
后端
绯雾sama2 小时前
易扣AI (Go + CloudWeGo) 企业级AI智能体项目教程 第2章:后端项目用户模块搭建
后端
fliter2 小时前
半小时读懂 Rust:从语法符号到所有权思维
后端
fliter2 小时前
深入 Rust enum 的内存世界
后端
_Evan_Yao2 小时前
从“全量发布”到“小步快跑”:灰度发布的简单实践与学习路径
java·后端·学习
石小石Orz2 小时前
给Claude增加状态栏显示:claude-hud保姆级教程
前端·人工智能·后端
折哥的程序人生 · 物流技术专研2 小时前
《Java 100 天进阶之路》第21篇:Java Object类
java·开发语言·后端·面试·哈希算法
喵个咪3 小时前
Kratos + WebRTC 实战:实现浏览器 P2P 音视频通话与实时数据通信
后端·微服务·webrtc