零基础入门 Rust 编程语言:迈出安全、高性能编程的第一步
欢迎来到 Rust 的世界!如果你对编程充满好奇,或者已经接触过其他语言但被 Rust 的独特魅力(安全、高性能、并发无畏)所吸引,那么这篇为你量身打造的零基础入门指南将是你的完美起点。
不用担心你没有任何编程经验,我们将从最基本的概念讲起,一步一步带你领略 Rust 的风采。Rust 可能有些概念(比如所有权)初听起来会觉得陌生,但这正是它强大和独特之处。请保持耐心和好奇心,你将打开一扇通往现代系统编程的大门。
本文将带你了解:
- Rust 是什么,为什么选择 Rust?
- 如何安装 Rust 开发环境。
- 你的第一个 Rust 程序:Hello, World!
- 认识 Rust 的构建工具:Cargo。
- Rust 的基本概念:变量、数据类型、函数。
- 控制流程:条件判断与循环。
- Rust 的核心:所有权、借用与生命周期(入门级理解)。
- 结构体与枚举:组织数据的方式。
- 错误处理:如何优雅地处理可能出错的情况。
- 接下来的学习路径。
准备好了吗?让我们开始这段令人兴奋的旅程!
第一章:Rust 是什么,为什么选择 Rust?
在我们动手写代码之前,先花点时间了解一下 Rust 的背景和它解决的问题。
Rust 是什么?
Rust 是一种现代的系统编程语言,由 Mozilla 研究院开发,现在由 Rust 基金会管理。它设计的目标是实现以下三者的平衡:
- 安全 (Safety): 在编译时检查出通常会在运行时引发问题的错误,特别是内存安全问题(比如空指针引用、数据竞争等)。Rust 保证在没有使用
unsafe
关键字的情况下,你的程序不会出现内存安全错误。 - 性能 (Performance): Rust 拥有媲美 C/C++ 的零成本抽象,它不使用垃圾回收机制,对硬件的控制能力很强,这使得它非常适合编写对性能要求极高的应用,比如操作系统、游戏引擎、数据库、命令行工具、Web 服务器等。
- 并发 (Concurrency): Rust 的所有权系统让编写安全高效的并发代码变得更加容易,它可以在编译时防止数据竞争。
简单来说,你可以把 Rust 看作是一种既拥有 C/C++ 的性能和底层控制能力,又拥有更高级语言(如 Java, Python)的内存安全和开发效率的语言。
为什么选择 Rust?
对于初学者来说,选择 Rust 可能看起来有点挑战,因为它的某些概念确实需要时间去理解。但正是这些“挑战”,为你带来了巨大的回报:
- 学习底层原理: 学习 Rust 会迫使你思考内存是如何管理的,这有助于你更好地理解计算机底层的工作方式。
- 写出健壮的代码: Rust 的严格编译器(通常被称为“借用检查器”)会帮助你提前发现很多潜在的 bug,让你写出更可靠、更少运行时错误的程序。
- 高性能: 如果你未来需要编写对速度要求很高的程序,Rust 是一个绝佳的选择。
- 活跃的社区和生态: Rust 有一个友好且活跃的社区,并且其包管理器 Cargo 和第三方库生态 (crates.io) 非常成熟和易用。
- 日益增长的应用领域: 从 WebAssembly 到命令行工具,从网络服务到嵌入式设备,Rust 的应用范围越来越广。
诚然,入门 Rust 可能不像 Python 那样轻松,但它提供的“安全保障”和“性能潜力”是独一无二的。把它想象成学习一门乐器,最初可能会有些困难,但一旦掌握了基本技巧,你就能演奏出美妙的乐章。
第二章:安装 Rust 开发环境
要开始编写 Rust 代码,首先需要安装 Rust 的工具链。最推荐的方式是使用 rustup
,这是一个管理 Rust 版本和相关工具的命令行工具。
安装步骤:
-
打开终端或命令提示符:
- 在 Windows 上,搜索并打开 “Command Prompt” 或 “PowerShell”。
- 在 macOS 或 Linux 上,打开 “Terminal” 应用。
-
运行安装命令:
- macOS 或 Linux: 在终端中粘贴并运行以下命令:
bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
这个命令会下载并运行一个脚本,脚本会指引你完成安装。通常选择默认安装即可(输入1
或直接按回车)。 - Windows: 访问 https://rustup.rs,下载并运行
rustup-init.exe
安装程序。按照提示进行安装,同样通常选择默认安装即可。
- macOS 或 Linux: 在终端中粘贴并运行以下命令:
-
配置环境变量 (如果需要): 安装程序通常会自动配置环境变量,但如果没有,或者你需要手动配置,请确保 Rust 的
bin
目录(通常是$HOME/.cargo/bin
或%USERPROFILE%\.cargo\bin
)被添加到了系统的 PATH 环境变量中。这允许你在任何地方运行rustc
、cargo
和rustup
命令。安装程序完成时通常会有提示。 -
验证安装: 安装完成后,关闭并重新打开你的终端或命令提示符,然后运行以下命令来验证 Rustc(Rust 编译器)和 Cargo(Rust 的构建工具和包管理器)是否成功安装:
bash
rustc --version
cargo --version
如果它们都显示了版本信息,恭喜你!Rust 开发环境已经准备就绪。
推荐的开发工具:
虽然你可以用任何文本编辑器编写 Rust 代码,但使用支持 Rust 的集成开发环境 (IDE) 或代码编辑器会极大地提升开发效率,提供语法高亮、代码补全、错误检查等功能。一些流行的选择包括:
- VS Code: 安装
Rust-analyzer
插件。这是目前社区最推荐的配置。 - IntelliJ IDEA / CLion: 安装 Rust 插件。
建议你安装 VS Code 并配置 Rust-analyzer 插件,它提供非常出色的代码辅助功能。
第三章:你的第一个 Rust 程序:Hello, World!
安装完成后,是时候写下所有编程语言的第一个经典程序了!
-
创建项目文件夹:
在你的电脑上选择一个合适的位置,创建一个新的文件夹,比如叫做rust_intro
。
bash
mkdir rust_intro
cd rust_intro -
创建源代码文件:
在rust_intro
文件夹内,创建一个名为main.rs
的文件。Rust 源代码文件的扩展名是.rs
。 -
编写代码:
用你喜欢的文本编辑器打开main.rs
文件,输入以下代码:
“`rust
// 这是一个注释,编译器会忽略它fn main() {
// println! 是一个宏,用于打印文本到控制台
println!(“Hello, world!”);
}
“`代码解释:
*//
开头的是注释,用来解释代码,方便自己和他人理解。
*fn main() { ... }
定义了一个名为main
的函数。在 Rust 中,main
函数是程序的入口点,程序运行时会首先执行main
函数里的代码。fn
关键字用来声明一个函数,()
里是函数的参数列表(这里为空),{}
里是函数体。
*println!("Hello, world!");
是函数体里的一条语句。println!
是一个 宏 (macro),而不是一个普通的函数。宏在编译时展开,可以执行一些比普通函数更灵活的操作。println!
的作用是将括号里的字符串打印到标准输出(通常是你的终端),并在末尾添加一个换行符。注意字符串是写在双引号""
里面的。
* 每条语句的末尾都以分号;
结束(除了块表达式的最后一行)。 -
编译程序:
打开终端,进入到rust_intro
文件夹。运行 Rust 编译器rustc
来编译你的源代码文件:
bash
rustc main.rs
如果一切顺利,终端不会有任何输出(或者只显示一些编译信息),并且在rust_intro
文件夹下会生成一个可执行文件。在 Windows 上是main.exe
,在 macOS 或 Linux 上是main
。 -
运行程序:
现在,运行生成的可执行文件:- Windows:
bash
.\main.exe - macOS 或 Linux:
bash
./main
你应该会在终端看到输出:
Hello, world!
- Windows:
恭喜你!你已经成功编写、编译并运行了你的第一个 Rust 程序。这是一个重要的里程碑!
虽然直接使用 rustc
编译单个文件很简单,但在实际开发中,Rust 项目通常会包含多个文件、依赖其他库。这时候,我们更常用 Rust 的构建工具和包管理器 Cargo。
第四章:认识 Rust 的构建工具:Cargo
Cargo 是 Rust 生态系统的核心工具,它帮助你处理很多任务:创建项目、管理依赖、编译代码、运行测试、生成文档等。对于任何非 trivial 的 Rust 项目,强烈推荐使用 Cargo。
使用 Cargo 创建项目:
相比手动创建文件,使用 Cargo 创建项目更方便:
- 打开终端,进入你想要存放项目的目录。
-
运行
cargo new
命令:
bash
cargo new hello_cargo
cd hello_cargo
这个命令会创建一个名为hello_cargo
的新文件夹。进入该文件夹,你会看到 Cargo 生成的项目结构:
hello_cargo/
├── Cargo.toml
└── src/
└── main.rsCargo.toml
: 这是 Cargo 的配置文件,采用 TOML (Tom’s Obvious, Minimal Language) 格式。它包含了项目的元信息(如名称、版本、作者)以及项目依赖的其他库。src/
: 这是存放项目源代码的地方。src/main.rs
: Cargo 默认生成的主程序文件,里面已经包含了经典的Hello, world!
代码。
使用 Cargo 编译和运行项目:
进入到 hello_cargo
文件夹后,你可以使用 Cargo 的命令来编译和运行程序:
-
编译项目:
bash
cargo build
这个命令会读取Cargo.toml
,下载所需的依赖(如果需要),然后编译src
目录下的源代码。编译成功后,会在项目根目录下的target/debug/
目录里生成可执行文件。 -
运行项目:
bash
cargo run
这个命令更常用。它会先检查代码是否需要重新编译,如果需要则进行编译,然后直接运行生成的可执行文件。运行
cargo run
后,你应该会看到输出:
Finished dev [unoptimized + debuginfo] target(s) in X.XXs
Running `target/debug/hello_cargo`
Hello, world!
第一行是 Cargo 的编译信息,下面是程序本身的输出。
Cargo 的其他常用命令:
cargo check
: 快速检查代码是否有编译错误,但不生成可执行文件。比cargo build
更快,适合在编写代码时频繁检查。cargo build --release
: 以优化模式编译项目,生成的可执行文件性能更高,但编译时间会更长。生成的文件在target/release/
目录下。cargo clean
: 清理编译生成的文件(target
目录)。
为什么 Cargo 如此重要?
- 标准化的项目结构: 所有 Rust 项目看起来都类似,易于理解。
- 依赖管理: 在
Cargo.toml
中声明依赖,Cargo 会自动下载、编译和管理这些依赖库,解决了版本冲突等问题。 - 构建流程简化: 一个简单的命令 (
cargo build
或cargo run
) 就能完成复杂的编译和链接过程。
从现在开始,我们几乎所有的例子都会在 Cargo 项目中使用 cargo run
来运行。
第五章:Rust 的基本概念:变量、数据类型、函数
现在,让我们深入了解 Rust 的一些基本构建模块。
变量与可变性 (Variables and Mutability)
在编程中,变量用于存储数据。在 Rust 中,声明变量使用 let
关键字:
“`rust
fn main() {
let x = 5; // 声明一个名为 x 的变量,赋值为 5
println!(“x 的值是: {}”, x);
// 默认情况下,Rust 的变量是不可变的 (immutable)
// x = 6; // 尝试修改 x 的值会导致编译错误!
// 如果需要变量可变,使用 mut 关键字
let mut y = 10;
println!("y 的值是: {}", y);
y = 15; // 现在可以修改 y 的值了
println!("y 的新值是: {}", y);
}
“`
重要概念:不可变性 (Immutability)
Rust 默认变量是不可变的。这看起来可能有些限制,但它是 Rust 安全性设计的关键之一。不可变性使得代码更易于理解和推理,因为你不需要担心变量的值在某个地方被意外修改。当你确实需要改变变量的值时,明确地使用 mut
关键字来表明你的意图。
数据类型 (Data Types)
Rust 是一种静态类型语言,这意味着在编译时就需要确定变量的数据类型。Rust 通常可以根据值推断出变量的类型(称为类型推断),但你也可以显式地指定类型。
基本(标量)类型:
-
整型 (Integers):
- 有符号整型 (
i8
,i16
,i32
,i64
,i128
,isize
):i
表示 signed (有符号),数字表示位数。isize
的大小取决于你的计算机架构(32位或64位)。 - 无符号整型 (
u8
,u16
,u32
,u64
,u128
,usize
):u
表示 unsigned (无符号)。usize
的作用类似于isize
。 - 例如:
let age: u32 = 30;
- 字面量:
123
(i32),98_222
(i32),0xff
(u8),0o77
(u8),0b1111_0000
(u8),b'A'
(u8 – ASCII 字符)
- 有符号整型 (
-
浮点型 (Floating-Point Numbers):
f32
(单精度)f64
(双精度,默认类型)- 例如:
let pi: f64 = 3.14159;
-
布尔型 (Booleans):
bool
:只有两个可能的值true
和false
。- 例如:
let is_active: bool = true;
-
字符型 (Characters):
char
:表示一个 Unicode 标量值,占用 4 个字节。用单引号'
包围。- 例如:
let initial: char = 'R';
let emoji: char = '😊';
复合类型:
-
元组 (Tuples):
- 将多个不同类型的值组合到一个复合类型中。元组的长度是固定的。
- 可以使用模式匹配或索引来解构或访问元组中的值。
-
例如:
“`rust
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // 解构
println!(“y 的值是: {}”, y); // 输出 6.4let five_hundred = tup.0; // 通过索引访问
let six_point_four = tup.1;
“`
-
数组 (Arrays):
- 将多个相同类型的值组合到一个固定长度的集合中。
- 数组长度是固定的,声明后不能改变。
- 使用方括号
[]
定义。 -
例如:
“`rust
let a = [1, 2, 3, 4, 5]; // 自动推断类型和长度
let months: [&str; 12] = [“Jan”, “Feb”, “Mar”, “Apr”, “May”, “Jun”, “Jul”, “Aug”, “Sep”, “Oct”, “Nov”, “Dec”]; // 显式指定类型和长度let first = a[0]; // 通过索引访问,索引从 0 开始
let second = a[1];// 注意:访问数组越界会导致运行时错误 (panic),Rust 会检查索引是否有效
// let index = 10;
// let element = a[index]; // 这行如果执行,会导致程序崩溃
“`
函数 (Functions)
在 Rust 中,函数使用 fn
关键字定义。main
函数是我们已经见过的特殊函数。
“`rust
fn main() {
println!(“Hello from main!”);
another_function(); // 调用另一个函数
function_with_params(5); // 调用带参数的函数
print_labeled_measurement(5, 'h'); // 调用带多个参数的函数
let x = five(); // 调用有返回值的函数
println!("five() 返回的值是: {}", x);
let y = plus_one(5);
println!("plus_one(5) 返回的值是: {}", y);
}
// 定义一个没有参数和返回值的函数
fn another_function() {
println!(“Hello from another function!”);
}
// 定义带一个参数的函数
fn function_with_params(x: i32) {
println!(“传入的参数值是: {}”, x);
}
// 定义带多个参数的函数
fn print_labeled_measurement(value: i32, unit_label: char) {
println!(“测量值: {}{}”, value, unit_label);
}
// 定义有返回值的函数
// -> 后面跟着的是返回值的类型
fn five() -> i32 {
5 // 表达式,它就是函数的返回值。注意没有分号!
}
// 函数体包含语句和表达式
fn plus_one(x: i32) -> i32 {
x + 1 // 这是一个表达式,它的值是 x + 1。注意没有分号!
// 如果加上分号: x + 1; 它就变成了语句,函数将返回单元类型 ()
}
“`
关于函数返回值:
- 在 Rust 中,函数的返回值是函数体中最后一个 表达式 的值。
- 表达式末尾没有分号。
- 如果函数体以一个语句结束(带分号),或者函数体为空,那么函数将返回单元类型
()
,表示“没有有意义的值”。 - 你也可以使用
return
关键字提前从函数返回一个值,例如return 5;
。但在 Rust 中,隐式返回最后一个表达式的值是更常见的做法。
第六章:控制流程:条件判断与循环
控制流程允许你的程序根据条件执行不同的代码块,或者重复执行某些代码。
if 表达式
if
表达式允许你根据布尔条件执行代码。
“`rust
fn main() {
let number = 7;
if number < 5 {
println!("条件为真");
} else {
println!("条件为假");
}
// 可以有多个 else if 分支
let number = 6;
if number % 4 == 0 {
println!("number 可以被 4 整除");
} else if number % 3 == 0 {
println!("number 可以被 3 整除");
} else if number % 2 == 0 {
println!("number 可以被 2 整除");
} else {
println!("number 不能被 2, 3, 或 4 整除");
}
// 在 let 语句中使用 if
let condition = true;
let number = if condition {
5 // if 块的返回值
} else {
6 // else 块的返回值
// 注意:if 和 else 块的返回类型必须相同!
};
println!("number 的值是: {}", number);
// 以下会导致编译错误,因为类型不匹配
// let number = if condition { 5 } else { "six" };
}
“`
注意: if
条件必须是 bool
类型。不像某些语言,你不能直接使用非布尔值(如数字 0 或非零值)作为条件。
循环 (Loops)
Rust 提供了三种循环:loop
、while
和 for
。
loop
loop
关键字创建一个无限循环。你可以使用 break
关键字退出循环,或者使用 continue
跳过当前迭代的剩余部分,进入下一次迭代。
“`rust
fn main() {
let mut counter = 0;
loop {
println!("loop!");
counter += 1;
if counter == 3 {
break; // 当 counter 等于 3 时退出循环
}
}
// loop 还可以返回值
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // break 后面跟着的值是循环的返回值
}
};
println!("loop 返回的值是: {}", result); // 输出 20
}
“`
while
while
循环根据一个布尔条件执行。只要条件为真,循环就会一直执行。
“`rust
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
}
“`
for
for
循环用于遍历集合(如数组、范围)。这是 Rust 中最常用的循环类型,因为它更安全、更简洁。
“`rust
fn main() {
let a = [10, 20, 30, 40, 50];
// 遍历数组
for element in a.iter() { // .iter() 方法提供一个迭代器
println!("数组元素是: {}", element);
}
// 遍历一个范围
// 1..4 表示从 1 到 3 (不包含 4)
// rev() 表示倒序
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
“`
for
循环结合 .iter()
或范围 (..
, ..=
) 是遍历集合的标准方式,它避免了手动管理索引和边界检查,从而减少了出错的可能性。
第七章:Rust 的核心:所有权、借用与生命周期(入门级理解)
这是 Rust 最独特、也是对初学者来说最具挑战性的部分。但请记住,正是这些概念保证了 Rust 的内存安全,而无需垃圾回收。理解它们是掌握 Rust 的关键。
这里我们只做入门级的解释,旨在让你对这些概念有一个初步的认识。完整的细节需要查阅官方文档或深入学习。
所有权 (Ownership)
Rust 的所有权系统是一组规则,编译器在编译时使用它们来管理内存。它不会产生运行时开销。
所有权规则:
- Rust 中的每个值都有一个变量作为它的所有者 (owner)。
- 同一时刻,一个值只能有一个所有者。
- 当所有者(变量)离开其作用域 (scope) 时,该值将被丢弃 (drop),占用的内存会被自动释放。
“`rust
fn main() { // s 进入作用域
let s = String::from(“hello”); // s 是一个 String 类型的值的所有者
// move 例子
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移动 (move) 给了 s2
// s1 不再有效!尝试使用 s1 会导致编译错误。
// println!("{}, world!", s1); // 编译错误: value borrowed here after move
// copy 例子 (针对基本类型)
let x = 5; // x 是一个 i32 类型的值的所有者
let y = x; // i32 是 Copy trait 的类型,x 的值被复制给 y
// x 仍然有效!
println!("x = {}, y = {}", x, y); // 输出 x = 5, y = 5
// clone 例子 (对于复杂类型需要显式复制)
let s3 = String::from("hello");
let s4 = s3.clone(); // 显式复制 String 数据
// s3 仍然有效!
println!("s3 = {}, s4 = {}", s3, s4); // 输出 s3 = hello, s4 = hello
} // s 离开作用域,内存被释放
// s2 离开作用域,其拥有的 String 内存被释放
// s4 离开作用域,其拥有的 String 内存被释放
“`
解释:
- 对于基本数据类型(如整数、布尔值、定长数组等),它们实现了
Copy
trait。当赋值或作为函数参数传递时,会直接进行值的复制,原始变量仍然有效。 - 对于复杂数据类型(如
String
,动态大小的集合),它们实现了Drop
trait。默认行为是发生 所有权转移 (move)。这避免了“双重释放”的问题:如果两个变量拥有同一块内存并在作用域结束时都尝试释放它,就会出错。通过所有权转移,只有一个变量是真正的所有者,负责释放内存。 - 如果确实需要复制复杂类型的值(而不是转移所有权),可以使用
.clone()
方法。但克隆操作可能会比较耗时,因为它需要复制底层的数据。
借用 (Borrowing)
如果你想在不转移所有权的情况下使用一个值,可以使用引用 (references)。通过引用访问值称为借用 (borrowing)。
引用使用 &
符号创建。默认引用是不可变的。
“`rust
fn main() {
let s1 = String::from(“hello”);
let len = calculate_length(&s1); // 将 s1 的引用传递给函数
println!("'{}' 的长度是 {}", s1, len); // s1 仍然有效,因为我们只是借用了它
// 可变引用
let mut s = String::from("hello");
change(&mut s); // 将 s 的可变引用传递给函数
println!("修改后的 s: {}", s); // 输出 修改后的 s: hello, world!
// 可变引用的重要规则:
let mut s = String::from("hello");
let r1 = &mut s; // 第一个可变引用
// let r2 = &mut s; // 编译错误!不能同时创建第二个可变引用
// 可以创建多个不可变引用
let mut s = String::from("hello");
let r1 = &s; // 第一个不可变引用
let r2 = &s; // 第二个不可变引用
// let r3 = &mut s; // 编译错误!不能在拥有不可变引用时创建可变引用
println!("{}, and {}", r1, r2); // 在不可变引用 r1 和 r2 使用后,它们的生命周期结束
let r3 = &mut s; // 现在可以创建可变引用了
println!("{}", r3);
} // r3 离开作用域
fn calculate_length(s: &String) -> usize { // 接收一个 String 的不可变引用
s.len() // 使用引用来访问 String 的方法
} // s 离开作用域,但因为它不拥有值,所以内存不会被释放
fn change(some_string: &mut String) { // 接收一个 String 的可变引用
some_string.push_str(“, world!”); // 使用可变引用修改 String
}
“`
借用规则(核心):
在任意给定时间,你只能选择以下两种借用方式之一:
- 一个可变的引用 (
&mut T
)。 - 任意数量的不可变引用 (
&T
)。
你不能在持有不可变引用的同时创建可变引用。这些规则是 Rust 的“借用检查器” (borrow checker) 在编译时执行的,它防止了数据竞争等并发问题,以及悬垂指针等内存安全问题。
生命周期 (Lifetimes)
生命周期是 Rust 编译器用来确保所有借用都有效的概念。生命周期参数不是改变引用的生命周期长短,而是声明引用的生命周期之间的关系,从而帮助借用检查器进行分析。
对于初学者,你暂时不需要深入理解生命周期的语法 (<'a>
)。在很多情况下,编译器可以推断出生命周期。你只需要记住:
- 生命周期是关于引用是否有效的问题。
- 借用检查器使用生命周期来确保引用不会活得比它们指向的数据长(防止悬垂引用)。
- 如果你编写函数或结构体,并且它们的引用可能存在歧义的生命周期关系时,编译器会要求你添加生命周期注解来明确这些关系。
“`rust
// 这是一个需要生命周期注解的例子,但初学者可以先跳过语法细节,
// 仅理解其目的:确保返回的引用是有效的。
// fn longest(x: &str, y: &str) -> &str { // 编译错误,缺少生命周期参数
fn longest<‘a>(x: &’a str, y: &’a str) -> &’a str { // 声明返回的引用和输入的引用有相同的生命周期
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from(“abcd”);
let string2 = “xyz”;
// 函数返回的引用 (&str) 的生命周期不能比 string1 和 string2 中较短的那个长
let result = longest(string1.as_str(), string2);
println!("最长的字符串是 {}", result);
let string3 = String::from("long string is long");
{ // 内部作用域
let string4 = String::from("xyz");
let result = longest(string3.as_str(), string4.as_str()); // result 的生命周期只在这个内部作用域内有效
println!("最长的字符串是 {}", result);
} // string4 离开作用域并被丢弃
// result 在这里仍然有效,因为它借用的是 string3 的内容,而 string3 在这里仍然有效
// println!("result: {}", result); // 如果 result 的生命周期与 string4 绑定,这里就会报错
}
“`
总结: 所有权、借用和生命周期是相互关联的概念。所有权决定了谁拥有数据,借用允许你在不拥有数据的情况下访问它,而生命周期确保借用是安全的、不会产生悬垂引用。虽然一开始可能觉得困难,但随着练习的深入,你会越来越熟悉它们,并体会到它们带来的安全性优势。
第八章:结构体与枚举:组织数据的方式
结构体 (Structs)
结构体允许你创建自定义的复杂数据类型,将多个相关联的值打包成一个有意义的结构。
“`rust
// 定义一个结构体
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
// 你也可以定义元组结构体 (Tuple Structs)
struct Color(i32, i32, i32); // RGB 颜色
struct Point(i32, i32, i32); // 3D 点
// 或者单元结构体 (Unit-like Structs)
struct AlwaysEqual; // 没有字段,用于表示某种标记或类型
fn main() {
// 创建结构体实例 (顺序无关,字段名重要)
let mut user1 = User {
email: String::from(“[email protected]”),
username: String::from(“someusername123”),
active: true,
sign_in_count: 1,
};
// 访问结构体字段
println!("用户邮箱: {}", user1.email);
// 修改结构体字段 (如果实例是可变的)
user1.email = String::from("[email protected]");
println!("修改后的用户邮箱: {}", user1.email);
// 使用字段初始化简写语法
let email = String::from("[email protected]");
let username = String::from("user456");
let user2 = User {
email, // 字段名和变量名相同,可以简写
username,
active: false, // 其他字段正常赋值
sign_in_count: 5,
};
// 使用其他实例的部分字段创建新实例 (结构体更新语法)
let user3 = User {
email: String::from("[email protected]"),
..user2 // 复制 user2 剩余字段的值 (username, active, sign_in_count)
};
// 注意:如果 user2 包含实现了 Copy trait 的字段,那些字段会被复制。
// 如果包含没有实现 Copy trait 的字段 (如 String),这些字段的所有权会转移给 user3,user2 将不能再使用这些字段。
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
println!("黑色 RGB: ({}, {}, {})", black.0, black.1, black.2);
let subject = AlwaysEqual;
}
“`
结构体除了存储数据,还可以定义方法 (methods)。方法是关联到结构体的函数,使用 impl
关键字定义。
“`rust
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 这是一个方法,它的第一个参数是 self (引用了调用方法的实例)
fn area(&self) -> u32 {
self.width * self.height
}
// 这是一个带有参数的方法
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// 这是一个关联函数 (Associated Function),不借用或拥有实例
// 类似于其他语言的静态方法,常用于创建结构体的新实例
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
// 调用方法
println!("矩形的面积是 {} 平方像素。", rect1.area()); // 注意这里 rect1 后没有加 &,Rust 会自动借用 (&rect1)
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
println!("rect1 能容纳 rect2 吗? {}", rect1.can_hold(&rect2)); // true
println!("rect1 能容纳 rect3 吗? {}", rect1.can_hold(&rect3)); // false
// 调用关联函数
let sq = Rectangle::square(25);
println!("正方形的面积是 {}", sq.area());
}
“`
枚举 (Enums)
枚举允许你定义一个类型,它可能是一个有限集合中的几种不同变体 (variants) 之一。枚举的变体可以关联不同类型和数量的数据。
“`rust
// 定义一个枚举
enum IpAddrKind {
V4,
V6,
}
// 枚举的变体可以关联数据
enum IpAddr {
V4(String), // V4 变体关联一个 String
V6(String), // V6 变体关联一个 String
}
// 枚举变体可以关联不同类型的数据
enum Message {
Quit, // 没有关联数据
Move { x: i32, y: i32 }, // 关联一个匿名结构体
Write(String), // 关联一个 String
ChangeColor(i32, i32, i32), // 关联三个 i32
}
// 枚举也可以定义方法
impl Message {
fn call(&self) {
// 在这里定义处理不同 Message 变体的逻辑
match self {
Message::Quit => { println!(“Quitting”); }
Message::Move { x, y } => { println!(“Moving to ({}, {})”, x, y); }
Message::Write(text) => { println!(“Writing: {}”, text); }
Message::ChangeColor(r, g, b) => { println!(“Changing color to R:{} G:{} B:{}”, r, g, b); }
}
}
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
let m = Message::Write(String::from("hello"));
m.call(); // 调用枚举的方法
let msg = Message::Move { x: 10, y: 20 };
msg.call();
let msg2 = Message::Quit;
msg2.call();
}
“`
强大的 match
表达式:
Rust 的 match
表达式非常强大,它允许你将一个值与一系列模式进行匹配,并执行与匹配到的模式相关的代码。match
表达式是穷尽的,意味着你必须处理所有可能的变体或值。
“`rust
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => { // 匹配到 Penny 变体
println!(“幸运硬币!”); // 可以执行一些代码
1 // 返回值
},
Coin::Nickel => 5, // 匹配到 Nickel 变体,直接返回 5
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
println!(“A Penny is worth {} cents.”, value_in_cents(Coin::Penny));
println!(“A Quarter is worth {} cents.”, value_in_cents(Coin::Quarter));
// match 结合 Option<T> 的例子 (Option 是标准库内置的枚举)
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
println!("five + 1 = {:?}", six); // Some(6)
println!("None + 1 = {:?}", none); // None
}
// Option
fn plus_one(x: Option
match x {
None => None, // 如果 Option 是 None,返回 None
Some(i) => Some(i + 1), // 如果 Option 是 Some(i),返回 Some(i+1)
}
}
// 如果只想匹配某个模式,可以使用 if let (它是 match 的语法糖)
fn main() {
let config_max = Some(3u8);
match config_max {
Some(max) => println!(“配置的最大值是 {}”, max),
_ => (), // 使用 _ 表示匹配所有其他情况,并执行空操作 ()
}
// 等价于上面的 match
if let Some(max) = config_max {
println!("配置的最大值是 {}", max);
} // 忽略其他情况
}
“`
枚举和 match
是 Rust 中处理不同情况和模式匹配的强大工具,特别是在处理可能缺失的值 (Option<T>
) 和可能出错的操作 (Result<T, E>
) 时。
第九章:错误处理:如何优雅地处理可能出错的情况
Rust 鼓励你编写能够优雅地处理错误的代码,而不是简单地崩溃。Rust 没有像 Java 或 Python 那样的异常机制,它主要依赖于两种枚举来处理错误:Option<T>
和 Result<T, E>
。
我们已经在上面简要介绍了 Option<T>
,它用于表示一个值可能存在 (Some(T)
) 或不存在 (None
)。
Result
Result<T, E>
是另一个重要的枚举,它用于表示一个操作可能成功并返回一个值 (Ok(T)
),或者失败并返回一个错误 (Err(E)
)。
“`rust
// Result 枚举的简化定义
// enum Result
// Ok(T), // 成功时包含的值
// Err(E), // 失败时包含的错误
// }
use std::fs::File; // 引入标准库的 File 类型
use std::io::ErrorKind; // 引入标准库的 ErrorKind 枚举
fn main() {
// 尝试打开一个文件
let greeting_file_result = File::open(“hello.txt”);
// 使用 match 来处理 Result
let greeting_file = match greeting_file_result {
Ok(file) => file, // 如果成功,返回文件句柄
Err(error) => { // 如果失败,匹配不同的错误类型
match error.kind() {
ErrorKind::NotFound => { // 文件不存在错误
match File::create("hello.txt") { // 尝试创建文件
Ok(fc) => { // 如果创建成功
println!("文件已创建!");
fc // 返回新创建的文件句柄
},
Err(e) => { // 如果创建失败
panic!("尝试创建文件时发生错误: {:?}", e); // 发生致命错误,程序崩溃
},
}
}
other_error => { // 其他类型的错误
panic!("打开文件时发生错误: {:?}", other_error); // 发生致命错误,程序崩溃
}
}
}
}; // greeting_file 现在是一个成功打开或创建的文件句柄
println!("成功获取到文件句柄。");
// 可以在 match 之后继续使用 greeting_file 句柄进行文件操作...
// greeting_file 会在 main 函数结束时自动关闭 (通过 Drop trait)
}
“`
上面的 match
代码有点冗长,尤其是在你只关心成功或某种特定失败时。Rust 提供了更简洁的方式来处理 Result
。
简写方式:unwrap
和 expect
unwrap()
是 Result
和 Option
类型的方法,它是一个便捷方法:
- 如果
Result
是Ok
,unwrap()
返回Ok
中包含的值。 - 如果
Result
是Err
,unwrap()
会调用panic!
,使程序崩溃。
expect()
类似于 unwrap()
,但允许你提供一个错误信息,在 panic!
时显示。
“`rust
use std::fs::File;
fn main() {
// 使用 unwrap(): 如果文件不存在或无法打开,程序会崩溃
// let greeting_file = File::open(“hello.txt”).unwrap();
// 使用 expect(): 如果文件不存在或无法打开,程序会崩溃并显示指定信息
let greeting_file = File::open("hello.txt").expect("无法打开文件 hello.txt");
println!("成功获取到文件句柄 (使用 expect)。");
// greeting_file 会在 main 函数结束时自动关闭
}
“`
unwrap
和 expect
对于原型开发、测试或你确定不会失败(或失败了就应该崩溃)的情况很方便。但在生产代码中,通常应该更优雅地处理错误。
传播错误:?
运算符
当一个函数调用另一个可能返回 Result
的函数时,你通常不希望在当前函数内立即处理错误,而是希望将错误“传播”给调用者。?
运算符就是用来做这个的。
?
运算符只能用于返回 Result
(或 Option
) 的函数中。
“`rust
use std::fs::File;
use std::io::{self, Read};
// 这个函数尝试从文件读取用户名,并返回一个 Result
// 如果过程中有任何错误,错误会被 ? 运算符传播出去
fn read_username_from_file() -> Result
let mut username_file = File::open(“username.txt”)?; // 打开文件,如果出错,错误会被返回
let mut username = String::new();
username_file.read_to_string(&mut username)?; // 读取文件内容到字符串,如果出错,错误会被返回
Ok(username) // 如果一切顺利,返回包含用户名的 Ok
}
// 更短的版本
fn read_username_from_file_short() -> Result
let mut username = String::new();
File::open("username.txt")?.read_to_string(&mut username)?; // 链式调用
Ok(username)
}
// 甚至更短的版本 (使用 fs::read_to_string)
// 注意:这个版本不需要 ? 运算符,因为它返回 Result
// use std::fs;
// fn read_username_from_file_shortest() -> Result
// fs::read_to_string(“username.txt”)
// }
fn main() {
match read_username_from_file() {
Ok(username) => println!(“从文件读取到用户名: {}”, username),
Err(e) => println!(“读取用户名时发生错误: {:?}”, e),
}
// 也可以使用 expect 来处理结果
// let username = read_username_from_file().expect("无法读取用户名文件");
// println!("从文件读取到用户名: {}", username);
}
“`
?
运算符的工作方式是:如果 Result
是 Ok(v)
,?
会解包 v
并继续执行。如果 Result
是 Err(e)
,?
会立即从当前函数返回 Err(e)
。这极大地简化了错误处理的代码。
Rust 对错误处理的设计鼓励你思考并处理可能发生的错误,而不是让它们在运行时导致意外的崩溃。
第十章:接下来呢?
恭喜你!你已经了解了 Rust 的基础知识,包括环境搭建、Cargo 的使用、基本语法、控制流程、核心概念(所有权、借用、生命周期)以及结构体、枚举和错误处理。这为你深入学习 Rust 打下了坚实的基础。
但是,这仅仅是开始。Rust 还有很多强大的特性等待你去探索,比如:
- 包、Crates 和 Modules: 如何组织大型项目,使用外部库。
- 测试: 如何编写和运行单元测试、集成测试等。
- Traits: Rust 的抽象机制,类似于其他语言的接口或类型类。
- 泛型 (Generics): 编写适用于多种类型的代码。
- 闭包 (Closures): 可以捕获其周围环境的匿名函数。
- 迭代器 (Iterators): 一种处理序列的强大模式。
- 并发编程: 如何使用 Rust 的所有权系统安全地编写多线程代码。
- 宏 (Macros): 编写可以生成代码的代码。
- Unsafe Rust: 允许你绕过 Rust 的安全保证,进行底层操作(请谨慎使用!)。
- 模式匹配 (Pattern Matching) 的更深入用法。
推荐的下一步学习资源:
- The Rust Programming Language (Rust 官方书籍): 这是学习 Rust 最权威、最全面的资源,俗称“Rust Book”。它有在线版本,并且已经翻译成多种语言(包括中文)。强烈建议你对照本文的基础内容,继续深入阅读 Rust Book。
- Rust By Example: 通过大量的代码示例来学习 Rust 的概念和标准库。
- Exercism: 提供大量的编程练习题,让你通过实践来巩固和提升 Rust 技能,并可以获得社区成员的代码评审。
- 构建小项目: 学习最好的方式是实践。尝试用 Rust 编写一些小工具、命令行程序、简单的 Web 服务等。你可以从简单的“猜数字游戏”开始,然后尝试构建更复杂的项目,比如一个简单的 TODO 应用、一个文件搜索工具等。
结论
祝贺你完成了这篇 Rust 入门文章的学习!你已经踏上了掌握这门令人兴奋的语言的道路。
学习 Rust 需要耐心和毅力,特别是当你第一次接触所有权、借用等概念时。不要因为遇到困难而气馁。这些概念是 Rust 独特安全性的基石,一旦理解并习惯了与借用检查器“合作”,你会发现它是一个强大的盟友,帮助你避免许多常见的 bug。
记住,实践是掌握编程语言的关键。多写代码,多尝试,多查阅官方文档和社区资源。加入 Rust 社区(论坛、Discord、IRC 等),与其他 Rust 开发者交流,提问问题。
Rust 拥有一个热情友好的社区,他们乐于帮助新手。
祝你在 Rust 的学习旅程中一切顺利!期待你在 Rust 世界里创造出令人惊叹的作品。
现在,打开你的编辑器,开始编写更多的 Rust 代码吧!