第一章 入门 (Getting Started)
一、导读
1.1 本章学习目标
理解 Rust 语言的设计哲学与核心特性,包括安全性、并发性和高性能
掌握 Rust 开发环境的搭建流程,包括 rustup、cargo 和 rustc 的安装与配置
能够使用 cargo 创建、构建和运行 Rust 项目,理解项目生命周期
熟悉 Rust 的基本语法元素:变量绑定、数据类型、函数定义与控制流
理解 Rust 中"所有权"概念的初步含义,以及它与内存安全的关系
掌握使用 cargo doc 生成和查阅项目文档的方法
了解 Rust 的包管理系统 crates.io 的基本使用方式
能够编写并运行简单的 Rust 程序,理解编译错误信息的阅读方法
1.2 为什么本章如此重要
本章是整个 Rust 学习旅程的起点。Rust 作为一门系统级编程语言,自 2015 年发布 1.0 版本以来,凭借其独特的内存安全保证和零成本抽象理念,迅速在系统编程领域占据了重要地位。连续多年在 Stack Overflow 开发者调查中被评为"最受喜爱的编程语言",Rust 的设计哲学深刻影响了现代编程语言的发展方向。
对于初学者而言,Rust 的学习曲线相对陡峭。与 Python、JavaScript 等高级语言不同,Rust 在编译阶段就会强制执行严格的内存安全规则。这意味着开发者必须在编写第一行代码之前,就理解编译器为什么"拒绝"看似合理的代码。本章将帮助你建立正确的心理模型,理解 Rust 编译器不是你的敌人,而是你最严格的代码审查者。
从工程实践角度来看,Rust 的工具链生态是其核心竞争力之一。cargo 不仅是构建工具,更是包管理器、测试运行器、文档生成器和项目管理器的统一体。掌握 cargo 的工作方式,将极大提升你的开发效率。同时,rustup 作为 Rust 的版本管理工具,使得在不同 toolchain 之间切换变得轻而易举,这在团队协作和 CI/CD 场景中尤为重要。
本章还将带你完成第一个 Rust 程序的编写与运行。通过经典的"Hello, World!"示例,你将亲身体验 Rust 的编译流程、错误报告机制和基本的 I/O 操作。这些基础技能将在后续所有章节中反复使用,因此务必在本章打下扎实的基础。
1.3 核心问题引导
Rust 与其他系统级编程语言(如 C/C++)相比,在内存安全方面有哪些根本性的不同?
cargo new 命令创建的项目结构包含哪些文件?每个文件的作用是什么?
Rust 中的变量默认是不可变的(immutable),这一设计决策背后的考量是什么?
什么是"零成本抽象"(zero-cost abstraction)?Rust 如何实现这一目标?
Rust 的编译器错误信息为什么被开发者认为"友好"?它提供了哪些帮助?
在 Rust 中,表达式(expression)和语句(statement)有什么区别?
如何理解 Rust 中"一切皆表达式"的设计理念?
二、核心概念详解
2.1 Rust 的设计哲学与核心特性
Rust 的设计目标可以概括为三个核心支柱:安全性(Safety)、并发性(Concurrency)和性能(Performance)。这三者并非简单的功能列表,而是通过一套精心设计的类型系统和所有权模型有机地统一在一起。
内存安全无需垃圾回收:Rust 通过所有权系统(Ownership System)在编译时保证内存安全,无需运行时垃圾回收(GC)的开销。这意味着 Rust 程序既拥有 C/C++ 级别的运行效率,又避免了悬垂指针(dangling pointer)、数据竞争(data race)和内存泄漏(memory leak)等常见错误。编译器会在编译阶段检查每一处内存访问的合法性,任何违反所有权规则的代码都无法通过编译。
零成本抽象:Rust 的核心理念之一是"你不需要为你不使用的东西付出代价,而你使用的东西也不可能做得更好"。这意味着高级抽象(如迭代器、闭包、泛型)在编译后会被优化为与手写底层代码同样高效的机器码。例如,Rust 的迭代器链在编译后通常会被优化为与 C 语言手写循环相同的汇编代码,甚至更优。
无畏并发(Fearless Concurrency):Rust 的类型系统在编译时就能捕获数据竞争。当多个线程尝试同时访问同一数据时,所有权和借用检查器(Borrow Checker)会确保这种访问是安全的。这使得开发者可以在不担心并发 Bug 的情况下充分利用多核处理器的性能。
2.2 开发环境搭建与工具链
Rust 的开发环境搭建通过 rustup 工具一站式完成。rustup 是 Rust 的官方版本管理工具,负责安装、更新和管理不同版本的 Rust 编译器及工具链。
安装 rustup:在类 Unix 系统上,通过以下命令即可完成安装:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh安装完成后,系统会获得三个核心工具:rustc(编译器)、cargo(包管理器和构建工具)以及 rustup(版本管理器)。
工具链(Toolchain)概念:Rust 的工具链分为三个发布通道——stable(稳定版)、beta(测试版)和 nightly(每夜构建版)。稳定版每六周更新一次,包含经过充分测试的新特性。nightly 版本包含实验性特性,某些第三方库可能依赖 nightly 特性。通过 rustup toolchain install nightly 可以安装 nightly 版本,通过 rustup override set nightly 可以为特定项目指定工具链版本。
cargo 的工作方式:cargo 是 Rust 生态系统中不可或缺的工具。它管理项目的依赖关系、编译流程、测试执行和文档生成。一个典型的 cargo 项目包含 Cargo.toml(项目元数据和依赖声明)和 src/ 目录(源代码)。执行 cargo build 编译项目,cargo run 编译并运行,cargo check 仅进行类型检查而不生成二进制文件(速度更快,适合开发过程中频繁使用)。
2.3 变量绑定与可变性
Rust 中的变量通过 let 关键字进行绑定(binding),这与许多语言中的"变量声明"有本质区别。Rust 的变量默认是不可变的(immutable),这是 Rust 安全性保证的重要组成部分。
let x = 5;
// x = 6; // 编译错误:不能对不可变变量赋值不可变性的意义:默认不可变的设计鼓励开发者编写更可预测的代码。当一个值被声明为不可变时,你可以确信它在整个作用域内不会改变,这大大降低了推理代码行为的认知负担。在并发场景中,不可变数据天然就是线程安全的。
可变变量:如果确实需要修改一个值,必须显式使用 mut 关键字:
let mut x = 5;
x = 6; // 合法mut 的存在是一种"意图声明"——它告诉阅读代码的人(以及编译器),这个值会被修改。这种显式声明使得代码审查时可以快速定位可能发生变化的状态。
变量遮蔽(Shadowing):Rust 允许用相同的名字声明新变量,新变量会"遮蔽"前一个同名变量。遮蔽与 mut 的区别在于,遮蔽可以改变变量的类型:
let x = 5; // i32
let x = x + 1; // 新的 x,仍为 i32
let x = "hello"; // 再次遮蔽,类型变为 &str2.4 基本数据类型与控制流
Rust 是强类型静态类型语言,每个值在编译时都有确定的类型。Rust 的类型推断系统非常强大,大多数情况下无需显式标注类型。
标量类型包括:整数(i8、i16、i32、i64、i128、u8、u16、u32、u64、u128、isize、usize)、浮点数(f32、f64)、布尔值(bool)和字符(char,占用 4 字节,表示 Unicode 标量值)。
let age: u32 = 30;
let price: f64 = 19.99;
let is_active: bool = true;
let letter: char = 'A';复合类型包括元组(tuple)和数组(array)。元组可以包含不同类型的元素,数组则要求所有元素类型相同且长度固定:
let point: (f64, f64) = (3.0, 4.0);
let numbers: [i32; 5] = [1, 2, 3, 4, 5];控制流方面,Rust 使用 if 表达式(注意是表达式而非语句)、loop(无限循环)、while 循环和 for 循环。if 作为表达式可以返回值,这使得它类似于其他语言中的三元运算符:
let number = if condition { 5 } else { 6 };for 循环是最常用的循环结构,通常与迭代器配合使用:
for i in 0..10 {
println!("{}", i);
}2.5 函数与代码组织
Rust 使用 fn 关键字定义函数。函数名遵循 snake_case 命名约定。Rust 中的函数可以返回值,使用 -> 指定返回类型。函数体中最后一个表达式如果不跟分号,就会作为返回值:
fn add(a: i32, b: i32) -> i32 {
a + b
}返回值机制:Rust 区分"返回值"和"返回语句"。return 关键字用于提前返回,而函数末尾的表达式(不带分号)是惯用的返回方式。这种设计使得代码更加简洁,避免了不必要的 return 关键字。
代码组织:Rust 使用模块(module)系统组织代码。mod 关键字定义模块,use 关键字引入模块中的项,pub 关键字控制可见性。一个典型的 Rust 项目通过 lib.rs 和 main.rs 分别定义库和二进制入口,通过模块树(module tree)组织代码结构。
mod utils {
pub fn helper() {
// 公开函数
}
fn internal() {
// 私有函数
}
}
use utils::helper;三、深入分析与讨论
3.1 Rust 编译器作为学习工具
Rust 编译器(rustc)以其详尽且富有教育意义的错误信息著称。与 C/C++ 编译器晦涩的模板错误信息不同,Rust 编译器会明确指出错误位置、原因,甚至提供修复建议。这种设计并非偶然——Rust 团队投入了大量精力优化错误信息的可读性。
编译器的错误信息通常包含三个部分:错误代码(如 E0384)、错误描述和修复建议。错误代码可以在 Rust 官方文档中查到详细说明。更重要的是,编译器会告诉你"为什么"这是错误的,而不仅仅是"什么"地方错了。例如,当你尝试修改一个不可变变量时,编译器会提示"考虑使用 mut 使其可变"。
这种"编译器即导师"的模式深刻改变了 Rust 的开发体验。在开发过程中,cargo check 命令可以在数秒内完成类型检查,让你快速获得编译器的反馈。许多 Rust 开发者养成了"写一小段代码,立即 cargo check"的习惯,将编译器作为实时的学习伙伴。
3.2 表达式与语句的哲学区分
Rust 严格区分表达式(expression)和语句(statement),这是理解 Rust 代码风格的关键。表达式会产生一个值,语句则不会。大多数代码行都是表达式或语句。
let x = 5; 是一条语句——它执行了一个绑定动作,但不产生有意义的值。而 5、{ 1 + 2 }、if true { 1 } else { 2 } 都是表达式——它们都会求值为一个值。这种区分使得 Rust 的代码更加简洁和一致。例如,在 JavaScript 中需要三元运算符 condition ? a : b,而在 Rust 中直接使用 if 表达式即可。
这种设计还影响了 Rust 的函数返回机制。函数体的最后一个表达式自动成为返回值,不需要 return 关键字。这使得简单的包装函数可以写得非常紧凑:
fn double(x: i32) -> i32 { x * 2 }3.3 Rust 与其他系统语言的生态对比
Rust 的生态系统虽然在规模上不及 C/C++,但其质量备受推崇。crates.io(Rust 的包注册中心)上的包(称为 crate)遵循统一的构建和发布规范,这得益于 cargo 的标准化管理。与 C/C++ 世界中多种构建系统(Make、CMake、Bazel 等)并存的混乱局面不同,Rust 生态中 cargo 是唯一的构建工具,这极大降低了新项目的上手成本。
在性能方面,Rust 与 C/C++ 处于同一水平线,某些场景下甚至更优。这是因为 Rust 的所有权系统消除了运行时的内存管理开销,同时编译器能够进行更激进的优化。例如,Rust 的迭代器在编译后通常会被优化为与手写循环相同的机器码,而 C++ 的 STL 迭代器在某些情况下会引入额外的间接调用开销。
四、实践案例
4.1 温度转换器
编写一个程序,实现摄氏度与华氏度之间的相互转换。这个练习涵盖了变量绑定、函数定义、用户输入和格式化输出等基础概念。
use std::io;
fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
fn fahrenheit_to_celsius(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
fn main() {
println!("温度转换器");
println!("请输入温度值:");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
let temperature: f64 = input.trim().parse().expect("请输入有效的数字");
println!("请输入单位(C/F):");
let mut unit = String::new();
io::stdin().read_line(&mut unit).expect("读取输入失败");
match unit.trim().to_uppercase().as_str() {
"C" => println!("{:.1}°C = {:.1}°F", temperature, celsius_to_fahrenheit(temperature)),
"F" => println!("{:.1}°F = {:.1}°C", temperature, fahrenheit_to_celsius(temperature)),
_ => println!("无效的单位输入"),
}
}4.2 Fibonacci 数列生成器
实现一个生成 Fibonacci 数列的程序,展示 Rust 中循环、条件判断和可变变量的使用。
fn fibonacci(n: u32) -> Vec<u64> {
let mut fib = Vec::new();
if n == 0 {
return fib;
}
fib.push(0);
if n == 1 {
return fib;
}
fib.push(1);
for i in 2..n {
let next = fib[(i - 1) as usize] + fib[(i - 2) as usize];
fib.push(next);
}
fib
}
fn main() {
let n = 20;
let result = fibonacci(n);
println!("Fibonacci 数列(前 {} 项):", n);
for (i, &val) in result.iter().enumerate() {
println!(" F({}) = {}", i, val);
}
}五、常见误区与注意事项
误以为 `mut` 可以改变类型:mut 只是让变量可变,不能改变其类型。如果需要改变类型,应使用变量遮蔽(shadowing)。
混淆 `let` 和 `let mut` 的使用场景:许多从其他语言转过来的开发者习惯性地给所有变量加 mut。应遵循"默认不可变"的原则,只在确实需要修改时才使用 mut。
忽略编译器警告:Rust 编译器不仅会报错,还会产生警告(warning)。许多警告预示着潜在的 Bug(如未使用的变量),应认真对待每一个警告,必要时使用 _ 前缀显式忽略。
混淆 `==` 和 `=`:Rust 中 = 是赋值运算符,== 是比较运算符。在 if 条件中必须使用 ==,这与 C 语言相同,但与某些函数式语言不同。
忘记处理 `Result` 类型:Rust 的 I/O 操作返回 Result 类型,必须显式处理。使用 .expect() 或 ? 运算符处理错误,不要忽略 Result 值——编译器会发出警告。
六、本章小结
本章介绍了 Rust 语言的基础知识和开发环境搭建。我们学习了 Rust 的设计哲学——安全性、并发性和性能的统一,理解了所有权系统的初步概念。通过安装和配置 rustup、cargo 等工具,我们建立了完整的开发环境。在语法层面,我们掌握了变量绑定与可变性、基本数据类型、函数定义和控制流等核心概念。Rust 的编译器不仅是代码检查工具,更是学习过程中的导师,其详尽的错误信息帮助开发者快速定位和修复问题。表达式与语句的区分是 Rust 代码风格的基础,理解这一点有助于编写更加地道的 Rust 代码。
七、思考题与练习
解释为什么 Rust 默认将变量设为不可变。这种设计在实际开发中有哪些好处?请举出至少两个具体场景。
编写一个程序,使用 for 循环和范围表达式计算 1 到 100 的和,然后改写为使用 while 循环的版本。比较两种写法的优劣。
尝试在不使用 mut 的情况下实现一个计数器(提示:考虑递归或函数调用链)。这种写法与使用 mut 相比有什么特点?
研究 cargo check、cargo build 和 cargo run 的区别。在什么场景下应该使用 cargo check?
编写一个程序,接收用户输入的字符串,统计其中元音字母和辅音字母的数量。注意处理用户输入的空白字符和大小写转换。
解释变量遮蔽(shadowing)与 mut 的区别。在什么场景下应该选择遮蔽而不是可变变量?
尝试创建一个包含多个模块的 Rust 项目,理解 mod、pub 和 use 关键字的作用。设计一个简单的模块层次结构。
阅读 Rust 编译器的一个错误信息(如 E0502),解释编译器是如何帮助你理解问题的。