内容概览
- 为什么 Rust 难读:不是语法多,而是规则密度高
- 从
let开始:绑定、类型标注、遮蔽与_ - Rust 的表达式思维:代码块、
if、match都能产生值 - 结构体、模式匹配与方法:数据如何被组织和拆开
- 所有权、借用与生命周期:Rust 真正的核心
- Trait 与泛型:Rust 如何表达"多种类型的共同能力"
Option、Result与错误处理:别把异常藏起来- 闭包与迭代器:Rust 代码为什么常常链式调用
- 结语:读懂 Rust 的关键,是读懂约束背后的设计意图
本内容是对 A half-hour to learn Rust的翻译与整理
半小时读懂 Rust:从语法符号到所有权思维
很多人第一次看 Rust 代码,会有一种很微妙的感觉:
每个单词好像都认识,但连起来就不像熟悉的语言了。
比如:
rust
let value = items
.iter()
.filter(|x| x.is_active())
.map(|x| x.score)
.sum::<u32>();
这里有 let、点号、闭包、泛型、迭代器、turbofish 语法。单看都不复杂,放在一起就像一段压缩过的知识点。
fasterthanlime 的文章《A half-hour to learn Rust》有一个很好的切入点:先别急着"写好 Rust",先训练自己"读懂 Rust"。因为只有能读大量 Rust 代码,才会慢慢形成语感。
这篇文章就沿着这个思路,用一篇公众号文章的节奏,把 Rust 里最常见、最容易卡住的概念串起来。
一、let 不是变量声明那么简单
Rust 里用 let 创建一个绑定:
rust
let x = 42;
这里更准确的说法是"绑定",而不是"变量"。因为 Rust 默认是不可变的:
rust
let x = 42;
x = 43; // 编译错误
如果要修改,需要显式写 mut:
rust
let mut x = 42;
x = 43;
这个设计很重要。Rust 默认假设数据不变,只有你明确说"我要改",编译器才允许你修改。
类型可以写,也可以推断
rust
let x: i32 = 42;
: i32 是类型标注,表示 32 位有符号整数。很多时候可以不写,因为 Rust 会从上下文推断。
rust
let x = 42;
如果上下文不够,才需要手动标注。
_ 表示"我不关心"
rust
let _ = do_something();
这表示调用函数,但丢弃返回值。
如果你写:
rust
let _unused = 42;
这仍然是一个正常变量,只是以下划线开头,告诉编译器:"我知道它没被用,不要警告我。"
Shadowing:重新绑定同名变量
Rust 允许你这样写:
rust
let x = 10;
let x = x + 1;
let x = x.to_string();
这不是修改同一个变量,而是创建了新的绑定,遮蔽了旧的绑定。
这在 Rust 中很常见。比如先拿到一个数字,再把它转换成字符串,仍然叫 x。这样可以减少临时变量名的噪音。
二、Rust 里很多东西都是表达式
在很多语言里,if 是语句,只负责控制流程。但在 Rust 里,if 可以产生值:
rust
let level = if score > 90 {
"excellent"
} else {
"normal"
};
match 也一样:
rust
let label = match code {
200 => "ok",
404 => "not found",
_ => "unknown",
};
代码块也可以产生值:
rust
let result = {
let a = 1;
let b = 2;
a + b
};
注意最后一行 a + b 没有分号。
在 Rust 中,代码块最后一个没有分号的表达式,就是这个代码块的返回值。
函数也是这样:
rust
fn add(a: i32, b: i32) -> i32 {
a + b
}
这和下面写法等价:
rust
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
Rust 鼓励你把很多逻辑写成"表达式组合",而不是到处声明临时变量再赋值。
三、元组、结构体和模式匹配
Rust 有元组:
rust
let pair = ('a', 17);
let c = pair.0;
let n = pair.1;
也可以解构:
rust
let (c, n) = ('a', 17);
结构体更适合表达有名字的数据:
rust
struct Point {
x: f64,
y: f64,
}
let p = Point { x: 1.0, y: 2.0 };
字段顺序不重要,字段名才重要:
rust
let p = Point { y: 2.0, x: 1.0 };
结构体也能解构:
rust
let Point { x, y } = p;
如果只关心一部分字段:
rust
let Point { x, .. } = p;
这里的 .. 表示剩下的字段不关心。
四、if let 和 match:Rust 的模式匹配思维
Rust 的模式匹配不只是 switch 的替代品,它更像是一种"拆数据"的方式。
比如:
rust
struct Number {
odd: bool,
value: i32,
}
我们可以用 match 匹配结构:
rust
fn print_number(n: Number) {
match n {
Number { odd: true, value } => {
println!("odd: {}", value);
}
Number { odd: false, value } => {
println!("even: {}", value);
}
}
}
match 必须穷尽所有可能性。
这意味着如果你漏掉某种情况,编译器会提醒你。
如果只关心一种情况,可以用 if let:
rust
if let Some(value) = maybe_value {
println!("{}", value);
}
这在处理 Option、Result 时非常常见。
五、所有权:Rust 最核心的规则
Rust 真正不同的地方,不是语法,而是所有权。
看这段:
rust
let name = String::from("Rust");
let other = name;
println!("{}", name); // 编译错误
为什么?
因为 String 不是简单的栈上值,它拥有堆上的内存。
当 name 赋给 other 时,所有权移动了。之后 name 就不能再用了。
这叫 move。
但像 i32 这种简单类型可以复制:
rust
let a = 1;
let b = a;
println!("{}", a); // 可以
因为 i32 实现了 Copy。
借用:不拿走,只看看
如果不想转移所有权,可以借用:
rust
fn print_name(name: &String) {
println!("{}", name);
}
let name = String::from("Rust");
print_name(&name);
print_name(&name);
&String 是不可变引用。
它只是借来看,不拿走所有权。
如果要修改,需要可变引用:
rust
fn add_suffix(name: &mut String) {
name.push_str(" language");
}
let mut name = String::from("Rust");
add_suffix(&mut name);
这里有两个关键词:
rust
let mut name = ...
&mut name
一个表示变量本身可变,一个表示把它作为可变引用借出去。
六、借用规则:多个只读,或一个可写
Rust 的借用规则可以简单记成一句话:
同一时间,要么有多个不可变引用,要么有一个可变引用。
这样可以:
rust
let x = 42;
let a = &x;
let b = &x;
println!("{} {}", a, b);
这样不行:
rust
let mut x = 42;
let a = &x;
let b = &mut x; // 编译错误
println!("{}", a);
原因很朴素:
如果有人正在读,就不能同时有人改。
如果有人正在改,就不能有其他人读或改。
这套规则让 Rust 在没有垃圾回收的情况下,也能避免大量内存安全问题和数据竞争。
七、生命周期:引用不能活得比数据更久
生命周期听起来很吓人,其实它解决的问题很直接:
引用不能指向已经不存在的东西。
比如:
rust
let r = {
let x = 42;
&x
};
println!("{}", r);
这段不合法。因为 x 在代码块结束后就被释放了,r 不能继续引用它。
很多时候生命周期可以省略,编译器会帮你推断:
rust
fn value(n: &Number) -> &i32 {
&n.value
}
它实际表达的是:
rust
fn value<'a>(n: &'a Number) -> &'a i32 {
&n.value
}
意思是:返回值引用的生命周期,和输入参数 n 的生命周期绑定。
如果你理解了这一点,生命周期就不再是玄学,而是"引用关系的有效范围"。
八、Trait:Rust 的接口系统
Trait 描述一种能力:
rust
trait Signed {
fn is_negative(self) -> bool;
}
然后你可以给自己的类型实现它:
rust
struct Number {
value: i32,
}
impl Signed for Number {
fn is_negative(self) -> bool {
self.value < 0
}
}
也可以给某些已有类型实现你自己的 trait:
rust
impl Signed for i32 {
fn is_negative(self) -> bool {
self < 0
}
}
但 Rust 有孤儿规则:
你不能给外部类型实现外部 trait。
也就是说:
- 你的 trait,可以实现到别人的类型上
- 别人的 trait,可以实现到你的类型上
- 别人的 trait,不能实现到别人的类型上
这是为了避免不同 crate 之间出现实现冲突。
九、泛型:让函数适配多种类型
最简单的泛型函数长这样:
rust
fn identity<T>(x: T) -> T {
x
}
但很多时候,泛型需要约束。
比如要打印一个值,必须要求它实现 Debug:
rust
use std::fmt::Debug;
fn print<T: Debug>(value: T) {
println!("{:?}", value);
}
复杂一点可以用 where:
rust
fn compare<T>(left: T, right: T)
where
T: Debug + PartialEq,
{
println!("{:?} == {:?}: {}", left, right, left == right);
}
Rust 的泛型不是运行时擦除,而是会在编译期为具体类型生成对应代码。
这也是 Rust 能兼顾抽象和性能的重要原因。
十、Option 和 Result:错误必须被看见
Rust 没有传统意义上的空指针。
可能为空的值通常用 Option:
rust
enum Option<T> {
Some(T),
None,
}
可能失败的操作通常用 Result:
rust
enum Result<T, E> {
Ok(T),
Err(E),
}
比如字符串解析:
rust
let n: Result<i32, _> = "42".parse();
可以 match:
rust
match n {
Ok(value) => println!("{}", value),
Err(err) => println!("parse error: {}", err),
}
也可以用 ? 把错误向上传递:
rust
fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
let n = s.parse::<i32>()?;
Ok(n)
}
? 是 Rust 里非常常见的错误处理方式。
它的意思是:如果是 Ok,取出里面的值;如果是 Err,直接返回错误。
十一、String 和 &str:拥有和借用的区别
Rust 里经常会看到成对出现的类型:
rust
String 和 &str
Vec<T> 和 &[T]
PathBuf 和 &Path
前者通常是拥有数据的类型,后者通常是借用视图。
比如:
rust
let name: String = String::from("readme.txt");
let slice: &str = &name;
String 拥有字符串内容。
&str 只是指向一段字符串数据。
切片也是类似:
rust
let nums = vec![1, 2, 3, 4, 5];
let part = &nums[1..3];
part 不拥有数据,它只是借用了 nums 的一部分。
所以只要 part 还在用,nums 就不能被随意修改或释放。
十二、闭包:带环境的函数
Rust 闭包写在一对竖线中:
rust
let double = |x| x * 2;
println!("{}", double(21));
闭包可以捕获外部变量:
rust
let greeting = String::from("hello");
let say = |name| {
println!("{}, {}", greeting, name);
};
say("Rust");
如果闭包需要拿走外部变量,可以用 move:
rust
let greeting = String::from("hello");
let say = move |name| {
println!("{}, {}", greeting, name);
};
Rust 根据闭包如何使用捕获的变量,把闭包分成几类:
Fn:只读捕获的环境FnMut:会修改捕获的环境FnOnce:会消费捕获的变量,只能调用一次
这套分类听起来复杂,但本质还是所有权和借用规则在闭包上的自然延伸。
十三、迭代器:Rust 代码为什么常常链式调用
Rust 里的 for 可以遍历很多东西:
rust
for n in vec![1, 2, 3] {
println!("{}", n);
}
也可以遍历引用:
rust
let nums = vec![1, 2, 3];
for n in &nums {
println!("{}", n);
}
迭代器经常和 map、filter、sum 组合:
rust
let total: i32 = vec![1, 2, 3, 4]
.into_iter()
.filter(|x| x % 2 == 0)
.map(|x| x * 10)
.sum();
这段代码的意思是:
- 把向量变成迭代器
- 只保留偶数
- 每个数乘以 10
- 求和
Rust 的迭代器通常是零成本抽象。也就是说,写法高级,但编译后往往能优化成高效代码。
十四、读 Rust 的几个抓手
如果你刚开始读 Rust,可以先盯住这些信号。
1. 看 let 后面有没有 mut
rust
let x = ...
let mut x = ...
有没有 mut,决定这个绑定能不能被修改。
2. 看函数参数有没有 & 或 &mut
rust
fn read(x: &Data)
fn write(x: &mut Data)
fn consume(x: Data)
这三种含义完全不同:
&Data:借来看&mut Data:借来改Data:拿走所有权
3. 看返回值是不是 Option 或 Result
rust
fn find() -> Option<Item>
fn load() -> Result<Item, Error>
这说明调用方必须处理"没有"或"失败"。
4. 看 match 是否穷尽
rust
match value {
Some(x) => ...
None => ...
}
Rust 会逼你处理所有情况,这是它安全性的来源之一。
5. 看生命周期是不是在表达"谁依赖谁"
rust
fn get<'a>(x: &'a Data) -> &'a str
这不是装饰语法,而是在说:返回的引用不能比输入活得更久。
结语:Rust 不是难在语法,而是难在诚实
Rust 代码看起来密度高,是因为它把很多隐含的事情显式写出来了:
- 谁拥有数据
- 谁只是借用
- 谁可以修改
- 哪些情况可能失败
- 引用能活多久
- 泛型需要满足什么能力
这些规则一开始会让人觉得啰嗦,但它们换来的是更少的运行时意外。
所以学习 Rust 的第一步,不一定是马上写出优雅代码。
更现实的目标是:看到一段 Rust,能读懂每个符号在表达什么约束。
一旦你开始能读懂 Rust,你会发现编译器不是在刁难你,而是在不断提醒你:这段程序的边界在哪里,风险在哪里,数据到底属于谁。