半小时读懂 Rust:从语法符号到所有权思维

内容概览

  1. 为什么 Rust 难读:不是语法多,而是规则密度高
  2. let 开始:绑定、类型标注、遮蔽与 _
  3. Rust 的表达式思维:代码块、ifmatch 都能产生值
  4. 结构体、模式匹配与方法:数据如何被组织和拆开
  5. 所有权、借用与生命周期:Rust 真正的核心
  6. Trait 与泛型:Rust 如何表达"多种类型的共同能力"
  7. OptionResult 与错误处理:别把异常藏起来
  8. 闭包与迭代器:Rust 代码为什么常常链式调用
  9. 结语:读懂 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 letmatch: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);
}

这在处理 OptionResult 时非常常见。


五、所有权: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 能兼顾抽象和性能的重要原因。


十、OptionResult:错误必须被看见

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);
}

迭代器经常和 mapfiltersum 组合:

rust 复制代码
let total: i32 = vec![1, 2, 3, 4]
    .into_iter()
    .filter(|x| x % 2 == 0)
    .map(|x| x * 10)
    .sum();

这段代码的意思是:

  1. 把向量变成迭代器
  2. 只保留偶数
  3. 每个数乘以 10
  4. 求和

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. 看返回值是不是 OptionResult

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,你会发现编译器不是在刁难你,而是在不断提醒你:这段程序的边界在哪里,风险在哪里,数据到底属于谁。

相关推荐
fliter2 小时前
深入 Rust enum 的内存世界
后端
_Evan_Yao2 小时前
从“全量发布”到“小步快跑”:灰度发布的简单实践与学习路径
java·后端·学习
石小石Orz2 小时前
给Claude增加状态栏显示:claude-hud保姆级教程
前端·人工智能·后端
折哥的程序人生 · 物流技术专研2 小时前
《Java 100 天进阶之路》第21篇:Java Object类
java·开发语言·后端·面试·哈希算法
喵个咪3 小时前
Kratos + WebRTC 实战:实现浏览器 P2P 音视频通话与实时数据通信
后端·微服务·webrtc
Gopher_HBo3 小时前
GoFrameMap转换详解
后端
小江的记录本3 小时前
【MySQL】《MySQL日志面试背诵版+思维导图》(核心考点 + MySQL 8.0最新优化)
java·数据库·后端·python·sql·mysql·面试
yoyo_zzm3 小时前
PHP vs Java:后端语言终极选择指南
java·spring boot·后端·架构·php
苏三说技术3 小时前
从索引失效到性能翻倍,DBA不愿透露的10个优化技巧
后端