第二章:工欲善其事——环境搭建与第一个程序
本章导读:每一门语言的学习都始于那个神奇的"Hello, World!"时刻。但在此之前,我们需要先搭建一个称手的工作环境。本章将手把手带你安装 Rust 工具链、配置开发环境、理解 Cargo 项目结构,并最终运行你的第一个 Rust 程序。准备好了吗?让我们开始吧。
🛠️ 2.1 Rust工具链的安装
Rust 的官方安装工具叫做 rustup,它不仅仅是一个安装程序,更是一个完整的 Rust 版本管理器。通过 rustup,你可以轻松地在不同版本的 Rust 之间切换,安装额外的编译目标,以及保持工具链的最新状态。
🪟 2.1.1 Windows 系统安装
在 Windows 上安装 Rust 需要两个组件:Rust 工具链本身,以及一个 C 语言编译器(用于链接系统库)。
首先,安装 Visual Studio Build Tools。访问 Microsoft 官网下载 "Build Tools for Visual Studio",在安装器中选择"使用 C++ 的桌面开发"工作负载。这会安装 MSVC 编译器和 Windows SDK,是 Rust 在 Windows 上的默认链接器。
为什么需要 C 编译器? Rust 编译器生成的是目标代码,而不是可以直接执行的程序。最终的链接步骤需要系统提供的链接器,而 Windows 上的标准链接器是 MSVC 的一部分。如果你不想安装 Visual Studio,也可以使用 GNU 工具链(通过 MSYS2),但 MSVC 方式与 Windows 生态的兼容性更好。
接下来,访问 rustup.rs,下载并运行 rustup-init.exe。安装器会询问你想要哪种安装方式——对于新手,直接按回车选择默认选项即可。
🐧 2.1.2 Linux 和 macOS 系统安装
在 Unix 系统上,安装过程更加简洁。打开终端,运行以下命令:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
这个命令从官方服务器下载安装脚本并执行。脚本会检测你的系统环境,下载合适的预编译二进制文件,并配置好环境变量。
安装完成后,你需要重新加载 shell 配置:
source $HOME/.cargo/env
✅ 2.1.3 验证安装
无论你使用哪个操作系统,安装完成后都可以通过以下命令验证:
rustc --version # 显示编译器版本
cargo --version # 显示包管理器版本
rustup --version # 显示工具链管理器版本
如果这三个命令都能正常输出版本号,恭喜你——Rust 已经成功安装!
📦 2.2 Cargo:你的得力助手
Cargo 是 Rust 生态系统的核心。它同时扮演着多个角色:构建系统、包管理器、测试运行器、文档生成器……可以说,Cargo 是 Rust 开发体验之所以优秀的关键原因之一。
🎯 2.2.1 为什么需要 Cargo?
在没有 Cargo 之前,使用 C/C++ 库是一件令人头疼的事情。你需要手动下载源码、配置编译选项、解决依赖冲突、管理不同平台的差异。Makefile 和 CMake 虽然强大,但学习曲线陡峭,而且每个项目都有自己的"惯例"。
Cargo 的设计哲学是"约定优于配置"。它定义了一套标准的项目结构和构建流程,让你可以专注于代码本身,而不是构建配置。
🆕 2.2.2 创建新项目
让我们创建你的第一个 Rust 项目。打开终端,导航到你希望存放代码的目录,运行:
cargo new hello_rust
这个命令会创建一个名为 hello_rust 的目录,里面包含以下结构:
hello_rust/
├── Cargo.toml # 项目配置文件
└── src/
└── main.rs # 源代码入口
让我们看看 Cargo 自动生成的 Cargo.toml 文件:
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
这个文件被称为清单(manifest),它描述了项目的元数据和依赖。edition 字段指定了 Rust 的版本——2021 Edition 是目前的主流选择,2024 Edition 则带来了更多现代特性。
Edition vs Version:Rust 的"版本"有两个概念。编译器版本(如 1.75.0)每六周发布一次,包含新功能和 bug 修复。Edition(如 2018、2021、2024)每两三年发布一次,可能包含不兼容的语言变化。通过在 Cargo.toml 中指定 Edition,你可以选择何时采用这些变化,而不必一次性升级所有代码。
🔨 2.2.3 构建和运行
进入项目目录,运行以下命令:
cd hello_rust
cargo run
你会看到类似这样的输出:
Compiling hello_rust v0.1.0 (/path/to/hello_rust)
Finished dev [unoptimized + debuginfo] target(s) in 0.5s
Running `target/debug/hello_rust`
Hello, world!
cargo run 做了三件事:编译代码、生成可执行文件、运行程序。如果代码没有变化,再次运行会直接执行已有的二进制文件,跳过编译步骤。
如果你想只编译不运行,可以使用:
cargo build # 开发模式编译(包含调试信息)
cargo build --release # 发布模式编译(开启优化)
开发模式编译速度快,但生成的代码未优化。发布模式编译耗时更长,但生成的二进制文件运行更快。对于最终发布给用户的产品,一定要使用 --release 模式。
💻 2.3 第一个Rust程序:逐行解析
让我们打开 src/main.rs,看看 Cargo 为我们生成的代码:
fn main() {
println!("Hello, world!");
}
只有三行代码,但它们包含了 Rust 语法的几个重要元素。让我们逐行分析。
🔑 2.3.1 fn main():程序入口
fn main() {
fn 是 Rust 中定义函数的关键字。main 是一个特殊的函数名——每个可执行的 Rust 程序都必须有一个 main 函数,它是程序执行的起点。
函数名后面跟着一对括号 (),表示这个函数不接受任何参数。花括号 { 标记函数体的开始。
命名约定:Rust 使用
snakecase</code> 命名函数和变量(如 <code>calculatetotal),使用PascalCase命名类型(如HashMap)。编译器不会强制这些约定,但遵循它们会让你的代码更容易被其他 Rust 开发者理解。
🖨️ 2.3.2 println!:宏的初见
println!("Hello, world!");
这一行打印文本到标准输出。注意 println 后面的感叹号 !——它表示这不是一个普通函数,而是一个宏(macro)。
宏 vs 函数:宏是一种编译时代码生成机制。与函数不同,宏可以接受可变数量的参数、进行模式匹配、甚至生成新的语法结构。
println!宏之所以需要是宏,是因为它要支持格式化字符串,而这在 Rust 的类型系统中很难用普通函数实现。
"Hello, world!" 是一个字符串字面量。在 Rust 中,双引号包围的内容是字符串,单引号包围的内容是单个字符。
行末的分号 ; 标记一个语句的结束。在 Rust 中,大多数语句都以分号结尾,这与 C 和 JavaScript 类似。
🧪 2.3.3 动手修改
让我们把程序变得更复杂一点。修改 main.rs:
fn main() {
let name = "Rustacean"; // 声明一个变量
println!("Hello, {}!", name); // 使用占位符格式化
}
运行 cargo run,你会看到输出变成了 Hello, Rustacean!。
这里引入了两个新概念:let 关键字用于声明变量,{} 是格式化字符串中的占位符。println! 宏会将 name 的值填入占位符的位置。
🔤 2.4 变量与可变性
Rust 的变量系统有一个与众不同的特点:默认不可变。这听起来像是限制,但它是 Rust 安全哲学的重要组成部分。
🔒 2.4.1 不可变变量
fn main() {
let x = 5;
x = 6; // 错误!不能对不可变变量重新赋值
}
尝试编译这段代码,你会得到一个错误:
error[E0384]: cannot assign twice to immutable variable `x`
这个设计背后的理念是:大多数变量实际上不需要改变。默认不可变可以帮助你避免意外修改,也让编译器能够进行更激进的优化。
🔓 2.4.2 可变变量
如果你确实需要改变一个变量,可以使用 mut 关键字:
fn main() {
let mut x = 5;
println!("x 的初始值是: {}", x);
x = 6;
println!("x 的新值是: {}", x);
}
这段代码可以正常编译和运行。mut 关键字就像是你的一个声明:"我打算改变这个变量,请大家注意。"
费曼技巧提问:为什么要默认不可变?想象你在读一本书。如果书的内容在你阅读时不断变化,你将很难理解它。同样,当代码阅读者看到一个没有
mut的变量时,他们可以确信这个变量的值在声明后不会改变——这大大降低了认知负担。
🎭 2.4.3 变量遮蔽(Shadowing)
Rust 允许你声明一个与已有变量同名的新变量。这叫做遮蔽(shadowing):
fn main() {
let x = 5;
let x = x + 1; // 第一个 x 被遮蔽
let x = x * 2; // 第二个 x 被遮蔽
println!("x 的最终值是: {}", x); // 输出: 12
}
遮蔽与 mut 不同:每次 let 都创建了一个新的变量,而不是修改原有变量。这意味着你甚至可以改变变量的类型:
fn main() {
let spaces = " "; // spaces 是字符串类型
let spaces = spaces.len(); // spaces 现在是数字类型
println!("空格数量: {}", spaces);
}
如果用 mut,这是做不到的——你不能把字符串变成数字。遮蔽提供了一种安全的方式来"复用"变量名。
📐 2.5 数据类型:Rust的类型系统
Rust 是一门静态类型语言:每个值在编译时都有确定的类型,编译器会检查类型是否匹配。这可以在编译阶段捕获大量错误。
🔢 2.5.1 标量类型
Rust 有四种基本的标量类型:整数、浮点数、布尔值和字符。
整数类型有多种大小可选:
| 类型 | 含义 | 范围 |
|---|---|---|
i8 | 8位有符号整数 | -128 到 127 |
u8 | 8位无符号整数 | 0 到 255 |
i32 | 32位有符号整数 | -2^31 到 2^31-1 |
u64 | 64位无符号整数 | 0 到 2^64-1 |
isize | 指针大小有符号整数 | 取决于平台 |
usize | 指针大小无符号整数 | 取决于平台 |
技术术语:
isize和usize的大小取决于目标平台的指针大小——在 64 位系统上是 64 位,在 32 位系统上是 32 位。它们主要用于索引集合和内存地址计算。
浮点数有两种:f32(单精度)和 f64(双精度)。Rust 默认使用 f64,因为现代 CPU 上它的速度与 f32 几乎相同,但精度更高。
let x = 2.0; // f64(默认)
let y: f32 = 3.0; // f32(显式指定类型)
布尔值只有两个值:true 和 false,类型是 bool。
字符类型 char 代表一个 Unicode 标量值,用单引号包围:
let c = 'z';
let emoji = '😊';
let chinese = '中';
注意 Rust 的 char 是 4 字节,可以表示任何 Unicode 字符,而不仅仅是 ASCII。
📦 2.5.2 复合类型
元组(Tuple) 可以将多个不同类型的值组合成一个复合值:
let person: (&str, i32, bool) = ("Alice", 30, true);
println!("姓名: {}, 年龄: {}", person.0, person.1);
元组的元素通过索引访问(.0、.1、.2……),索引从零开始。
数组(Array) 存储固定数量的同类型元素:
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
let first = numbers[0]; // 访问第一个元素
let months = ["一月", "二月", "三月"];
数组的大小是类型的一部分——[i32; 3] 和 [i32; 4] 是不同的类型。如果你需要一个可以动态增长的大小,应该使用向量(Vector),我们将在后面的章节介绍。
⚙️ 2.6 函数:代码的组织单元
函数是 Rust 程序的基本构建块。让我们深入了解函数的定义和使用。
📝 2.6.1 函数定义
fn greet(name: &str) {
println!("你好, {}!", name);
}
fn add(a: i32, b: i32) -> i32 {
a + b // 返回值(没有分号)
}
函数参数必须声明类型,这与变量不同(变量通常可以由编译器推断)。-> 后面是返回值类型。
注意 add 函数的最后一行 a + b 没有分号——在 Rust 中,块的最后一个表达式的值就是块的返回值。如果你加上分号,它就变成了语句,函数会返回 ()(空元组),这会导致类型不匹配的错误。
表达式 vs 语句:这是一个重要的区分。表达式产生值,语句执行操作但不产生值。
5是表达式,x + 1是表达式,代码块{ let y = 1; y + 1 }也是表达式(值为 2)。let x = 5;是语句,它没有值。
🔄 2.6.2 控制流
Rust 的控制流结构与其他语言类似,但有一些独特之处。
if 表达式:
let number = 6;
if number % 4 == 0 {
println!("可以被 4 整除");
} else if number % 3 == 0 {
println!("可以被 3 整除");
} else {
println!("既不能被 4 整除,也不能被 3 整除");
}
注意:Rust 的 if 是表达式,可以有返回值:
let condition = true;
let number = if condition { 5 } else { 6 };
循环有三种形式:loop、while 和 for。
// loop:无限循环,需要手动 break
let mut count = 0;
let result = loop {
count += 1;
if count == 10 {
break count * 2; // break 可以携带返回值
}
};
// while:条件循环
let mut number = 3;
while number != 0 {
println!("倒计时: {}!", number);
number -= 1;
}
// for:遍历集合
let fruits = ["苹果", "香蕉", "橙子"];
for fruit in fruits {
println!("我喜欢吃{}", fruit);
}
// for:范围遍历
for i in 1..=5 {
println!("数字: {}", i); // 输出 1, 2, 3, 4, 5
}
第一性原理:为什么 Rust 有
loop关键字?它看起来就是while true的别名。但loop表达了更强的语义:"这个循环在正确的地方 break,而不是因为某个条件变为 false 而退出"。它让代码的意图更加清晰,也帮助编译器进行更好的分析(例如,检测是否所有路径都有返回值)。
🎨 2.7 开发环境配置
一个好的开发环境可以大大提升效率。让我们配置一个现代化的 Rust 开发环境。
📝 2.7.1 推荐编辑器
VS Code + rust-analyzer 是目前最流行的 Rust 开发环境组合。
- 安装 VS Code
- 安装 "rust-analyzer" 扩展(注意:不是 "Rust" 扩展,那个已经过时了)
- 可选:安装 "CodeLLDB" 扩展用于调试
rust-analyzer 提供了智能补全、类型提示、错误诊断、重构工具等功能,是开发 Rust 不可或缺的利器。
🔧 2.7.2 有用的 Cargo 命令
| 命令 | 功能 |
|---|---|
cargo check | 快速检查代码是否有错误(比 build 更快) |
cargo clippy | 运行 linter,捕获潜在问题 |
cargo fmt | 自动格式化代码 |
cargo doc --open | 生成并打开文档 |
cargo test | 运行测试 |
cargo add <crate> | 添加依赖(需要 Rust 1.62+) |
工作流建议:在开发过程中,频繁使用
cargo check而不是cargo build。check只进行类型检查,不生成二进制文件,速度更快。等你准备运行程序时,再用cargo run。
📝 本章小结
本章,我们从零开始搭建了 Rust 开发环境,认识了 Cargo 这个强大的工具,并写下了第一行 Rust 代码。我们学习了变量与可变性、基本数据类型、函数和控制流。
关键要点:
rustup管理 Rust 工具链,cargo管理项目和依赖- 变量默认不可变,使用
mut声明可变变量 - Rust 是静态类型语言,编译器在编译时检查类型
- 函数参数需要显式类型声明,返回值可以是块的最后一个表达式
if是表达式,loop可以返回值
在下一章,我们将深入 Rust 最核心、最独特的概念:所有权、借用和生命周期。这些概念是理解 Rust 的关键,也是 Rust 区别于其他语言的根基。
动手实验:
- 使用
cargo new创建一个新项目,编写一个程序,打印 1 到 100 之间所有能被 3 整除的数。- 编写一个函数
fibonacci(n: u32) -> u64,返回第 n 个斐波那契数。- 探索
cargo clippy和cargo fmt,尝试让它们检查和格式化你的代码。