从昨天敲下第一行println!到现在,满打满算也就十几个小时,但我已经明显感觉到 Rust 和其他语言不太一样。都说 Rust 很难,真正上手之后我发现难点其实不在语法本身,而在于你要用一套完全不同的思维方式去写代码——每写一行,编译器都在问你:这个数据到底归谁管?会不会没人要了?会不会有人同时改?
Day 2 我给自己定的目标是三件事:把开发环境调到最顺手、认认真真写一个能跑的小程序、最重要的是把"所有权"这个概念啃下来。现在一天下来,我觉得可以交一份还不错的作业了。
这篇文章全部是我今天踩过的坑、试过的方法、反复读文档才想明白的思路,给和我一样刚在坑里爬的新手做个参考,也给自己留个备份。
1. 先把地基打牢:Rust 工具链与 VSCode 环境的完整搭建
昨天我其实只是装了 rustup 然后跑了个 hello world,离"能工程化写代码"还差得远。今天上午专门花时间把环境从头理了一遍,包括工具链、编辑器、路径配置这些细节。这些基础的东西看起来不起眼,但后面每行代码都依赖它们。
1.1 用 rustup 管理工具链,而不是直接装 Rust
我在官网 ruffle 了一圈,最后选的还是 rustup 方案。原因很简单:Rust 的版本更新节奏很快,稳定版大概每 6 周发一次,如果手动下载安装包管理版本会非常痛苦。rustup 相当于 Rust 官方的版本管理器,可以随时切换 stable、beta、nightly 工具链,还能按项目目录覆盖版本——这对后面想试 nightly 特性或者做嵌入式开发的人来说是刚需。
安装我是在 Linux 环境下做的,Windows 用户其实也差不多,但需要注意一点:不要通过国内某些所谓的"一键安装包"装 rustup,直接用官方脚本最稳。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh脚本运行完会提示你选择安装方式,我直接选了默认的 stable 工具链。装完以后有个关键步骤:重新打开终端,让 PATH 环境变量生效。然后验证一下三件套:
rustc --version cargo --version rustup --version我记得当时cargo --version显示的版本和rustc对不上,后来才发现是因为系统里之前装过旧版系统的 cargo,和 rustup 管理的冲突了。解决方法是把~/.cargo/bin的路径放到 PATH 最前面,或者干脆把系统自带的/usr/bin/cargo卸掉。这不是 Rust 特有的事,玩 Python 遇到过 Pyenv 的人应该秒懂。
注意:不要用
sudo apt install rustc这类系统包管理器直接装 Rust。版本旧、更新麻烦,而且和 rustup 岔开之后容易出现"编译器版本不一致"的玄学问题。
1.2 VSCode 里必装的三个插件,一个都不能少
编辑器我用的是 VSCode,因为 Rust 生态里 IDE 的老大其实是 rust-analyzer 项目,它作为语言服务器的协议实现,在 VSCode 里体验最顺。装插件这件事没什么技术含量,但选对插件有讲究。
我实际安装且现在正在用的有这三个:
- rust-analyzer:核心中的核心。提供代码补全、跳转定义、类型标注、错误提示。现在 rust 官方插件(rust-lang.rust)已经基本被它取代了,所以直接搜 rust-analyzer 装官方发布版就好。
- crates:Cargo.toml 里写依赖的时候,它能提示最新版本号,还能一键升级。这个后期写项目非常香。
- Even Better TOML:格式化 Cargo.toml 的,主要是看着舒服,也能避免手动改配置文件时把缩进弄乱。
装完后有个常见坑:打开 .rs 文件,右下角出现"Rust Analyzer: failed to discover workspace"之类的提示,大概率是 r-a 没找到 Cargo.toml。这时候直接在命令面板里执行rust-analyzer: Restart server一般能好。如果还不行,八成是 path 配置问题,去 VSCode settings.json 里手动指一下:
{ "rust-analyzer.cargo.buildScripts.enable": true, "rust-analyzer.checkOnSave.command": "clippy", "rust-analyzer.check.command": "check" }其中把 checkOnSave 设为 clippy 是我个人的偏好,它会用 clippy linter 帮你做静态检查,比默认的 rustc 报错更详细,很多不符合惯例的写法都会被提示。
1.3 给 Cargo 换个更快的下载源
这一步算是新手必做。Rust 的依赖包都从 crates.io 拉,国内访问经常慢得离谱,慢到cargo build卡在 "Updating crates.io index" 半天不动。我果断配置了清华的镜像源(也可以选其他稳定的镜像,原理一样)。
在~/.cargo/config.toml里写:
[source.crates-io] replace-with = "tuna" [source.tuna] registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"然后随便建一个项目测一下速度,第一次cargo build如果还是慢,耐心点,因为它在拉索引全量缓存,后面就快了。这里还想提一嘴:不要因为下载慢就所有依赖全都手动去 GitHub 找 release 包,Cargo 的锁定机制Cargo.lock是会帮你管好版本的,手动换包很容易把自己坑了。
2. Cargo 项目的结构和第一个能算数的小工具
搭建完环境,下一步必须上手写真正的代码。Hello World 只能证明你装了编译器,但一个像样的项目要能用 Cargo 来组织、编译、运行,而且能看懂 Cargo.toml 里的每一项是什么意思。今天我做了一个超简单的 fizzbuzz 增强版——一个能处理输入参数、有基础错误处理的小命令行工具,麻雀虽小五脏俱全。
2.1 Cargo 新建项目,到底生成了什么东西
用cargo new day2_demo --name fizzbuzz_cli建一个二进制项目,进去以后会看到:
day2_demo/ ├── Cargo.toml ├── src/ │ └── main.rs └── .gitignorecargo new默认还会初始化 git 仓库,这点比很多脚手架都省事。核心文件 Cargo.toml 长这样:
[package] name = "fizzbuzz_cli" version = "0.1.0" edition = "2021"这里的edition是 Rust 的版本纪元。2021 是最常用的,如果老项目看到 2015 或 2018,建议考虑升级处理,但先不操这个心,知道有这回事就行。
依赖部分写在[dependencies]里。我今天这个程序想做得稍微像样一点,就用了两个 crates:
[dependencies] clap = { version = "4.4", features = ["derive"] } anyhow = "1.0"clap是 Rust 最常用的命令行参数解析库,功能非常全,derive特性可以让你用结构体+宏来定义参数规则。anyhow是一个错误处理库,提供便捷的错误链包装。对新手来说,它会让你写出?操作符时省掉一大段手写错误转换的代码。
提醒:新手一开始不要习惯性堆依赖。Rust 编译器很聪明,你用了
use才能编译,没用的依赖一多,cargo 会警告,而且编译时间肉眼可见地变长。
2.2 命令行接收参数,不再写死逻辑
用 clap 定义参数很爽,直接在 main.rs 里写结构体:
use clap::Parser; #[derive(Parser)] #[command(name = "fizzbuzz_cli", version = "1.0", about = "FizzBuzz 命令行版本")] struct Cli { /// 要跑到的最大数字 #[arg(short, long, default_value_t = 20)] num: u32, } fn main() -> anyhow::Result<()> { let cli = Cli::parse(); for i in 1..=cli.num { fizzbuzz_print(i); } Ok(()) }这里#[derive(Parser)]是 clap 的 derive 宏,它会在编译代码时自动根据结构体字段生成参数解析逻辑。你可能不习惯这种"写代码还要靠宏"的风格,但在 Rust 里这是常态,编译器帮你生成代码,减少手写样板。
2.3 函数和基本控制流的实战练习
fizzbuzz_print这个函数本身很简单,但有一个我今天的收获——优先用条件表达式而不是语句,可以让代码更贴近 Rust 的表达风格:
fn fizzbuzz_print(n: u32) { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{}", n), } }用match+ 元组模式匹配是我今天最惊喜的一刻。想清楚逻辑再动手:"既能被 3 整除又能被 5 整除"就是(0, 0),这个条件表达式非常直观,且不会出现 if 嵌套地狱。Rust 里match是表达式而不是语句,它是有结果的,这种设计贯穿整个语言。
编译运行一下:
cargo run -- --num 15终端输出:
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz这个例子小,但它把cargo run传参、match模式匹配、println!格式化这些要点都过了一遍。写完后我随手cargo clippy了一把,居然有个警告——原来是println!("{}", n)里的 n 明明实现了 Display,不需要手写{}之前的空格,改了注释才发现自己还是带着 C 语言的习惯,老想加空格。
3. 今日最硬核:把 Rust 所有权这套逻辑彻底搞懂
如果说今天有什么知识点让我头皮发麻,那就是所有权。简单说:Rust 的每个值都有一个"主人",主人的职责是负责在适当的时候清理这个值的资源。当所有权转移、借用、拷贝这些概念混在一起,我感觉自己像一个刚接触引用计数的法学生。
难归难,但这个概念有一个非常生活化的类比帮了我大忙——图书馆借书。
3.1 移动与拷贝:同是赋值,行为天差地别
先来看一个让我懵了半天的例子:
let s1 = String::from("hello"); let s2 = s1; // s1 的所有权"移动"到了 s2 println!("{}", s1); // 编译错误!s1 已经被移动我第一反应是:"不就是赋值给另一个变量吗?按 Python 的思维,s1 和 s2 应该都指向同一个字符串才是。"但 Rust 不是这个逻辑。String是堆上分配的数据,s1 持有的是指向堆数据的句柄。把 s1 赋值给 s2 后,为了避免两个变量同时释放同一块堆内存导致"双重释放",Rust 直接让 s1 失效——这就叫移动。s2 成了唯一合法的 owner,只有它能负责释放那块内存。
而基本类型就不一样:
let a = 10; let b = a; println!("{}", a); // 没问题!因为i32这类标量类型实现了Copytrait,赋值时是拷贝整个值,没有堆内存的归属转移,两个变量各自独立。这个设计很像传统 C 语言里的按值传递,但规则标注得非常清晰,编译器用类型系统把行为给定死了。
3.2 借用:你自己不拥有,但可以暂时看一眼
只要涉及函数传参,所有权就会被疯狂牵扯。想把变量传进函数,但传完之后还想继续用原来的变量,怎么办?答案是借用——把引用传给函数,而不是把所有权交出去。
fn calc_len(s: &String) -> usize { s.len() } fn main() { let s = String::from("hello"); let len = calc_len(&s); println!("{} length is {}", s, len); // s 还能用! }&String就是对一个String的不可变引用,函数里只是借来看一眼,不会拿走所有权。对应到借书的场景:图书馆把书借给你看,看完你还给图书馆,书的所有权始终在图书馆。
但如果你想在函数里修改原来的字符串呢?那就要用可变引用&mut:
fn change(s: &mut String) { s.push_str(" world"); } let mut s = String::from("hello"); change(&mut s); println!("{}", s); // hello world这里有两个非常容易踩的坑我记下来了:
- 借用一个变量时,原变量必须声明为
mut,否则无法创建可变引用。 - 同一作用域内,对同一个数据不能同时存在多个可变引用。否则编译器会直接拒绝编译,这是为了防止数据竞争。这和很多语言里"谁都能改"的风格差异非常非常大。
3.3 生命周期和切片:借用规则的最日常案例
借用规则延伸到字符串切片,是我今天练习时遇到的又一关卡。&str本质上是一个"借用来的字符串视图",它不像String拥有数据,只是指向某段字符串数据的引用。
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[..i]; } } &s[..] }这个函数做的事:输入一个字符串引用,返回第一个空格之前的部分。你能看到返回的&str的生命周期其实和传入的s绑定在一起——如果s消失了,返回的引用也不能存续,Rust 通过生命周期标注来强行保证这一点。
我刚写这个函数时,第一个版本直接返回&s[..i],但函数签名没标注生命周期,编译器直接报错。用&str作为参数和返回值,Rust 会把它们的生命周期自动关联起来,这是生命周期省略规则的一部分。等以后写到更复杂的 struct 时,控制生命周期标注就会变成真正的必修课。
3.4 实操验证:用编译错误逼自己想清楚
今天最有价值的练习是我故意写了几段会被编译器拒绝的代码,读编译器给我的每条错误提示。Rust 的编译器错误提示是我见过所有语言里最友好的,它会很具体地告诉你"第几行发生了什么、为什么不行、建议怎么改"。
比如我故意写这段代码:
let mut v = vec![1, 2, 3]; let first = &v[0]; v.push(4); println!("{}", first);编译器错误是:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable它像一位严格的老师:你已经借了v的第一个元素出来看,还没还呢,现在你又想把新元素推到 vec 后面,这可能导致底层数组重新分配内存,first这个引用就悬空了。Rust 在编译期把这种"悬空引用"直接拦截下来,而不是让它到了运行时才爆出来。
说实话,这种情况在 C/C++ 里是经典的未定义行为,但在 Rust 里几乎不可能写出来。很多人说 Rust 难,很大程度上是因为它不让你按以前的经验随便写——但一旦适应了这套约束,代码的可靠性会提升一个档次。
4. 新手最容易翻车的五个细节,我全踩了一遍
一天下来我记录了好几个"看起来没问题但编译器拼命报错"的场景,这些大多不是知识点的问题,而是习惯问题。如果这几条你在入门阶段就注意到,真的能省下不少掉头发的时间。
4.1 分号是表达式和语句的分界线
Rust 里函数体的最后一个表达式决定了返回值,多写一个分号就等于变成"语句",函数就变成返回()了。我第一天就因为这个下面这个错误困惑了很久:
fn add_one(x: i32) -> i32 { x + 1; // 多了分号,返回值变成了 () }编译器报错是"expectedi32, found()",很明确的提示。修掉分号就一切正常。
这个设计其实很优雅:表达式到处都能用,let y = if x > 5 { 10 } else { 20 };在 Rust 里是合法写法。但这也意味着你写带分号的语句时要时时留意函数最终返回的是什么。
4.2 unwrap 可以先用,但别养成终身习惯
Result和Option这两个枚举是 Rust 处理可失败操作和"可能没有值"的核心工具。新手最省事的做法是.unwrap()——直接在值可用时取出,没有值就 panic。但这会掩盖错误,特别是在写库给别人用的时候,panic 远远糟糕于返回错误。
我在 fizzbuzz 里用anyhow的原因,就是为了把错误传播出来而不是让程序炸掉。今天学到的一个小技巧是:
let num = args.next().ok_or_else(|| anyhow!("missing argument"))?;这里?操作符会把Err分支提前返回,错误链会带着上下文继续往上走,比 unwrap 体面得多。
4.3 格式化输出,别偷懒用{}一把梭
println!里{}用的是Display,{:?}用的是Debug。结构体一般需要手动#[derive(Debug)]才能打印。我一开始不知道这一点,想打印一个结构体直接用{}直接报错。
#[derive(Debug)] struct Points { x: i32, y: i32, }之后做调试时,println!("{:?}", p)就能看到一个结构化的输出,这点在开发时非常有用。如果字段特别多,还能用{:#?}让它带缩进展示,看起来舒服很多。
4.4 模块引入路径:绝对路径还是相对路径?
今天建项目写代码时,我还顺便整理了模块的概念。Rust 的模块路径有两种风格:以 crate 根开始的绝对路径、以及self/super开始的相对路径。
use crate::utils::format; // 或 use super::parent::something;新手刚接触可能分不清什么时候用哪个,我的建议是:组件少的小项目用相对路径,工程大以后从 crate 根开始用绝对路径,避免深层模块改动时牵一发而动全身。而且use在 Rust 里只引入到当前作用域,不像 Java 的 package 会全局生效,理解这一点就不容易混淆模块和命名空间。
4.5 大项目先把模块规划清楚,别把什么都塞进 main.rs
今天项目小,main.rs 里写两三个函数没问题,但任何超过 200 行的代码就不适合堆在 main.rs 里了。Rust 组织代码的官方模型是模块系统——两个文件,mod声明和文件之间的关联关系有很多种方式,还没必要今天细究,但结论先记住:一开始就把代码拆成小模块,后面重构的痛苦会小很多。
5. 展望一下:学好这些,Tauri 和 OPC UA 都是好去处
学 Rust 的人迟早要回答一个问题:学它干嘛?我今天学完基础语法后,专门研究了一下几个热门方向的路线,给自己定个目标,也给看到这篇文章的人参考。
5.1 Tauri:用 Rust 写桌面应用的新浪潮
Tauri 对我这种前端背景的人非常有吸引力。它允许你用 HTML/CSS/JS(或任何编译到 Web 的前端框架)做界面,但核心后端二进制是用 Rust 写的,启动体积和内存占用都比 Electron 小一截。用过 Electron 的人都知道那个"一个音乐软件占 500MB 内存"的痛,Tauri 在系统资源占用方面的优势是很明显的。
学 Tauri 需要的基础其实不算高:掌握 Rust 的所有权、借用,知道struct和enum,再了解一点tokio异步运行时和 webview 交互的基本概念,就可以开始动手做 TODO 应用了。如果你以前有 Node 经验,上手会更顺,因为它很多模式看起来像 Node 的后端服务。
周末我打算用 Tauri 做一个文件批量重命名的小工具,正好可以检验今天学的所有权和命令行参数解析。
5.2 OPC UA:工业自动化的通信基石
另一个我高强度关注的方向是 OPC UA。在工业自动化和物联网领域,OPC UA 是设备之间通信的重要标准之一。Rust 在这块也有对应的 crate,比如opcua和配套的客户端/服务器示例,能让 Rust 开发者直接用强类型的方式去写工业设备通信的数据采集和下发逻辑。
之所以 Rust 在这块有戏,是因为工业场景对内存安全、实时性、并发能力都有极高要求,而这些恰好是 Rust 的强项。安全性上,Rust 不用 GC 就能保证内存安全,对嵌入式设备和工控网关来说非常适合。如果你本身是自动化行业出身,又把 Rust 基础打牢了,这个方向会很有竞争力。
官方 GitHub 上有不少 tauri + rust 开发桌面应用的 demo,也有一些 opcua 的完整客户端服务器 demo。我今天的建议是:先别急着直接啃这些大库,先把 Rust 核心语法和所有权练到能闭眼写的程度,然后从最小 demo 开始,拆出它的信息模型、通信握手、安全策略这些层,逐个击破。
6. 今日总结与给同样在 Day 2 的你的实操建议
一天下来其实做了不少事,但最值得记住的不是代码量,而是那几个关键节点的心理转变——原来编译错误没那么可怕,被拒绝反而是一种保护。
由于今天踩了太多坑,我把最值得记住的几条实践经验写在这里,方便你翻车时快速查:
- 环境问题排查顺序:PATH 是不是有问题 -> rustup toolchain 是否已安装 -> 编辑器插件有没有检测到 Cargo.toml。绝大多数"rust-analyzer 不工作"的问题都出在这三步。
- 遇到编译错误别急着搜报错文本,先自己读一遍错误信息,编译器已经告诉了你"为什么"和"建议怎么做",这一点远比你想的值钱。
- 每个新概念都拿一个小程序练手。今天我用 fizzbuzz 练了 match 和命令行解析;用
first_word练了切片和生命周期;用故意写错的代码练了借用检查。这样学,比对着书抄十遍概念有用得多。 - 别盲目追新库。Rust 的 crate 生态里,有些库文档不全、维护不活跃,选库前先去 crates.io 看下载量、更新时间,再看 GitHub 的 issue 活跃度,能避掉大部分坑。
- 今天可以白日做梦想想最终目标。学 Tauri 还是 OPC UA,还是做 WebAssembly、嵌入式、后端。没有目标,Rust 中后期很容易迷失在逃离借用的泥潭里。
Day 2 暂时到这里。虽然我还写不出多复杂的程序,但至少现在打开终端跑起cargo run,心里不慌了。接下来几天我会继续把基础语法揉碎,顺便开始动手搞第一个 Tauri 小项目,到时候再写一篇实操记录。
如果你也是在 Day 2 前后的新手,欢迎一起交流踩坑经历。这语言确实不容易,但每过一次编译器审查,都是实打实的成长。