从零开始学习 Rust:入门篇
欢迎来到 Rust 的世界!
或许你已经听说过 Rust 的大名——它以其卓越的性能、可靠性和开发效率而闻名。在系统编程、WebAssembly、命令行工具、网络服务等领域,Rust 正变得越来越受欢迎。对于许多开发者来说,Rust 提供了一种在追求极致性能的同时,也能享受到强大安全保障的编程体验,尤其是其独特的内存安全保证,无需垃圾回收(GC)即可避免常见的内存错误(如空指针解引用、数据竞争等)。
如果你是一名对系统编程感兴趣、希望编写高性能代码、或者只是想学习一门设计哲学与众不同的现代化语言的初学者,那么 Rust 绝对值得你投入时间。
这篇入门指南将带领你从零开始,了解 Rust 是什么,为什么要学习它,以及如何安装 Rust 开发环境,编写你的第一个 Rust 程序,并初步掌握 Rust 的一些核心概念。我们将尽量详细地讲解每一个步骤,确保即使是编程新手也能顺利入门。
1. 为什么选择学习 Rust?
在深入学习之前,我们先来聊聊 Rust 的吸引力所在。为什么这么多开发者对 Rust 趋之若鹜?
- 性能卓越: Rust 是一门编译型语言,编译后的代码运行速度极快,与 C++ 等语言相当。它提供了对底层硬件的精细控制,没有运行时开销(比如垃圾回收),非常适合编写对性能要求极高的应用,如操作系统内核、游戏引擎、嵌入式系统、高性能服务器等。
- 内存安全(无 GC): 这是 Rust 最具特色的卖点。通过其创新的所有权(Ownership)、借用(Borrowing)和生命周期(Lifetimes)系统,Rust 在编译期就能检查并预防 C/C++ 中常见的内存安全问题,如悬垂指针(Dangling Pointers)、空指针解引用(Null Pointer Dereferencing)和数据竞争(Data Races)。这一切都在不引入垃圾回收器(GC)的情况下实现,这意味着你可以获得 C/C++ 的性能,同时避免大量的内存安全 bug。
- 强大的并发性: Rust 的所有权系统天然地支持安全并发编程。在编译期,Rust 就能检测到可能导致数据竞争的并发访问,从而让你在编写多线程程序时更加自信和安全。
- 可靠性高: 除了内存安全,Rust 强大的类型系统和模式匹配等特性,有助于捕获更多类型的 bug,使得代码更加健壮和可靠。Rust 编译器以其严格而闻名,虽然有时会让新手感到挫败,但它更像是一位严格的导师,帮助你编写出高质量的代码。
- 友好的开发工具: Rust 配备了一流的工具链,特别是
Cargo
,它是 Rust 的构建工具和包管理器。Cargo 让创建项目、管理依赖、编译、测试和运行代码变得异常简单,极大地提升了开发效率。 - 跨平台: Rust 代码可以编译到多种平台,包括 Windows、macOS、Linux 以及各种嵌入式系统和 WebAssembly。
- 活跃的社区和生态系统: Rust 拥有一个充满活力、乐于助人且不断成长的社区。Rust 的标准库功能强大,同时 Crates.io(Rust 的官方包注册中心)上有海量的第三方库(称为“crate”),覆盖了各种应用领域。
总而言之,Rust 旨在提供一种既能充分利用硬件性能,又能保证代码安全和可靠性的编程体验。学习 Rust 可能会有挑战,但它带来的回报是巨大的。
2. 环境搭建:安装 Rust
学习任何编程语言的第一步都是安装开发环境。Rust 的安装非常简单,主要依赖于一个官方工具 rustup
。rustup
是一个 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
时,编译器会进行以下工作:
- 解析(Parsing): 读取你的源代码,检查语法是否符合 Rust 的规则,并构建一个抽象语法树(AST)。
- 类型检查和借用检查: 这是 Rust 独有的重要阶段。编译器会在这里检查变量的类型是否匹配,以及所有权、借用和生命周期规则是否被遵守。这个阶段会捕获大量的潜在运行时错误,包括内存安全问题和数据竞争。如果存在任何错误,编译器会在这里停止并输出详细的错误信息,提示你如何修改。
- 代码生成(Code Generation): 如果代码通过了所有检查,编译器会将 AST 转换为中间表示(IR),然后使用 LLVM 后端将 IR 优化并生成目标平台的机器代码。
- 链接(Linking): 将生成的机器代码与 Rust 标准库以及你程序可能依赖的任何其他库的代码合并,最终生成一个独立的可执行文件。
这个编译过程是 Rust 保证高性能和安全的关键。虽然编译可能需要一些时间(尤其是对于大型项目),但它换来了运行时的高效率和稳定性。
5. Rust 的构建工具和包管理器:Cargo
虽然可以使用 rustc
直接编译简单的单个文件程序,但对于任何稍微复杂一点的 Rust 项目,你都会需要使用 Cargo
。Cargo
是 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 推断整数类型为
i32
,usize
或isize
用于索引。 - 可以使用后缀指定类型,例如
57u8
。
- 有符号整数:
-
浮点数类型(Floating-Point Types):
f32
(单精度)f64
(双精度)- 默认情况下,Rust 推断浮点类型为
f64
(在现代 CPU 上速度与f32
相似但精度更高)。
-
布尔类型(Boolean Type):
bool
类型,有两个可能的值:true
和false
。- 通常在条件语句中使用。
-
字符类型(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 关键字也可以
}
“`
注意在 five
和 plus_one
函数中,最后一行 5
和 x + 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 的所有权系统基于以下三个规则:
- 每个值都有一个被称为其所有者(owner)的变量。
- 在任何时间,一个值只有一个所有者。
- 当所有者(变量)离开作用域时,该值将被丢弃(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 的学习旅程中一切顺利!