Rust 为什么越来越受欢迎?开发者角度谈

我从 2023 年开始真正学习 Rust。实际上,2019 年左右我就已经跃跃欲试,不过当时学到闭包(也就是能捕获外部变量的匿名函数)的时候,就放弃了。

这门语言上手确实不容易,即使到了现在,不借助大语言模型(LLM),自己手写 Rust 代码,还是要花不少时间。

即使使用 Agent,想一次就写出能正确运行的过程宏,也很不容易。

不过,随着 AI 越来越强,我也越来越愿意在后端软件项目中选择 Rust。这篇文章,我想聊聊自己喜欢的几个 Rust 特性,以及让 LLM 写代码时,编译器能帮上什么忙。

所有权和借用

Rust 的基本规则是,每个值都有一个所有者。所有权没有转移时,所有者离开作用域,值就会被自动清理,不需要垃圾回收器,也不用手动安排释放。RcArc 则通过引用计数支持共享所有权,内部的值会在最后一个强引用被释放时清理。

如果只是想使用一个值,不需要它的所有权,就用 & 来借用。

借用检查器的规则可以这样理解:对同一个值,可以同时有多个共享引用,或者一个独占的可变引用,两者不能同时存在。

所有权和借用规则帮助安全 Rust 避免悬垂指针和重复释放。数据竞争涉及多个线程在缺少适当同步的情况下访问同一份数据,且至少一方在修改。借用规则配合 SendSync,约束值能否在线程间转移、引用能否在线程间共享,从而防止数据竞争。

当然,内存泄漏和死锁仍然可能发生。unsafe 允许解引用裸指针等额外操作,需要开发者自己保证这些操作的安全,但它不会关闭借用检查器。

默认不可变

除非显式写上 mut,否则变量绑定默认不可变。

写了这么多年代码,我认为对工程来说,默认不可变是更简洁、更安全的做法。默认可变或许降低了入门难度,但也容易增加后续的维护负担。

这个关键字很短,却能明确标出可变性:声明可变绑定时用 mut,显式创建可变引用时用 &mut

如果需要通过共享引用修改内部数据,可以使用内部可变性:RefCell 在运行时检查借用规则,Mutex 则通过互斥锁协调访问。

用 Option 代替 null,用 Result 处理错误

在 Rust 中,"没有值"由一个明确的类型表示:Option<T>

一个 String 就是一个实际存在的字符串。如果类型是 Option<String>,那就需要先检查它有没有值。

Rust 没有通常意义上的异常机制,失败也可以用值来表示:Result<T, E>。如果函数返回的是错误类型兼容的 Result,就可以用 ? 把错误继续向上传递。

match 必须覆盖所有可能的情况。没有用 _ 等兜底模式时,新增一个枚举变体后,哪些匹配还没处理它,编译器会直接列给你,不用自己在整个项目里到处找。

panic! 留给无法恢复的情况,而 unwrap 则是一条可能直接触发 panic 的捷径。

零成本抽象

写泛型函数时,我们可以用 trait 约束类型需要具备的能力,再写一份通用实现。比如 fn largest<T: Ord>(items: &[T]) -> &T,在约定输入非空的前提下,同一个函数体,既能处理整数切片,也能处理字符串切片。

迭代器是我每天都会用的东西。比如 items.iter().filter(|i| i.active).map(|i| i.id).collect::<Vec<_>>(),一行就能看明白它要做什么。而且,只有到了 collect 收集结果时,前面的迭代操作才会真正执行。

编译后,这两种写法都可以接近手写底层实现的性能:泛型会被单态化,也就是针对具体类型生成对应的代码;开启优化后,迭代器调用链通常可以被内联、合并,消除中间的抽象开销,具体结果取决于代码和优化情况。

成本还是有的,主要体现在构建时:编译时间可能更长,生成的二进制文件也可能更大。不过,这并不是什么不能接受的缺点。如果能用更长的编译时间换来更短的运行时间,这个买卖还是可以做的。

通常,可读性好的写法,性能也很好。我们不用为了让程序跑得快,就把代码写得难以理解。

编译错误,也能帮我们改代码

Rust 的编译错误会告诉你违反了哪条规则,标出发生冲突的代码行,而且经常会直接给出具体的修改建议。

我可以根据这些信息改代码,Agent 修正代码时也很需要这样的反馈。

比如,它可能会写出下面这样的代码,毕竟同样的逻辑放在 Python 或 JavaScript 里完全没问题:

rust 复制代码
fn greet(name: String) {
    println!("Hello {}", name);
}

fn main() {
    let name = String::from("Thomas");
    greet(name);
    greet(name);
}

rustc 给出的信息如下,还是相当详细明了的:

rust 复制代码
error[E0382]: use of moved value: `name`
 --> greet.rs:8:11
  |
6 |     let name = String::from("Thomas");
  |         ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
7 |     greet(name);
  |           ---- value moved here
8 |     greet(name);
  |           ^^^^ value used here after move
  |
note: consider changing this parameter type in function `greet` to borrow instead if owning the value isn't necessary
 --> greet.rs:1:16
  |
1 | fn greet(name: String) {
  |    -----       ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
7 |     greet(name.clone());
  |               ++++++++

一条错误信息,就把问题讲清楚了:String 没有实现 Copy,传参时会转移所有权。第一次调用已经把值交出去了,第二次自然不能再用。

编译器还标出了发生冲突的两行代码,并给了两种解决办法:把参数改成引用,或者克隆一份值。对于克隆,它既提醒了性能成本,也标出了需要插入的 .clone()

第一种办法解决了函数设计上的问题。这个函数本来就应该写成 fn greet(name: &str),调用时传入 &name

最初那个函数签名的问题就在这里:明明只需要读取参数,却要求拿走所有权。这样一来,每个调用方都得在克隆和交出所有权之间做选择。

Rust 会在第二次调用时拒绝编译,把问题指回函数签名,再给出修改建议。

一点想法

学 Rust 的时候,编译器经常让我卡住,但写得多了,我反而越来越看重这些约束。所有权、默认不可变和显式的错误处理,让很多问题在代码运行前就暴露出来。让 Agent 写代码时,这些规则和具体的报错也能帮它及时修正。对我来说,愿意花时间学 Rust,很大一部分原因就在这里。

This is why we love Rust!

相关推荐
孙启超1 小时前
【AI开发之Rust】第 6 课:结构体、枚举与方法 —— 开始定义自己的类型
开发语言·人工智能·后端·ai·rust
foolishlee1 小时前
Rust 下载依赖包
rust
蓝宝石的傻话1 小时前
MiBee Eye 蜂眼:把闲置的 Linux 小板做成一台能进国标平台的摄像头
golang·rust
传奇开心果编程2 小时前
【Rust入门知识点学与练】第36课:所有权——移动、借用、Copy、切片
开发语言·学习·rust
传奇开心果编程2 小时前
【Rust入门知识点学与练】第35课:工具链与反馈环——先学会读编译器在说什么
开发语言·学习·rust
刘发财11 小时前
前端2秒生成500页矢量PDF,rust真的强到没朋友
前端·javascript·rust
yume_sibai13 小时前
06-Rust Web 开发实战(Axum 框架 + 数据库 + JWT 认证 + 中间件 + 部署)
前端·数据库·rust
PC2005-cloud18 小时前
Rust学习笔记:控制流——if、loop、while、for与match
笔记·学习·rust
IT笔记18 小时前
Rust 迭代器多级链式调用执行顺序笔记
开发语言·笔记·rust