从零开始学习 Rust:入门篇 – wiki基地


从零开始学习 Rust:入门篇

欢迎来到 Rust 的世界!

或许你已经听说过 Rust 的大名——它以其卓越的性能、可靠性和开发效率而闻名。在系统编程、WebAssembly、命令行工具、网络服务等领域,Rust 正变得越来越受欢迎。对于许多开发者来说,Rust 提供了一种在追求极致性能的同时,也能享受到强大安全保障的编程体验,尤其是其独特的内存安全保证,无需垃圾回收(GC)即可避免常见的内存错误(如空指针解引用、数据竞争等)。

如果你是一名对系统编程感兴趣、希望编写高性能代码、或者只是想学习一门设计哲学与众不同的现代化语言的初学者,那么 Rust 绝对值得你投入时间。

这篇入门指南将带领你从零开始,了解 Rust 是什么,为什么要学习它,以及如何安装 Rust 开发环境,编写你的第一个 Rust 程序,并初步掌握 Rust 的一些核心概念。我们将尽量详细地讲解每一个步骤,确保即使是编程新手也能顺利入门。

1. 为什么选择学习 Rust?

在深入学习之前,我们先来聊聊 Rust 的吸引力所在。为什么这么多开发者对 Rust 趋之若鹜?

  1. 性能卓越: Rust 是一门编译型语言,编译后的代码运行速度极快,与 C++ 等语言相当。它提供了对底层硬件的精细控制,没有运行时开销(比如垃圾回收),非常适合编写对性能要求极高的应用,如操作系统内核、游戏引擎、嵌入式系统、高性能服务器等。
  2. 内存安全(无 GC): 这是 Rust 最具特色的卖点。通过其创新的所有权(Ownership)、借用(Borrowing)和生命周期(Lifetimes)系统,Rust 在编译期就能检查并预防 C/C++ 中常见的内存安全问题,如悬垂指针(Dangling Pointers)、空指针解引用(Null Pointer Dereferencing)和数据竞争(Data Races)。这一切都在不引入垃圾回收器(GC)的情况下实现,这意味着你可以获得 C/C++ 的性能,同时避免大量的内存安全 bug。
  3. 强大的并发性: Rust 的所有权系统天然地支持安全并发编程。在编译期,Rust 就能检测到可能导致数据竞争的并发访问,从而让你在编写多线程程序时更加自信和安全。
  4. 可靠性高: 除了内存安全,Rust 强大的类型系统和模式匹配等特性,有助于捕获更多类型的 bug,使得代码更加健壮和可靠。Rust 编译器以其严格而闻名,虽然有时会让新手感到挫败,但它更像是一位严格的导师,帮助你编写出高质量的代码。
  5. 友好的开发工具: Rust 配备了一流的工具链,特别是 Cargo,它是 Rust 的构建工具和包管理器。Cargo 让创建项目、管理依赖、编译、测试和运行代码变得异常简单,极大地提升了开发效率。
  6. 跨平台: Rust 代码可以编译到多种平台,包括 Windows、macOS、Linux 以及各种嵌入式系统和 WebAssembly。
  7. 活跃的社区和生态系统: Rust 拥有一个充满活力、乐于助人且不断成长的社区。Rust 的标准库功能强大,同时 Crates.io(Rust 的官方包注册中心)上有海量的第三方库(称为“crate”),覆盖了各种应用领域。

总而言之,Rust 旨在提供一种既能充分利用硬件性能,又能保证代码安全和可靠性的编程体验。学习 Rust 可能会有挑战,但它带来的回报是巨大的。

2. 环境搭建:安装 Rust

学习任何编程语言的第一步都是安装开发环境。Rust 的安装非常简单,主要依赖于一个官方工具 rustuprustup 是一个 Rust 版本管理工具,它可以安装和管理多个 Rust 版本以及相关的工具链。

2.1 使用 rustup 安装 Rust

访问 Rust 官方网站:https://www.rust-lang.org/。在首页你会看到安装 Rust 的指引。

Linux 和 macOS 用户

打开终端,输入以下命令并按回车:

bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

这条命令会下载一个脚本并执行它。脚本会引导你完成安装过程。大多数情况下,你可以直接选择默认安装选项,输入 1 并按回车。

安装完成后,你需要将 Cargo 的 bin 目录添加到你的系统 PATH 环境变量中。安装脚本通常会提示你执行一个命令来完成这一步。例如,对于 Bash 或 Zsh 用户,你可能需要运行:

bash
source $HOME/.cargo/env

或者,为了让它永久生效,你需要将 export PATH="$HOME/.cargo/bin:$PATH" 这行添加到你的 shell 配置文件中(如 .bashrc, .zshrc, .profile 等)。然后重启终端或执行 source ~/.your_shell_config_file 使其生效。

Windows 用户

访问 https://www.rust-lang.org/tools/install,下载 rustup-init.exe 安装程序。

双击运行下载的 .exe 文件。安装程序会启动一个命令行窗口,引导你完成安装。同样,大多数情况下,选择默认安装选项即可。Rust 需要安装 Microsoft C++ Build Tools。安装程序可能会提示你安装它们。你可以通过 Visual Studio Installer(安装 Visual Studio Community 版本并选择“使用 C++ 的桌面开发”工作负载)或者单独安装 Microsoft C++ Build Tools 来获取。跟随安装程序的提示进行即可。

安装程序会自动将 Cargo 的 bin 目录添加到你的 PATH 环境变量中。你可能需要重启命令行窗口或者电脑使环境变量生效。

2.2 验证安装

安装完成后,打开一个新的终端或命令行窗口,运行以下命令:

bash
rustc --version
cargo --version

如果安装成功,你应该会看到 Rust 编译器 (rustc) 和 Cargo (cargo) 的版本信息。这表明 Rust 工具链已经正确安装并配置到你的系统 PATH 中了。

rustc 1.xx.x (xxxxxxxx xx-xx-xx) # 版本号可能不同
cargo 1.xx.x (xxxxxxxx xx-xx-xx) # 版本号可能不同

恭喜你!你已经成功安装了 Rust 开发环境。

3. 你的第一个 Rust 程序:“Hello, World!”

现在,让我们来编写并运行传统的第一个程序:“Hello, World!”。这将帮助你了解 Rust 代码的基本结构以及如何编译和运行它。

3.1 创建源文件

在你的文件系统的任何位置创建一个新文件夹,例如 rust_hello_world。进入这个文件夹,并创建一个名为 main.rs 的文件。Rust 源文件总是以后缀 .rs 结尾。

“`bash
mkdir rust_hello_world
cd rust_hello_world

对于 Linux/macOS

touch main.rs

对于 Windows

echo “” > main.rs
“`

用你喜欢的文本编辑器或集成开发环境(IDE)打开 main.rs 文件,输入以下代码:

rust
// 这是一个注释,以双斜杠开始
// main 函数是程序的入口点
fn main() {
// println! 是一个 Rust 宏,用于打印文本到控制台
// 双引号内的文本会被打印出来
println!("Hello, world!");
}

保存文件。

3.2 理解代码

  • // ...:这是单行注释。注释是编译器会忽略的文本,用于向人类解释代码。
  • fn main() { ... }
    • fn 关键字用于声明一个函数。
    • main 是函数的名称。在可执行的 Rust 程序中,main 函数是程序的入口点,当程序运行时,它会首先执行 main 函数中的代码。
    • () 表示 main 函数没有参数。
    • {} 包围的部分是函数体,包含了函数要执行的代码。
  • println!("Hello, world!");
    • println! 是一个 Rust 宏(Macro),而不是普通函数。宏的名称后面跟着一个感叹号 !。宏可以做一些函数做不到的事情,比如在编译时生成代码。println! 宏用于将文本打印到标准输出(通常是控制台)。
    • "Hello, world!" 是一个字符串字面量(string literal),是我们要打印的内容。
    • ! 表示这是一个宏调用。
    • 每行代码都以分号 ; 结束,表示一个语句的结束。

3.3 编译和运行

现在,打开终端或命令行窗口,导航到你创建的 rust_hello_world 文件夹。

使用 Rust 编译器 rustc 来编译你的 main.rs 文件:

bash
rustc main.rs

如果你没有输入错误,rustc 命令将不会输出任何信息,并且会在当前目录下生成一个可执行文件:
* 在 Linux/macOS 上,文件名为 main
* 在 Windows 上,文件名为 main.exe

现在,运行这个可执行文件:

“`bash

对于 Linux/macOS

./main

对于 Windows

.\main.exe
“`

你应该会在终端看到输出:

Hello, world!

恭喜!你已经成功编译并运行了你的第一个 Rust 程序!

4. Rust 的编译过程简介

与 Python 或 JavaScript 等解释型语言不同,Rust 是一种编译型语言。这意味着你编写的源代码(.rs 文件)在运行之前需要通过编译器(rustc)转换成机器可以直接执行的二进制代码。

当你在终端中运行 rustc main.rs 时,编译器会进行以下工作:

  1. 解析(Parsing): 读取你的源代码,检查语法是否符合 Rust 的规则,并构建一个抽象语法树(AST)。
  2. 类型检查和借用检查: 这是 Rust 独有的重要阶段。编译器会在这里检查变量的类型是否匹配,以及所有权、借用和生命周期规则是否被遵守。这个阶段会捕获大量的潜在运行时错误,包括内存安全问题和数据竞争。如果存在任何错误,编译器会在这里停止并输出详细的错误信息,提示你如何修改。
  3. 代码生成(Code Generation): 如果代码通过了所有检查,编译器会将 AST 转换为中间表示(IR),然后使用 LLVM 后端将 IR 优化并生成目标平台的机器代码。
  4. 链接(Linking): 将生成的机器代码与 Rust 标准库以及你程序可能依赖的任何其他库的代码合并,最终生成一个独立的可执行文件。

这个编译过程是 Rust 保证高性能和安全的关键。虽然编译可能需要一些时间(尤其是对于大型项目),但它换来了运行时的高效率和稳定性。

5. Rust 的构建工具和包管理器:Cargo

虽然可以使用 rustc 直接编译简单的单个文件程序,但对于任何稍微复杂一点的 Rust 项目,你都会需要使用 CargoCargo 是 Rust 官方推荐的构建工具和包管理器,它简化了 Rust 项目的开发、构建、测试和发布流程。

Cargo 提供了以下主要功能:

  • 创建项目: 使用简单的命令创建一个标准的 Rust 项目结构。
  • 构建代码: 编译你的项目及其依赖项。
  • 运行代码: 编译并执行你的项目。
  • 测试代码: 运行你为项目编写的测试。
  • 管理依赖: 指定你的项目依赖哪些外部库,并自动下载和编译它们。

我们强烈建议你从一开始就使用 Cargo 来管理你的 Rust 项目。

5.1 创建一个新项目

回到终端,导航到你想创建项目的目录(可以不是之前那个 rust_hello_world 目录),运行以下命令:

bash
cargo new hello_cargo

这条命令会创建一个名为 hello_cargo 的新目录,并在其中设置一个标准的 Rust 项目结构:

hello_cargo/
├── Cargo.toml
└── src/
└── main.rs

  • Cargo.toml:这是 Cargo 的配置文件。它使用 TOML(Tom’s Obvious, Minimal Language)格式编写,包含项目的元信息(如名称、版本、作者)以及项目依赖的库列表。
  • src/:这是一个目录,用于存放项目的源代码。
  • src/main.rs:Cargo 创建新项目时默认会在这里生成一个“Hello, World!”程序,作为项目的入口文件。

5.2 查看 Cargo.toml 文件

用文本编辑器打开 hello_cargo/Cargo.toml 文件,内容大致如下:

“`toml
[package]
name = “hello_cargo”
version = “0.1.0”
edition = “2021” # 或者其他年份,表示使用的 Rust 版本/版本集

See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

在这里添加你的项目依赖的外部库

“`

  • [package] 部分包含了项目的基本信息。
  • name:项目名称。
  • version:项目版本号。
  • edition:指定项目使用的 Rust 版本集(edition)。不同 edition 可能引入一些不兼容的语法或行为变化,但通常是向后兼容的。
  • [dependencies] 部分用于列出项目依赖的外部库。稍后当我们学习如何使用第三方库时,就会在这里添加。

5.3 查看 src/main.rs 文件

打开 hello_cargo/src/main.rs 文件,你会发现它和我们之前手动创建的 main.rs 内容一样:

rust
fn main() {
println!("Hello, world!");
}

这是 Cargo 为新项目生成的默认入口文件。

5.4 使用 Cargo 构建和运行项目

导航到 hello_cargo 目录,使用 Cargo 命令来构建和运行你的项目:

构建项目:

bash
cargo build

第一次运行 cargo build 时,Cargo 会在项目根目录下创建一个 target 目录。编译生成的可执行文件会放在 target/debug/ 目录下(在 Windows 上是 target\debug\hello_cargo.exe)。debug 是默认的构建模式,用于开发和调试,包含调试信息,但不进行最大程度的优化。

运行项目:

bash
cargo run

cargo run 命令非常方便。它会先检查代码是否有改动,如果有,它会先重新构建项目,然后运行生成的可执行文件。你会在终端看到输出:

Hello, world!

构建发布版本:

当你准备发布你的应用时,可以使用 --release 标志来构建优化后的版本:

bash
cargo build --release

这会在 target/release/ 目录下生成一个可执行文件。发布版本会进行更多的编译器优化,运行速度更快,但编译时间会更长。

检查代码(不生成可执行文件):

bash
cargo check

cargo check 命令会快速检查你的代码是否有错误和警告,但不会生成可执行文件。这比 cargo build 快得多,在你频繁修改代码时非常有用,可以快速获得编译器的反馈。

从现在开始,我们将在本书中使用 Cargo 来管理项目和运行代码。它是 Rust 开发中不可或缺的一部分。

6. Rust 的基本概念:变量、数据类型与函数

掌握了环境搭建和项目结构,我们现在可以开始学习 Rust 的一些基本编程概念了。

6.1 变量与可变性(Mutability)

在 Rust 中,变量使用 let 关键字声明。默认情况下,Rust 中的变量是不可变的(immutable)。这意味着一旦给变量赋了值,就不能再改变它的值。

“`rust
fn main() {
let x = 5; // 声明一个不可变变量 x,并赋值为 5
println!(“x 的值是:{}”, x);

// 下面这行会导致编译错误,因为 x 是不可变的
// x = 6; // error: cannot assign twice to immutable variable `x`

}
“`

不可变性是 Rust 提供的安全性特性之一。它可以防止你在不经意间修改了本不应该修改的值,使得代码更容易理解和推理。

如果你确实需要一个可变的变量,可以使用 mut 关键字使其变为可变:

“`rust
fn main() {
let mut y = 5; // 声明一个可变变量 y
println!(“y 的值是:{}”, y);

y = 6; // 现在可以改变 y 的值了
println!("y 的新值是:{}", y);

}
“`

6.2 影子(Shadowing)

Rust 允许你声明一个新的变量,其名称与之前声明的变量相同。这个新变量会“遮蔽”掉(shadow)之前的变量。

“`rust
fn main() {
let x = 5; // x 现在是 5
println!(“x 的值是:{}”, x);

let x = x + 1; // 声明一个新的 x,值为旧 x + 1 (即 6)
println!("x 的新值是:{}", x);

{ // 在一个作用域块内
    let x = x * 2; // 声明另一个新的 x,值为旧 x * 2 (即 12)
    println!("内层作用域的 x 是:{}", x);
} // 作用域块结束,内层的 x 不再有效

println!("外层作用域的 x 仍然是:{}", x); // 外层的 x (值为 6) 依然有效

}
“`

Shadowing 与 mut 的区别在于:

  • mut 允许你改变同一个变量绑定的值。
  • Shadowing 是创建的变量绑定,只是新变量的名字与旧变量相同。你可以改变新变量的类型,而 mut 不允许改变变量的类型。Shadowing 常用于转换变量的类型,例如从字符串解析出数字:

“`rust
fn main() {
let spaces = ” “;
let spaces = spaces.len(); // spaces 现在是数字 3,类型从 &str 变为 usize
println!(“空格的数量是:{}”, spaces);

// let mut spaces = "   ";
// spaces = spaces.len(); // 这会导致编译错误,因为不能改变变量的类型

}
“`

6.3 数据类型(Data Types)

Rust 是一种静态类型语言,这意味着在编译时需要知道所有变量的类型。不过,Rust 的编译器有强大的类型推断能力,很多时候你不需要显式地指定类型。

Rust 有多种内置(原生)数据类型:

标量类型(Scalar Types)

标量类型代表一个单一的值。Rust 有四种主要的标量类型:整数、浮点数、布尔值和字符。

  • 整数类型(Integer Types):

    • 有符号整数:i8, i16, i32, i64, i128 (能存储正负数)
    • 无符号整数:u8, u16, u32, u64, u128 (只能存储非负数)
    • 架构依赖的整数:isize, usize (类型取决于你运行程序的计算机架构,32 位系统上是 32 位,64 位系统上是 64 位。usize 通常用于索引集合或表示大小)
    • 默认情况下,Rust 推断整数类型为 i32usizeisize 用于索引。
    • 可以使用后缀指定类型,例如 57u8
  • 浮点数类型(Floating-Point Types):

    • f32 (单精度)
    • f64 (双精度)
    • 默认情况下,Rust 推断浮点类型为 f64(在现代 CPU 上速度与 f32 相似但精度更高)。
  • 布尔类型(Boolean Type):

    • bool 类型,有两个可能的值:truefalse
    • 通常在条件语句中使用。
  • 字符类型(Character Type):

    • char 类型,代表一个 Unicode 标量值。Rust 的 char 类型比 C/C++ 的 char 类型(通常是 ASCII 码)更宽泛,它可以表示各种语言的字母、符号、表情符号等。
    • 字符字面量使用单引号 'A'。字符串字面量使用双引号 "Hello"

“`rust
fn main() {
let an_integer = 5; // 类型推断为 i32
let another_integer: i32 = 10; // 显式指定类型

let a_float = 2.0; // 类型推断为 f64
let another_float: f32 = 3.0; // 显式指定类型

let t = true; // 类型推断为 bool
let f: bool = false; // 显式指定类型

let c = 'z'; // 类型推断为 char
let z: char = 'ℤ'; // Unicode 字符
let heart_eyed_cat = '😻'; // 表情符号

println!("整数: {}, {}", an_integer, another_integer);
println!("浮点数: {}, {}", a_float, another_float);
println!("布尔值: {}, {}", t, f);
println!("字符: {}, {}, {}", c, z, heart_eyed_cat);

let byte: u8 = 255; // 显式指定 u8 类型
println!("u8: {}", byte);

}
“`

复合类型(Compound Types)

复合类型可以将多个值组合成一个类型。Rust 有两种基本的复合类型:元组(tuple)和数组(array)。

  • 元组(Tuple):
    • 元组是将多个不同类型的值组合到一个复合类型中。元组的长度是固定的,一旦声明就不能改变。
    • 使用圆括号 () 声明元组,其中的值用逗号分隔。
    • 可以通过模式匹配来解构元组,或者使用点号 . 后跟索引来访问元组的元素(索引从 0 开始)。

“`rust
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1); // 声明一个元组,并显式指定类型
println!(“完整的元组: {:?}”, tup); // 使用 {:?} 格式化打印元组

// 解构元组
let (x, y, z) = tup;
println!("解构出的值: x = {}, y = {}, z = {}", x, y, z);

// 通过索引访问元组元素
let five_hundred = tup.0;
let six_point_four = tup.1;
let one = tup.2;
println!("通过索引访问: five_hundred = {}, six_point_four = {}, one = {}", five_hundred, six_point_four, one);

}
“`

  • 数组(Array):
    • 数组是一组相同类型的数据的集合。与元组不同,数组的长度也是固定的,但其中的元素必须是同一种类型。
    • 使用方括号 [] 声明数组。
    • 数组的长度在编译时确定。
    • 可以通过索引访问数组元素,索引从 0 开始。
    • 如果你需要一个长度可变的同类型值集合,可以使用标准库中的 Vec<T>(向量),这在后续会学习。

“`rust
fn main() {
let a = [1, 2, 3, 4, 5]; // 声明一个 i32 类型的数组,长度为 5
println!(“完整的数组: {:?}”, a);

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"]; // 字符串数组
println!("月份数组: {:?}", months);

// 显式指定数组类型和长度 [type; length]
let b: [i32; 5] = [1, 2, 3, 4, 5];

// 创建一个包含相同元素的数组 [value; length]
let c = [3; 5]; // 数组 c 是 [3, 3, 3, 3, 3]

// 访问数组元素
let first = a[0]; // 访问第一个元素
let second = a[1]; // 访问第二个元素
println!("数组的第一个元素: {}", first);
println!("数组的第二个元素: {}", second);

// 访问越界会导致运行时错误(Panic),而不是像 C/C++ 那样产生未定义行为
// let index = 10;
// let element = a[index]; // 这会导致程序运行时崩溃 (panic)
// println!("尝试访问越界元素: {}", element); // 这行不会执行

}
“`

6.4 函数(Functions)

函数是 Rust 代码的核心组成部分,用于组织和复用代码。我们已经看到了 main 函数。

  • 使用 fn 关键字定义函数。
  • 函数名通常使用蛇形命名法(snake_case),即全小写,单词之间用下划线分隔。
  • 参数在函数名后的圆括号中指定,需要声明类型。
  • 如果函数有返回值,需要使用 -> 符号指定返回值的类型。
  • 函数体由花括号 {} 包围。
  • Rust 中的函数体由一系列语句(statement)和一个可选的表达式(expression)组成。
    • 语句 执行某些操作,但不返回一个值。例如 let x = 5;
    • 表达式 执行某些操作,并计算(返回)一个值。例如 5 + 6。函数调用、宏调用、花括号内的代码块 {} 本身都可以是表达式。
  • 函数的最后一个表达式的值会作为函数的返回值。不需要使用 return 关键字,除非你想提前从函数中返回。

“`rust
fn main() {
println!(“Hello from main!”);

another_function(); // 调用另一个函数
function_with_params(5, 6); // 调用带参数的函数

let x = five(); // 调用有返回值的函数,将结果赋给 x
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, y: i32) {
println!(“参数 x 的值是:{}”, x);
println!(“参数 y 的值是:{}”, y);
}

// 定义一个有返回值的函数
// -> i32 表示函数返回一个 i32 类型的值
fn five() -> i32 {
5 // 这是函数体中的最后一个表达式,它的值 5 被作为返回值
}

// 定义一个带参数且有返回值的函数
fn plus_one(x: i32) -> i32 {
x + 1 // 这也是一个表达式,不需要加分号
// 如果加了分号,x + 1; 就变成了一个语句,语句不返回值,会导致编译错误
// return x + 1; // 使用 return 关键字也可以
}
“`

注意在 fiveplus_one 函数中,最后一行 5x + 1 后面没有分号。这表明它们是表达式,它们的值将作为函数的返回值。如果在后面加上分号,它们就变成了语句,函数将返回单元类型 ()(类似于其他语言中的 void),这与声明的返回类型 i32 不匹配,会导致编译错误。

7. 控制流程(Control Flow)

控制流程用于决定程序执行代码的顺序。Rust 提供了常见的控制流程结构:if/else 表达式和循环。

7.1 if/else 表达式

在 Rust 中,if 块是表达式,它会返回一个值。

“`rust
fn main() {
let number = 7;

if number < 5 {
    println!("条件为真:数字小于 5");
} else {
    println!("条件为假:数字大于或等于 5");
}

let number = 3;

if number % 4 == 0 {
    println!("数字可以被 4 整除");
} else if number % 3 == 0 {
    println!("数字可以被 3 整除");
} else if number % 2 == 0 {
    println!("数字可以被 2 整除");
} else {
    println!("数字不能被 4、3、2 整除");
}

// if 在 let 语句中使用 (if 是表达式)
let condition = true;
let number = if condition { 5 } else { 6 }; // if 块和 else 块的返回值类型必须一致

println!("if/else 表达式返回的值是:{}", number);

}
“`

注意,在 if 语句中,条件必须是 bool 类型。Rust 不会自动将非布尔类型转换为布尔类型(例如 C++ 中的非零整数被视为 true)。

7.2 循环(Loops)

Rust 提供了三种主要的循环结构:loop, while, 和 for

  • loop 循环:
    • loop 关键字创建一个无限循环。
    • 可以使用 break 关键字退出循环。
    • loop 也可以用作表达式,返回一个值。

“`rust
fn main() {
let mut counter = 0;

loop {
    println!("loop!");
    counter = counter + 1;
    if counter == 10 {
        break; // 当 counter 等于 10 时退出循环
    }
}
println!("loop 结束");

// loop 作为表达式返回值
let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2; // break 后面可以带一个表达式,其值作为 loop 表达式的返回值
    }
};
println!("loop 表达式的返回值是:{}", result); // 输出 20

}
“`

  • while 循环:
    • 当一个条件为真时,while 循环会一直执行循环体内的代码。

“`rust
fn main() {
let mut number = 3;

while number != 0 {
    println!("{}!", number);
    number = number - 1;
}
println!("while 循环结束!");

}
“`

  • for 循环:
    • for 循环用于遍历集合(如数组)中的元素,或者遍历一个范围。它是 Rust 中最常用的循环类型,因为它比 while 循环更安全,可以避免越界等错误。

“`rust
fn main() {
let a = [10, 20, 30, 40, 50];

// 遍历数组元素
for element in a {
    println!("数组元素的值是:{}", element);
}

// 遍历范围
// (1..4) 是一个 Range 表达式,表示从 1 到 4 (不包含 4)
// rev() 方法用于反转范围
for number in (1..4).rev() {
    println!("{}!", number);
}
println!("for 循环结束!");

}
“`

for 循环遍历集合或范围时,Rust 会确保你不会访问到无效的索引,这比使用 while 循环手动管理索引更加安全和方便。

8. 初探所有权(Ownership)

所有权系统是 Rust 最独特也是最重要的概念之一。它使得 Rust 能够在没有垃圾回收器的情况下实现内存安全。理解所有权对于编写正确的 Rust 代码至关重要,但对于初学者来说可能需要一些时间来适应。

在本入门篇中,我们只会对所有权做一个非常基础的概念性介绍。深入理解所有权、借用和生命周期需要单独花费时间学习,并且是掌握 Rust 的关键。

所有权规则

Rust 的所有权系统基于以下三个规则:

  1. 每个值都有一个被称为其所有者(owner)的变量。
  2. 在任何时间,一个值只有一个所有者。
  3. 当所有者(变量)离开作用域时,该值将被丢弃(dropped)。

示例说明

让我们通过一些简单的例子来理解这些规则:

“`rust
fn main() {
// 规则 1: 每个值都有一个所有者变量
let s1 = String::from(“hello”); // s1 是字符串 “hello” 的所有者

// 规则 2 & 3: 转移所有权 (Move)
let s2 = s1; // 所有权从 s1 转移给了 s2
// println!("{}, world!", s1); // 这行会导致编译错误,因为 s1 不再拥有值
println!("{}, world!", s2); // s2 现在是所有者,可以访问值

// 再次转移所有权 (传递给函数)
takes_ownership(s2); // s2 的所有权转移给了 takes_ownership 函数内部的 some_string
// println!("{}, world!", s2); // 这行也会导致编译错误,s2 的所有权已转移且被丢弃

// 复制 (Copy)
// 对于像整数这样的基本类型,它们的复制是廉价且固定大小的,
// Rust 会自动进行复制而不是转移所有权
let x = 5; // x 是 5 的所有者
let y = x; // y 是 x 的一个副本,它们各自拥有自己的值 5
println!("x = {}, y = {}", x, y); // x 和 y 都仍然有效

makes_copy(x); // x 的一个副本传递给了 makes_copy,x 本身仍然有效
println!("x 仍然有效: {}", x);

// 返回值也会转移所有权
let s3 = gives_ownership(); // gives_ownership 将其内部创建的 String 的所有权转移给了 s3
println!("s3 的值是: {}", s3);

let s4 = String::from("world"); // s4 是 String 的所有者
let s5 = takes_and_gives_back(s4); // s4 的所有权转移给函数,函数再转移所有权给 s5
// println!("s4 的值是: {}", s4); // 编译错误,s4 所有权已转移
println!("s5 的值是: {}", s5);

} // main 函数作用域结束,s3, s5 被丢弃 (因为它们是所有者)

// — 函数 —

fn takes_ownership(some_string: String) { // some_string 进入作用域
println!(“takes_ownership 接收到: {}”, some_string);
} // some_string 离开作用域,其值被丢弃 (drop)

fn makes_copy(some_integer: i32) { // some_integer 进入作用域 (是复制过来的)
println!(“makes_copy 接收到: {}”, some_integer);
} // some_integer 离开作用域

fn gives_ownership() -> String { // gives_ownership 会将返回值的所有权转移出去
let some_string = String::from(“hello from gives_ownership”); // some_string 进入作用域
some_string // 返回 some_string,其所有权转移给调用者
} // some_string 离开作用域,但其值已被转移

fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string,其所有权转移给调用者
} // a_string 离开作用域,但其值已被转移
“`

在这个例子中,你可以看到当 String 类型的值被赋给另一个变量或传递给函数时,所有权会发生转移(move)。一旦所有权转移,原来的变量就不能再访问该值了,以防止双重释放(double free)等问题。对于实现了 Copy trait 的基本类型(如整数、布尔值、固定大小数组等),赋值或函数参数传递时会进行复制而不是转移所有权。

所有权系统的核心思想是: 在任何时候,只有一个变量可以“拥有”一段内存资源。当这个所有者变量不再存在时,Rust 会自动释放这段内存。这消除了手动管理内存的负担,同时通过编译期的所有权检查保证了内存安全。

借用(Borrowing)

如果你不想转移所有权,只是想暂时使用一个值,可以使用引用(reference)。使用引用作为函数参数或赋给新变量称为借用。借用是 Rust 中另一种重要的概念,它允许你访问数据而无需获得其所有权。

“`rust
fn main() {
let s1 = String::from(“hello”);

// 使用引用借用 s1,不转移所有权
// &s1 创建一个对 s1 的引用 (借用)
// calculate_length 函数接收一个引用作为参数
let len = calculate_length(&s1);

println!("字符串 '{}' 的长度是 {}", s1, len); // s1 仍然有效

}

// 函数接收一个 String 的引用,不获取所有权
fn calculate_length(s: &String) -> usize { // s 是一个 String 的引用 (&String)
s.len() // len() 方法获取字符串长度
} // s 离开作用域,但因为它只是一个引用,所以它指向的值 (s1) 不会被丢弃
“`

可变借用: 默认引用是不可变的。如果你需要修改借用的值,可以使用可变引用 &mut T。但是,Rust 有一条重要的规则:在特定作用域内,对于特定数据,只能有一个可变引用,或者任意数量的不可变引用,但不能同时拥有可变引用和不可变引用。 这就是 Rust 如何在编译时防止数据竞争的。

“`rust
fn main() {
let mut s = String::from(“hello”); // 需要先将 s 声明为可变

// 传递可变引用
change(&mut s);

println!("修改后的字符串:{}", s);

// 错误示例:不能同时拥有可变引用和不可变引用
let mut s2 = String::from("hello");

let r1 = &s2; // 不可变引用
// let r2 = &mut s2; // 这会导致编译错误!不能在有不可变引用 r1 的同时创建可变引用 r2
println!("r1: {}", r1); // 使用 r1,r1 的作用域在这里结束

let r3 = &mut s2; // 现在可以创建可变引用了,因为 r1 的作用域已结束
println!("r3: {}", r3);

// 错误示例:不能在同一作用域内创建多个可变引用
// let mut s3 = String::from("hello");
// let r4 = &mut s3;
// let r5 = &mut s3; // 这会导致编译错误!

let mut s3 = String::from("hello");
{
    let r4 = &mut s3;
    println!("{}", r4);
} // r4 在这里离开作用域
let r5 = &mut s3; // 现在可以创建新的可变引用 r5 了
println!("{}", r5);

}

fn change(some_string: &mut String) { // 接收一个 String 的可变引用 (&mut String)
some_string.push_str(“, world”); // 可以修改借用的字符串
}
“`

所有权、借用和生命周期是 Rust 最具挑战性但也最有价值的部分。它们是 Rust 安全性的基石。在入门阶段,理解这些概念可能需要反复练习和查阅资料。记住,Rust 编译器的错误提示通常非常有帮助,它们会告诉你为什么你的代码违反了所有权规则,以及如何修改。

9. 结语与下一步

恭喜你!你已经走过了学习 Rust 的第一步。在这篇入门篇中,我们一起:

  • 了解了 Rust 的核心优势和学习价值。
  • 安装了 Rust 开发环境并验证了安装。
  • 编写并运行了你的第一个“Hello, World!”程序。
  • 理解了 Rust 的编译过程。
  • 掌握了 Rust 的构建工具和包管理器 Cargo 的基本用法。
  • 学习了 Rust 的基本概念,包括变量、可变性、数据类型(标量和复合)、函数和控制流程。
  • 对 Rust 独特的所有权系统有了初步的概念性了解。

这只是 Rust 世界的冰山一角。要真正掌握 Rust 并用它来构建实用的应用程序,你还需要深入学习更多内容,包括:

  • 所有权、借用和生命周期: 这是 Rust 最核心的部分,需要投入更多时间去理解其规则和实践。
  • 结构体(Structs)和枚举(Enums): 用于创建更复杂的数据结构。
  • 匹配(match)控制流运算符: 处理模式匹配,非常强大和常用。
  • 模块系统: 如何组织大型 Rust 项目。
  • 包(Crates)、模块(Modules)和 use 关键字: 管理依赖和代码可见性。
  • 错误处理: Rust 推荐使用 Result<T, E>Option<T> 来处理可恢复错误。
  • Traits: Rust 实现多态和共享行为的方式,类似于其他语言的接口或抽象类。
  • 泛型(Generics): 编写可以处理多种类型数据的灵活代码。
  • 测试(Testing): 如何编写和运行单元测试、集成测试。
  • 标准库(Standard Library): 学习常用的数据结构(如 Vec, HashMap)和功能(如文件 I/O,网络编程)。
  • 并发编程: Rust 提供的线程、消息传递、共享状态并发工具。
  • 不安全 Rust(Unsafe Rust): 在特定场景下放弃 Rust 安全保证以获得底层控制(通常只有在必要时才使用)。

推荐的下一步学习资源:

  • The Rust Programming Language (第二版),简称“The Book”: 这是 Rust 官方的免费在线书籍,是学习 Rust 最权威和完整的资源。你可以在 https://doc.rust-lang.org/book/ 找到它。建议从头开始阅读,尤其是关于所有权、借用和生命周期的章节。
  • Rust By Example: 通过大量的小代码示例来展示 Rust 的各种特性。网址:https://doc.rust-lang.org/rust-by-example/
  • Rustlings: 一个通过解决小型练习来学习 Rust 的交互式教程。网址:https://github.com/rust-lang/rustlings
  • 官方文档: Rust 拥有非常详细和高质量的官方文档。

学习一门新语言需要时间和实践。Rust 的学习曲线可能比一些解释型语言陡峭,但请不要气馁。Rust 社区非常活跃和乐于助人,当你遇到困难时,可以查阅官方文档、搜索网络资源,或者在 Rust 社区寻求帮助(如官方 Discord 频道、论坛或 Stack Overflow)。

从编写小型程序开始,逐步挑战更复杂的项目,通过实践来巩固你学到的知识。每一次编译错误都是一次学习的机会,Rust 编译器会引导你写出更安全、更高效的代码。

祝你在 Rust 的学习旅程中一切顺利!

发表评论

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

滚动至顶部