零基础入门 Rust 编程语言 – wiki基地


零基础入门 Rust 编程语言:迈出安全、高性能编程的第一步

欢迎来到 Rust 的世界!如果你对编程充满好奇,或者已经接触过其他语言但被 Rust 的独特魅力(安全、高性能、并发无畏)所吸引,那么这篇为你量身打造的零基础入门指南将是你的完美起点。

不用担心你没有任何编程经验,我们将从最基本的概念讲起,一步一步带你领略 Rust 的风采。Rust 可能有些概念(比如所有权)初听起来会觉得陌生,但这正是它强大和独特之处。请保持耐心和好奇心,你将打开一扇通往现代系统编程的大门。

本文将带你了解:

  1. Rust 是什么,为什么选择 Rust?
  2. 如何安装 Rust 开发环境。
  3. 你的第一个 Rust 程序:Hello, World!
  4. 认识 Rust 的构建工具:Cargo。
  5. Rust 的基本概念:变量、数据类型、函数。
  6. 控制流程:条件判断与循环。
  7. Rust 的核心:所有权、借用与生命周期(入门级理解)。
  8. 结构体与枚举:组织数据的方式。
  9. 错误处理:如何优雅地处理可能出错的情况。
  10. 接下来的学习路径。

准备好了吗?让我们开始这段令人兴奋的旅程!

第一章: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 版本和相关工具的命令行工具。

安装步骤:

  1. 打开终端或命令提示符:

    • 在 Windows 上,搜索并打开 “Command Prompt” 或 “PowerShell”。
    • 在 macOS 或 Linux 上,打开 “Terminal” 应用。
  2. 运行安装命令:

    • macOS 或 Linux: 在终端中粘贴并运行以下命令:
      bash
      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

      这个命令会下载并运行一个脚本,脚本会指引你完成安装。通常选择默认安装即可(输入 1 或直接按回车)。
    • Windows: 访问 https://rustup.rs,下载并运行 rustup-init.exe 安装程序。按照提示进行安装,同样通常选择默认安装即可。
  3. 配置环境变量 (如果需要): 安装程序通常会自动配置环境变量,但如果没有,或者你需要手动配置,请确保 Rust 的 bin 目录(通常是 $HOME/.cargo/bin%USERPROFILE%\.cargo\bin)被添加到了系统的 PATH 环境变量中。这允许你在任何地方运行 rustccargorustup 命令。安装程序完成时通常会有提示。

  4. 验证安装: 安装完成后,关闭并重新打开你的终端或命令提示符,然后运行以下命令来验证 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!

安装完成后,是时候写下所有编程语言的第一个经典程序了!

  1. 创建项目文件夹:
    在你的电脑上选择一个合适的位置,创建一个新的文件夹,比如叫做 rust_intro
    bash
    mkdir rust_intro
    cd rust_intro

  2. 创建源代码文件:
    rust_intro 文件夹内,创建一个名为 main.rs 的文件。Rust 源代码文件的扩展名是 .rs

  3. 编写代码:
    用你喜欢的文本编辑器打开 main.rs 文件,输入以下代码:
    “`rust
    // 这是一个注释,编译器会忽略它

    fn main() {
    // println! 是一个宏,用于打印文本到控制台
    println!(“Hello, world!”);
    }
    “`

    代码解释:
    * // 开头的是注释,用来解释代码,方便自己和他人理解。
    * fn main() { ... } 定义了一个名为 main 的函数。在 Rust 中,main 函数是程序的入口点,程序运行时会首先执行 main 函数里的代码。fn 关键字用来声明一个函数,() 里是函数的参数列表(这里为空),{} 里是函数体。
    * println!("Hello, world!"); 是函数体里的一条语句。println! 是一个 (macro),而不是一个普通的函数。宏在编译时展开,可以执行一些比普通函数更灵活的操作。println! 的作用是将括号里的字符串打印到标准输出(通常是你的终端),并在末尾添加一个换行符。注意字符串是写在双引号 "" 里面的。
    * 每条语句的末尾都以分号 ; 结束(除了块表达式的最后一行)。

  4. 编译程序:
    打开终端,进入到 rust_intro 文件夹。运行 Rust 编译器 rustc 来编译你的源代码文件:
    bash
    rustc main.rs

    如果一切顺利,终端不会有任何输出(或者只显示一些编译信息),并且在 rust_intro 文件夹下会生成一个可执行文件。在 Windows 上是 main.exe,在 macOS 或 Linux 上是 main

  5. 运行程序:
    现在,运行生成的可执行文件:

    • Windows:
      bash
      .\main.exe
    • macOS 或 Linux:
      bash
      ./main

    你应该会在终端看到输出:
    Hello, world!

恭喜你!你已经成功编写、编译并运行了你的第一个 Rust 程序。这是一个重要的里程碑!

虽然直接使用 rustc 编译单个文件很简单,但在实际开发中,Rust 项目通常会包含多个文件、依赖其他库。这时候,我们更常用 Rust 的构建工具和包管理器 Cargo

第四章:认识 Rust 的构建工具:Cargo

Cargo 是 Rust 生态系统的核心工具,它帮助你处理很多任务:创建项目、管理依赖、编译代码、运行测试、生成文档等。对于任何非 trivial 的 Rust 项目,强烈推荐使用 Cargo。

使用 Cargo 创建项目:

相比手动创建文件,使用 Cargo 创建项目更方便:

  1. 打开终端,进入你想要存放项目的目录。
  2. 运行 cargo new 命令:
    bash
    cargo new hello_cargo
    cd hello_cargo

    这个命令会创建一个名为 hello_cargo 的新文件夹。进入该文件夹,你会看到 Cargo 生成的项目结构:
    hello_cargo/
    ├── Cargo.toml
    └── src/
    └── main.rs

    • Cargo.toml: 这是 Cargo 的配置文件,采用 TOML (Tom’s Obvious, Minimal Language) 格式。它包含了项目的元信息(如名称、版本、作者)以及项目依赖的其他库。
    • src/: 这是存放项目源代码的地方。
    • src/main.rs: Cargo 默认生成的主程序文件,里面已经包含了经典的 Hello, world! 代码。

使用 Cargo 编译和运行项目:

进入到 hello_cargo 文件夹后,你可以使用 Cargo 的命令来编译和运行程序:

  1. 编译项目:
    bash
    cargo build

    这个命令会读取 Cargo.toml,下载所需的依赖(如果需要),然后编译 src 目录下的源代码。编译成功后,会在项目根目录下的 target/debug/ 目录里生成可执行文件。

  2. 运行项目:
    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 buildcargo 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:只有两个可能的值 truefalse
    • 例如: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.4

      let 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 提供了三种循环:loopwhilefor

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 的所有权系统是一组规则,编译器在编译时使用它们来管理内存。它不会产生运行时开销。

所有权规则:

  1. Rust 中的每个值都有一个变量作为它的所有者 (owner)。
  2. 同一时刻,一个值只能有一个所有者。
  3. 当所有者(变量)离开其作用域 (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
}
“`

借用规则(核心):

在任意给定时间,你只能选择以下两种借用方式之一:

  1. 一个可变的引用 (&mut T)。
  2. 任意数量的不可变引用 (&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 是一个枚举,定义为 enum Option { Some(T), None }
fn plus_one(x: Option) -> 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

简写方式:unwrapexpect

unwrap()ResultOption 类型的方法,它是一个便捷方法:

  • 如果 ResultOkunwrap() 返回 Ok 中包含的值。
  • 如果 ResultErrunwrap() 会调用 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 函数结束时自动关闭

}
“`

unwrapexpect 对于原型开发、测试或你确定不会失败(或失败了就应该崩溃)的情况很方便。但在生产代码中,通常应该更优雅地处理错误。

传播错误:? 运算符

当一个函数调用另一个可能返回 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);

}
“`

? 运算符的工作方式是:如果 ResultOk(v)? 会解包 v 并继续执行。如果 ResultErr(e)? 会立即从当前函数返回 Err(e)。这极大地简化了错误处理的代码。

Rust 对错误处理的设计鼓励你思考并处理可能发生的错误,而不是让它们在运行时导致意外的崩溃。

第十章:接下来呢?

恭喜你!你已经了解了 Rust 的基础知识,包括环境搭建、Cargo 的使用、基本语法、控制流程、核心概念(所有权、借用、生命周期)以及结构体、枚举和错误处理。这为你深入学习 Rust 打下了坚实的基础。

但是,这仅仅是开始。Rust 还有很多强大的特性等待你去探索,比如:

  • 包、Crates 和 Modules: 如何组织大型项目,使用外部库。
  • 测试: 如何编写和运行单元测试、集成测试等。
  • Traits: Rust 的抽象机制,类似于其他语言的接口或类型类。
  • 泛型 (Generics): 编写适用于多种类型的代码。
  • 闭包 (Closures): 可以捕获其周围环境的匿名函数。
  • 迭代器 (Iterators): 一种处理序列的强大模式。
  • 并发编程: 如何使用 Rust 的所有权系统安全地编写多线程代码。
  • 宏 (Macros): 编写可以生成代码的代码。
  • Unsafe Rust: 允许你绕过 Rust 的安全保证,进行底层操作(请谨慎使用!)。
  • 模式匹配 (Pattern Matching) 的更深入用法。

推荐的下一步学习资源:

  1. The Rust Programming Language (Rust 官方书籍): 这是学习 Rust 最权威、最全面的资源,俗称“Rust Book”。它有在线版本,并且已经翻译成多种语言(包括中文)。强烈建议你对照本文的基础内容,继续深入阅读 Rust Book。
  2. Rust By Example: 通过大量的代码示例来学习 Rust 的概念和标准库。
  3. Exercism: 提供大量的编程练习题,让你通过实践来巩固和提升 Rust 技能,并可以获得社区成员的代码评审。
  4. 构建小项目: 学习最好的方式是实践。尝试用 Rust 编写一些小工具、命令行程序、简单的 Web 服务等。你可以从简单的“猜数字游戏”开始,然后尝试构建更复杂的项目,比如一个简单的 TODO 应用、一个文件搜索工具等。

结论

祝贺你完成了这篇 Rust 入门文章的学习!你已经踏上了掌握这门令人兴奋的语言的道路。

学习 Rust 需要耐心和毅力,特别是当你第一次接触所有权、借用等概念时。不要因为遇到困难而气馁。这些概念是 Rust 独特安全性的基石,一旦理解并习惯了与借用检查器“合作”,你会发现它是一个强大的盟友,帮助你避免许多常见的 bug。

记住,实践是掌握编程语言的关键。多写代码,多尝试,多查阅官方文档和社区资源。加入 Rust 社区(论坛、Discord、IRC 等),与其他 Rust 开发者交流,提问问题。

Rust 拥有一个热情友好的社区,他们乐于帮助新手。

祝你在 Rust 的学习旅程中一切顺利!期待你在 Rust 世界里创造出令人惊叹的作品。

现在,打开你的编辑器,开始编写更多的 Rust 代码吧!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部