第 1 课. Rust 简介、安装与第一个程序
本课是基础。后续所有内容都建立在运行 cargo 的习惯、理解 build 和 check 的区别、以及了解工具链包含哪些组件的基础上。如果你对搭建环境和运行第一个程序有信心,后续会越来越轻松。
理论
什么是 Rust 以及为什么选择它
Rust 是一门系统级编程语言,设计目标主要有三个:
- 无垃圾回收器的内存安全。没有
null指针、悬垂引用、数据竞争、双重释放、释放后使用 —— 全部在编译时捕获。 - C/C++ 级别的性能。无运行时,零成本抽象:泛型、迭代器、闭包编译为最高效的代码。
- 人体工程学与工具链。现代包管理器、格式化工具、linter、文档生成、测试和 LSP 服务器 —— 全部"开箱即用"。
在工业界,Rust 用于操作系统(Linux 的一部分、Redox、Fuchsia)、浏览器引擎(Servo、Firefox 的一部分)、网络服务(Cloudflare、Discord、Dropbox)、嵌入式和 IoT(微控制器、RTIC)、区块链(Solana、Polkadot)、游戏开发(Bevy)、CLI 工具(ripgrep、fd、bat、exa)以及 ML 基础设施(Hugging Face Candle、重度 PyTorch kernel)。
安全性通过 所有权(ownership)、借用(borrowing) 和 生命周期(lifetimes) 系统实现,由编译器检查。代价是更陡峭的学习曲线 —— 编译器一开始会和你"争论",但它会在生产环境中帮你避免一整类 bug。
通过 rustup 安装工具链
官方安装程序叫做 rustup。它管理:
- Toolchains ——
stable、beta、nightly。默认使用stable,nightly启用实验性功能。 - 交叉编译目标 —— 例如
wasm32-unknown-unknown、aarch64-unknown-linux-gnu。 - 组件:
rustfmt(格式化)、clippy(linter)、rust-analyzer(LSP)、rust-src(标准库源码)、miri(查找 UB 的解释器)。
macOS/Linux 上的安装:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
在 Windows 上,从 rustup.rs 运行 rustup-init.exe。
安装后,以下命令很有用:
rustup update # 更新所有 toolchain
rustup default stable # 设置 stable 为默认
rustup toolchain install nightly # 安装 nightly
rustup component add clippy rustfmt rust-analyzer
rustup target add wasm32-unknown-unknown
rustup show # 显示已安装和当前激活的内容
rustc --version
cargo --version
Cargo:包管理器和构建系统
cargo 是 Rust 的"瑞士军刀"。需要掌握的最小命令集:
| 命令 | 功能 |
|---|---|
| `cargo new |
创建二进制项目(`--lib` 创建库项目) |
| `cargo init` | 在当前目录初始化项目 |
| `cargo build` | 构建到 `target/debug` |
| `cargo build --release` | 优化构建到 `target/release` |
| `cargo run` | 构建并运行 |
| `cargo run -- arg1 arg2` | 带参数运行 |
| `cargo check` | 快速类型检查,不生成代码 |
| `cargo test` | 运行测试 |
| `cargo clippy` | linter 检查 |
| `cargo fmt` | 格式化代码 |
| `cargo doc --open` | 生成并打开文档 |
| `cargo add |
添加依赖到 Cargo.toml |
| `cargo tree` | 依赖树 |
| `cargo clean` | 删除 `target` |
Cargo.toml —— 项目清单(元数据、依赖、edition)。Cargo.lock —— 精确版本快照,用于可复现构建。
项目结构
执行 cargo new hello_rust 后你会得到:
hello_rust/
├── .git/
├── .gitignore
├── Cargo.toml
└── src/
└── main.rs
src/main.rs —— 二进制 crate 的入口点。对于库则是 src/lib.rs。
Cargo.toml 的内容:
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
edition —— 语言的"方言"(2015、2018、2021、2024)。不会破坏 crate 兼容性,只改变单个 crate 内的某些解析规则和标准导入。默认选择最新版。
第一个程序:hello, world
fn main() {
println!("Hello, Rust!");
}
立即记住几个重要事项:
fn main()—— 可执行 crate 的入口点。main通常返回类型()或Result<(), E>。println!—— 这是一个宏,不是函数。感叹号是宏调用标记。宏在编译时检查格式字符串,支持可变数量的参数。- 函数体是块
{ ... },本身就是一个表达式。 ;放在语句末尾。没有;时该行成为块的返回值表达式(后面的课程会用到)。
格式化输出
Rust 使用与 Python 相同的占位符风格:
fn main() {
let name = "Alice";
let age = 30;
println!("Hello, {}! You are {}.", name, age); // 位置参数
println!("Hello, {name}! You are {age}."); // 命名参数(1.58 起支持)
println!("{0} and {1}, {0} again", "alpha", "beta"); // 按索引
println!("{:>10}", "right"); // 10 字符宽度右对齐
println!("{:0>5}", 42); // 00042
println!("{:.3}", 3.1415926); // 三位小数
println!("{:#?}", vec![1, 2, 3]); // Debug 美化打印
eprintln!("this goes to stderr");
}
cargo build vs cargo check vs cargo run
理解它们的区别可以节省大量时间:
cargo check—— 经过编译器前端:解析、名称解析、类型检查、borrow checker。不运行 LLVM 代码生成和链接。因此快 3-10 倍。cargo build—— 增加代码生成(LLVM IR -> 机器码)和链接。产出构建产物。cargo build --release—— 优化构建。较慢,但运行速度快数十倍。cargo run=cargo build+ 运行。
在"编辑代码 —— 检查错误"循环中,使用 cargo check 或 cargo clippy。需要可执行文件时才用 build。
新手常见陷阱
- **不要混淆
println!和 **print!—— 前者会添加换行符。 println!("{}", x)** 需要 ****Display**,而println!("{:?}", x)需要Debug。大多数自定义类型没有前者,后者可以通过#[derive(Debug)]自动添加。{name}** 只对作用域内的变量有效**。如果要替换表达式,请使用let tmp = ...; println!("{tmp}");或println!("{}", expr)。- 关闭所有括号。Rust 编译器非常友好,把错误信息读完 —— 里面通常有提示和建议修复方案。
实践
步骤 1. 安装和验证
rustup update
rustup default stable
rustup component add clippy rustfmt
rustc --version
cargo --version
步骤 2. 第一个项目
cargo new hello_rust
cd hello_rust
cargo run
步骤 3. 问候程序
将 src/main.rs 替换为:
fn greet(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let people = ["Alice", "Bob", "Charlie", "Diana"];
for n in people {
greet(n);
}
println!("Total greeted: {}", people.len());
}
运行 cargo run。然后尝试:
- 将
people数组改为Vec<&str>:let people = vec!["Alice", "Bob"];。 - 添加计数器:使用
for (i, n) in people.iter().enumerate()并打印{i}: {n}。 - 让
greet函数通过format!返回String。
步骤 4. 命令行参数
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("usage: {} <name>", args[0]);
std::process::exit(1);
}
println!("Hello, {}!", args[1]);
}
运行:cargo run -- Alice。双横线分隔你的程序参数和 cargo 的参数。
步骤 5. 简单计算器
fn main() {
let a: f64 = 2.5;
let b: f64 = 4.0;
println!("{a} + {b} = {}", a + b);
println!("{a} - {b} = {}", a - b);
println!("{a} * {b} = {}", a * b);
println!("{a} / {b} = {}", a / b);
println!("{a:.2} cubed = {:.2}", a.powi(3));
println!("square root of {b} = {}", b.sqrt());
}
步骤 6. 代码规范
在 main.rs 开头添加严格设置并通过 linter 运行:
#![deny(warnings)]
#![warn(clippy::pedantic)]
运行:
cargo fmt
cargo clippy -- -D warnings
如果 linter 有警告 —— 修复它们。这是从第一天就应养成的好习惯。
步骤 7. 从 crates.io 添加依赖
添加彩色输出的库:
cargo add colored
src/main.rs:
use colored::Colorize;
fn main() {
println!("{}", "success".green().bold());
println!("{}", "warning".yellow());
println!("{}", "error".red().on_white());
}
运行 cargo run —— 终端会显示颜色。cargo tree 会显示拉入了哪些依赖。
测试
1. cargo build 和 cargo check 的根本区别是什么?为什么 check 可以快好几倍?
2. 二进制应用程序是否应该将 Cargo.lock 提交到仓库?库呢?为什么规则不同?
3. 这个 main 能编译吗?
fn main() -> i32 {
0
}
4. 程序会输出什么?
fn main() {
let x = { let x = 1; let x = x + 1; x * 10 };
println!("{}", x);
}
5. 为什么 println! 是宏而不是函数?
6. stable、beta 和 nightly toolchain 有什么区别?各自用于什么场景?
7. 程序会打印什么?为什么?
fn main() {
let x = 7;
println!("{x:>5}|{x:0>5}|{x:<5}|{x:^5}");
}
8. 对于 CPU 密集型程序,cargo run 和 cargo run --release 有什么区别?
答案
cargo check只经过编译器前端:解析、类型检查、borrow checker。LLVM 代码生成和链接 ——build中最重的步骤 —— 被跳过。因此在开发循环中当你需要快速知道代码是否类型合法时,check特别有用。- 对于二进制程序 —— 是的,提交它:
Cargo.lock锁定依赖版本,确保生产构建的可复现性。对于库 —— 通常不提交:库的使用者通过自己的Cargo.lock解析版本,否则依赖树中会产生冲突。 - 无法按常规方式编译。
main必须返回实现Terminationtrait 的类型。()、Result<(), E>(其中E: Debug)、std::process::ExitCode和Infallible直接实现了该 trait。i32没有。 - 输出
20。内部块是表达式:首先x = 1,然后遮蔽x = 2,最后一行x * 10没有;成为块的返回值。 - 宏在编译时检查格式字符串(如果
x没有Display,println!("{}", x)会编译失败),接受不同类型的可变数量参数,并展开为代码。Rust 的普通函数不支持可变参数 —— 所以println!实现为宏。