第 1 课. Rust 简介、安装与第一个程序

本课是基础。后续所有内容都建立在运行 cargo 的习惯、理解 build 和 check 的区别、以及了解工具链包含哪些组件的基础上。如果你对搭建环境和运行第一个程序有信心,后续会越来越轻松。

理论

什么是 Rust 以及为什么选择它

Rust 是一门系统级编程语言,设计目标主要有三个:

  1. 无垃圾回收器的内存安全。没有 null 指针、悬垂引用、数据竞争、双重释放、释放后使用 —— 全部在编译时捕获。
  2. C/C++ 级别的性能。无运行时,零成本抽象:泛型、迭代器、闭包编译为最高效的代码。
  3. 人体工程学与工具链。现代包管理器、格式化工具、linter、文档生成、测试和 LSP 服务器 —— 全部"开箱即用"。

在工业界,Rust 用于操作系统(Linux 的一部分、Redox、Fuchsia)、浏览器引擎(Servo、Firefox 的一部分)、网络服务(Cloudflare、Discord、Dropbox)、嵌入式和 IoT(微控制器、RTIC)、区块链(Solana、Polkadot)、游戏开发(Bevy)、CLI 工具(ripgrepfdbatexa)以及 ML 基础设施(Hugging Face Candle、重度 PyTorch kernel)。

安全性通过 所有权(ownership)借用(borrowing) 和 生命周期(lifetimes) 系统实现,由编译器检查。代价是更陡峭的学习曲线 —— 编译器一开始会和你"争论",但它会在生产环境中帮你避免一整类 bug。

通过 rustup 安装工具链

官方安装程序叫做 rustup。它管理:

  • Toolchains —— stablebetanightly。默认使用 stablenightly 启用实验性功能。
  • 交叉编译目标 —— 例如 wasm32-unknown-unknownaarch64-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

新手常见陷阱

  1. **不要混淆 println! 和 **print! —— 前者会添加换行符。
  2. println!("{}", x)** 需要 ****Display**,而 println!("{:?}", x) 需要 Debug。大多数自定义类型没有前者,后者可以通过 #[derive(Debug)] 自动添加。
  3. {name}** 只对作用域内的变量有效**。如果要替换表达式,请使用 let tmp = ...; println!("{tmp}"); 或 println!("{}", expr)
  4. 关闭所有括号。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. stablebeta 和 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 有什么区别?


答案

  1. cargo check 只经过编译器前端:解析、类型检查、borrow checker。LLVM 代码生成和链接 —— build 中最重的步骤 —— 被跳过。因此在开发循环中当你需要快速知道代码是否类型合法时,check 特别有用。
  2. 对于二进制程序 —— 是的,提交它:Cargo.lock 锁定依赖版本,确保生产构建的可复现性。对于库 —— 通常不提交:库的使用者通过自己的 Cargo.lock 解析版本,否则依赖树中会产生冲突。
  3. 无法按常规方式编译。main 必须返回实现 Termination trait 的类型。()Result<(), E>(其中 E: Debug)、std::process::ExitCode 和 Infallible 直接实现了该 trait。i32 没有。
  4. 输出 20。内部块是表达式:首先 x = 1,然后遮蔽 x = 2,最后一行 x * 10 没有 ; 成为块的返回值。
  5. 宏在编译时检查格式字符串(如果 x 没有 Displayprintln!("{}", x) 会编译失败),接受不同类型的可变数量参数,并展开为代码。Rust 的普通函数不支持可变参数 —— 所以 println! 实现为宏。