Rust 编程语言核心主题详解
Rust 是一门着重于安全 (Safety)、性能 (Performance) 和并发 (Concurrency) 的现代系统编程语言。它旨在解决 C/C++ 等传统系统语言中常见的内存安全问题,同时又保持了零开销抽象和裸机控制的能力。Rust 通过其独特的所有权 (Ownership) 系统、借用 (Borrowing) 和生命周期 (Lifetimes) 规则,在编译时强制执行内存安全,无需垃圾回收器,从而避免了数据竞争和空指针解引用等常见错误。
核心思想:在保证与 C/C++ 匹敌性能的同时,通过严格的编译时检查(所有权系统)来消除内存安全漏洞和数据竞争,使开发者可以专注于业务逻辑而非底层内存管理。
一、变量和可变性 (Variables and Mutability)
Rust 的变量绑定默认是不可变的,这鼓励开发者编写更安全、更易于理解的代码。
1.1 let 绑定
使用 let 关键字声明的变量默认是不可变的 (immutable)。一旦绑定了一个值,就不能再改变它。
1 | fn main() { |
1.2 mut 关键字
如果需要使变量可变,可以使用 mut 关键字。
1 | fn main() { |
1.3 常量 (Constants)
常量使用 const 关键字声明,并且必须显式地指定类型。常量在程序运行的整个生命周期中都有效,可以声明在任何作用域,包括全局作用域。常量不能使用 mut。
1 | const MAX_POINTS: u32 = 100_000; // 常量名通常采用全大写和下划线 |
const 和 let 的主要区别:
const必须显式注明类型,let可以根据值推断类型。const不能使用mut。const可以在任意作用域声明,包括全局。const只能绑定到常量表达式,不能是函数调用的结果或任何在运行时计算的值。
1.4 遮蔽 (Shadowing)
Rust 允许使用相同名称声明新变量,这会“遮蔽” (shadow) 掉前一个同名变量。这意味着在当前作用域内,新变量将覆盖旧变量。遮蔽与可变性不同,它可以改变变量的类型。
1 | fn main() { |
2.2 复合类型 (Compound Types)
复合类型可以将多个值组合成一个类型:
元组 (Tuples):
- 元组可以将多种不同类型的值组合在一个固定大小的集合中。
- 一旦声明,元组的长度是固定的。
- 可以通过模式匹配或者索引来访问元组的元素。
1
2
3
4
5
6
7
8
9fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // 模式匹配解构
println!("The value of y is: {}", y); // 输出 6.4
let five_hundred = tup.0; // 通过索引访问
let six_point_four = tup.1;
let one = tup.2;
}数组 (Arrays):
- 数组可以将多个相同类型的值组合在一个固定大小的集合中。
- 数组的长度在编译时是已知的,一旦声明,长度不可改变。
- 如果需要可变长的列表,应使用
Vec(向量),这是一种标准库集合类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15fn main() {
let a = [1, 2, 3, 4, 5]; // 隐式类型推断和长度
let months: [&str; 12] = [ // 显式类型 [类型; 长度]
"January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"
];
let b = [3; 5]; // 等同于 [3, 3, 3, 3, 3]
let first = a[0]; // 通过索引访问
let second = a[1];
// 访问越界会发生运行时错误并 panic
// let index = 10;
// let element = a[index]; // 运行时 panic: index out of bounds
}
三、函数与作用域 (Functions and Scope)
Rust 代码通常通过函数组织。函数也是作用域的边界。
3.1 函数定义
使用 fn 关键字定义函数。函数可以接收参数,也可以返回一个值。
1 | fn main() { |
3.2 表达式与语句 (Expressions vs Statements)
- 语句 (Statements):执行一个动作,但不返回值。例如
let y = 6;。 - 表达式 (Expressions):求值并产生一个值。例如
5 + 6,函数调用,以及代码块。
在 Rust 中,函数体由一系列语句组成,并可选地以一个表达式结尾。表达式末尾没有分号,加分号会将其转换为语句,使其不再返回值。
1 | fn main() { |
3.3 作用域 (Scope)
Rust 拥有块级作用域。变量在声明它们的代码块内是有效的,并在块结束时超出作用域。
1 | fn main() { |
注意:if 表达式的每个分支返回的类型必须相同。
4.2 loop 循环
loop 关键字创建一个无限循环。可以使用 break 关键字退出循环,或者使用 continue 跳过当前迭代。loop 也可以返回一个值。
1 | fn main() { |
4.4 for 循环
for 循环用于遍历集合中的元素。它是 Rust 中最常用的循环结构,安全且简洁。
1 | fn main() { |
五、所有权与借用 (Ownership and Borrowing)
所有权是 Rust 最独特且最重要的特性,它使得 Rust 无需垃圾回收器即可保证内存安全。
5.1 所有权规则
- 每个值都有一个所有者 (owner)。
- 值在任何时候只能有一个所有者。
- 当所有者离开作用域时,值会被丢弃 (drop)。
5.2 所有权转移 (Move 语义)
当一个值被赋值给另一个变量或作为参数传递给函数时,所有权会发生转移。
对于栈上数据(如整数、布尔值、固定大小数组),因为 Copy Trait 的实现,它们会被复制而不是移动。
1 | fn main() { |
5.3 克隆 (Cloning)
如果希望复制堆上的数据而不是转移所有权,可以使用 clone 方法。
1 | fn main() { |
六、引用与切片 (References and Slices)
6.1 引用 (References)
& 符号表示引用,它允许你在不获取所有权的情况下使用值。这种行为称为借用 (borrowing)。引用本身是不可变的,但它们指向的值可以是不可变或可变的。
借用规则 (Borrowing Rules):
在任何给定时间,你只能拥有:
- 一个可变引用 (mutable reference),或者
- 任意数量的不可变引用 (immutable references)。
引用必须始终是有效的。
不可变引用
&T:- 可以通过
&创建。 - 允许读取数据,但不允许修改数据。
- 可以有多个不可变引用同时存在。
1
2
3
4
5
6
7
8
9fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 传递不可变引用
println!("The length of '{}' is {}.", s, len); // s 仍然有效
}
fn calculate_length(s: &String) -> usize { // s 是对一个 String 的引用
s.len()
} // s 离开作用域,但它没有所有权,所以不会 drop s- 可以通过
可变引用
&mut T:- 可以通过
&mut创建。 - 允许读取和修改数据。
- 在特定作用域内,只能有一个可变引用存在。不能与任何其他引用(不可变或可变)同时存在。
1
2
3
4
5
6
7
8
9
10
11
12
13fn main() {
let mut s = String::from("hello");
change(&mut s); // 传递可变引用
println!("{}", s); // 输出 "hello, world"
let r1 = &mut s;
// let r2 = &mut s; // 编译错误:cannot borrow `s` as mutable more than once at a time
// println!("{}, {}", r1, r2); // 错误发生在引用使用时
}
fn change(some_string: &mut String) { // some_string 是对一个可变 String 的引用
some_string.push_str(", world");
}- 可以通过
悬垂引用 (Dangling References):
- Rust 编译器会在编译时防止悬垂引用,即指向已被释放内存的引用。
1
2
3
4// fn dangle() -> &String { // 编译错误:this function's return type contains a borrowed value, but there is no value for it to borrow
// let s = String::from("hello"); // s 在 dangle 内部创建
// &s // 返回 s 的引用
// } // s 在这里超出作用域并被 drop,其内存被释放。但是我们试图返回一个指向该内存的引用。正确做法是返回
String本身(转移所有权)。
6.2 切片 (Slices)
切片是对集合中一部分连续元素的引用,它在不获取所有权的情况下引用数据。
字符串切片
&str:- 表示对
String的一部分的引用。 &str是不可变的。
1
2
3
4
5
6
7
8
9
10
11
12
13fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // 从索引 0 开始到(不包含)索引 5
let world = &s[6..11];
println!("{} {}", hello, world);
// 获取整个字符串的切片
let whole_slice = &s[..];
println!("{}", whole_slice);
// 字面量字符串是 &str 类型
let s_literal = "hello world"; // s_literal 的类型是 &str
}- 表示对
数组切片
&[T]:- 对数组或其他集合中连续元素的引用。
1
2
3
4
5fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // slice 的类型是 &[i32]
assert_eq!(slice, &[2, 3]);
}
切片让我们可以安全高效地操作集合的一部分。
七、结构体 (Struct) 和方法 (Method)
结构体是一种自定义复合数据类型,允许我们将相关联的数据打包成一个有意义的结构。
7.1 定义和实例化结构体
结构体是用户自定义的类型。可以定义带命名字段的结构体。
1 | struct User { |
7.2 元组结构体 (Tuple Structs)
元组结构体是具有元组特点的结构体,但有名称。
1 | struct Color(i32, i32, i32); // 具名元组 |
7.3 单元结构体 (Unit-Like Structs)
单元结构体不包含任何字段,通常用于需要实现某个 Trait 但不需要存储任何数据的场景。
1 | struct AlwaysEqual; |
7.4 方法 (Methods)
方法是与结构体(或枚举)关联的函数,并且其第一个参数必须是 self、&self 或 &mut self。
使用 impl 块来定义方法。
&self:方法借用所有者(不可变)。&mut self:方法可变地借用所有者。self:方法获取所有权,所有者在方法结束时被 Drop。
1 | struct Rectangle { |
八、枚举与模式匹配 (Enums and Pattern Matching)
8.1 枚举 (Enums)
枚举允许你通过定义一个类型,使其拥有一组可能的值。Rust 的枚举是强大的,它们可以存储数据。
1 | enum IpAddrKind { |
Option 枚举:
Rust 标准库中定义了一个非常有用的枚举 Option<T>,用于处理值可能存在或不存在的场景,避免了空指针的危险。
1 | enum Option<T> { // 由标准库定义 |
if let 语法:if let 是一种处理 match 表达式只关心一个匹配模式的简写方式。
1 | fn main() { |
九、常见集合类型及常用操作
Rust 标准库提供了几种常用的集合类型来存储数据。与内置数组不同,这些集合存储在堆上,并且可以根据需要增长或缩小。
9.1 Vec<T> (向量)
Vec<T> 是一个可变长的同类型列表。
它类似于其他语言中的动态数组或 ArrayList。
1 | fn main() { |
9.2 String
String 是一个可变的、堆分配的、UTF-8 编码的字符串类型。它是 Vec<u8> 的一个封装,并保证存储的是有效的 UTF-8 序列。
1 | fn main() { |
注意:String 不支持像 C 语言那样直接通过索引访问字符,因为 UTF-8 编码的字符长度不固定,索引操作不明确。如果需要,应使用 chars() 迭代器。
9.3 HashMap<K, V> (哈希映射)
HashMap<K, V> 存储键值对,键和值可以是不同类型。它通过哈希函数将键映射到内存位置,实现快速查找。
1 | use std::collections::HashMap; |
注意:对于 HashMap,键和值的所有权会被移动到 HashMap 中。如果想要使用引用,必须确保引用是有效的。
十、模块系统和包管理 (Module System and Package Management)
Rust 的模块系统和包管理工具 Cargo 紧密相关,它们共同协作帮助组织和管理 Rust 项目。
10.1 模块 (Modules)
模块允许你将代码组织成组,提高可读性和重用性。模块可以嵌套。
mod关键字:用于定义模块。pub关键字:使项(函数、结构体、枚举、模块等)对外可见。默认情况下,所有项都是私有的。use关键字:将模块中的路径引入当前作用域,以便更方便地使用。
1 | // main.rs |
10.2 包 (Packages) 和 Crate
- Crate (包):Rust 编译器的编译单元。
- 二进制 Crate (Binary Crate):编译生成可执行文件(如
main.rs)。 - 库 Crate (Library Crate):编译生成库文件(如
lib.rs)。
- 二进制 Crate (Binary Crate):编译生成可执行文件(如
- Package (包):包含一个或多个 Crate(最多一个库 Crate,任意数量的二进制 Crate)以及一个
Cargo.toml文件,描述如何构建这些 Crate。
10.3 Cargo
Cargo 是 Rust 的构建系统和包管理器。它负责:
- 创建项目:
cargo new <project_name>。 - 构建项目:
cargo build。 - 运行项目:
cargo run。 - 测试项目:
cargo test。 - 管理依赖:在
Cargo.toml中声明依赖,Cargo会自动下载和编译。
Cargo.toml 文件示例:
1 | [package] |
十一、错误处理 (Error Handling)
Rust 对错误处理采取了严格的态度,区分了可恢复错误和不可恢复错误。
11.1 可恢复错误 (Recoverable Errors) - Result<T, E>
可恢复错误(如文件未找到、网络连接中断)是预料之中且可以处理的。Rust 使用 Result<T, E> 枚举来处理这类错误。
1 | enum Result<T, E> { |
处理 Result 的常用方法:
match表达式:最彻底和灵活的方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19use std::{fs::File, io::ErrorKind};
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error);
}
},
};
// greeting_file 是一个 File 类型
}unwrap()和expect():简写方式,如果Result是Err则直接panic!。expect()允许提供自定义 panic 消息。它们不应用于生产代码中的可恢复错误。1
2
3// let greeting_file = File::open("hello.txt").unwrap(); // 如果是 Err 会直接 panic
// let greeting_file = File::open("hello.txt")
// .expect("hello.txt should be included in this project"); // 带消息的 panic?运算符:用于传播错误,是match表达式的语法糖。只能在返回Result的函数中使用。如果Result是Err,它会立即从当前函数返回Err;否则,它会解包Ok中的值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15use std::{fs::File, io::{self, Read}};
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?; // 如果 File::open 返回 Err,则此函数返回 Err
let mut s = String::new();
f.read_to_string(&mut s)?; // 如果 read_to_string 返回 Err,则此函数返回 Err
Ok(s) // 成功则返回 Ok(s)
}
// 更简洁的写法 (链式调用)
fn read_username_from_file_chaining() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
11.2 不可恢复错误 (Unrecoverable Errors) - panic!
不可恢复错误(如数组越界、逻辑错误)表示程序处于无法挽回的错误状态,通常需要终止程序。Rust 使用 panic! 宏来处理这类错误。
当 panic! 发生时,程序会默认展开 (unwind) 栈并清理数据,然后退出。也可以配置为直接终止 (abort) 进程,这会使二进制文件更小。
1 | fn main() { |
十二、泛型 (Generics)
泛型允许你编写更抽象、可重用的代码,它可以在编译时适用于多种类型。
12.1 函数中的泛型
1 | fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { // T 必须实现 PartialOrd 和 Copy Trait |
12.2 结构体中的泛型
1 | struct Point<T> { // 结构体字段可以使用泛型类型 |
12.3 枚举中的泛型
Option<T> 和 Result<T, E> 本身就是泛型枚举的经典例子。
1 | enum MyOption<T> { |
十三、Trait 与 Trait Bound
13.1 Trait (特性)
Trait 是 Rust 的核心特性之一,它定义了共享行为的接口。Trait 可以看作是其他语言中的接口 (interface) 或抽象基类 (abstract base class),但 Rust 的 Trait 更灵活。
定义 Trait:
1
2
3
4
5
6
7
8pub trait Summary {
fn summarize(&self) -> String; // Trait 方法签名
// 可以提供默认实现
fn summarize_author(&self) -> String {
String::from("Rustacean")
}
}为类型实现 Trait:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle { // 为 NewsArticle 实现 Summary Trait
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
// summarize_author 使用默认实现
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet { // 为 Tweet 实现 Summary Trait
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
// summarize_author 方法可以被覆盖
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
13.2 Trait Bounds (特性约束)
Trait Bounds 用于对泛型类型参数施加约束,确保它们实现了特定的 Trait,从而保证在泛型代码内部可以使用这些 Trait 定义的方法。
在函数中使用 Trait Bound:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 语法糖:impl Trait
pub fn notify(item: &impl Summary) { // 任何实现了 Summary Trait 的类型都可以作为参数
println!("Breaking news! {}", item.summarize());
}
// 等价于更长的 Trait Bound 语法
pub fn notify_verbose<T: Summary>(item: &T) { // T 必须实现 Summary Trait
println!("Breaking news! {}", item.summarize());
}
// 多个 Trait Bound
pub fn notify_multiple(item: &(impl Summary + Display)) { /* ... */ } // item 须实现 Summary 和 Display
// 泛型类型参数的多个 Trait Bound
pub fn notify_multiple_verbose<T: Summary + Display>(item: &T) { /* ... */ }
// where 从句简化复杂的 Trait Bound
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
// ...
42
}
13.3 Trait 作为返回类型
函数可以返回实现了 Trait 的类型。
1 | fn returns_summarizable() -> impl Summary { // 返回实现了 Summary 的类型(但调用者不知具体类型) |
13.4 Newtype 模式和孤儿规则 (Orphan Rule)
为了避免破坏现有的类型,Rust 有一个“孤儿规则”:只能为你 crates 中的类型实现 Trait,或者为你的 Trait 在你的 crates 中实现类型。这意味着不能为外部类型实现外部 Trait。
当需要为外部类型(如 Vec<i32>)实现外部 Trait (如 Display) 时,可以使用 Newtype 模式:将外部类型封装在新结构体中。
1 | struct MyVec(Vec<i32>); // 新类型 MyVec 拥有 Vec<i32> |
十四、生命周期 (Lifetimes)
生命周期是 Rust 编译器确保所有引用在程序执行期间都有效的一种机制。它们是编译器在编译时进行静态分析的工具,不涉及运行时开销。
14.1 核心概念
- 借用检查器 (Borrow Checker):Rust 编译器的一部分,负责比较作用域来确定所有借用都是有效的。
- 生命周期注解 (Lifetime Annotations):用
'a、'b等表示,用于告诉借用检查器引用之间生命周期的关系。 - 生命周期省略规则 (Lifetime Elision Rules):在许多情况下,Rust 编译器可以自动推断生命周期,不需要手动注解。
14.2 函数中的生命周期注解
当函数参数是引用,并且返回值也是引用时,需要生命周期注解来明确输入引用和输出引用之间的生命周期关系。
1 | // 'a 是生命周期参数,它表示返回的引用将与两个输入引用中生命周期较短的那个保持一致 |
14.3 结构体中的生命周期注解
如果结构体包含引用,那么结构体的定义需要生命周期注解。
1 | struct ImportantExcerpt<'a> { // 结构体包含一个引用,其生命周期为 'a |
14.4 静态生命周期 'static
'static 是一个特殊的生命周期,表示引用在整个程序的生命周期中都是有效的。字符串字面量拥有 'static 生命周期。
1 | let s: &'static str = "I have a static lifetime."; |
十五、常用标准库函数与实用宏
Rust 的标准库 (std) 提供了大量实用的函数、宏和类型。
15.1 常用类型与 Trait
std::collections:Vec,HashMap,BTreeMap,HashSet,BTreeSet,LinkedList等。std::io:输入输出操作,如File,BufReader,stdin(),stdout()。std::fs:文件系统操作,如File::open,fs::read_to_string。std::fmt:格式化输出相关的 Trait (Display,Debug) 和宏 (format!,print!,println!)。std::env:访问环境变量和命令行参数。std::path:路径操作。std::time:时间相关的类型和函数。
15.2 实用宏 (Macros)
Rust 宏在编译时扩展代码,提供了元编程能力。
println!/eprintln!:用于打印到标准输出/标准错误。dbg!:在调试时打印表达式的值和文件名、行号等,并返回表达式本身。非常方便。1
2
3
4
5fn main() {
let x = 5;
let y = dbg!(x * 2); // 打印 "src/main.rs:4:13: 10" 然后 y = 10
println!("y = {}", y);
}vec!:创建Vec的宏。assert!/assert_eq!/assert_ne!:测试宏,用于检查条件是否为 true,或者比较两个值是否相等/不相等。panic!:用于不可恢复错误,终止程序。format!:格式化字符串并返回String。1
2let name = "Alice";
let greeting = format!("Hello, {}!", name); // greeting 是一个 Stringinclude_str!/include_bytes!:在编译时将文件内容作为字符串/字节数组嵌入到二进制文件中。1
const MY_FILE_CONTENT: &str = include_str!("my_data.txt"); // 将文件内容编译进程序
十六、Rust 中的异步编程 (Async/Await)
Rust 通过 async/await 语法和 Future Trait 提供了零开销的异步编程模型,无需操作系统线程即可实现高效的并发。
16.1 核心概念
FutureTrait:Future表示一个异步计算,它可能尚未完成。当Future被poll方法调用时,它会尝试执行一些工作并返回Poll::Pending(如果未完成)或Poll::Ready(T)(如果已完成并返回一个值T)。async关键字:用于定义异步函数或异步块。async fn返回一个impl Future类型。await关键字:用于暂停异步函数的执行,直到一个Future完成,并获取其结果。await只能在async fn或async块中使用。- Executor (执行器):负责
pollFuture,驱动异步任务前进。标准库不提供执行器,需要依赖第三方runtime,如Tokio,async-std。
16.2 基本结构
1 | // main.rs |
Cargo.toml 依赖示例:
1 | [dependencies] |
上述示例使用了 tokio 运行时。
16.3 异步编程的优点
- 高并发性:允许程序在单个线程上同时处理大量 I/O 密集型任务,而不需要创建大量操作系统线程(每个线程都有自己的内存和上下文切换开销)。
- 高效利用资源:当任务等待 I/O 完成时,CPU 不会空闲,而是可以切换到其他等待执行的任务。
- 避免阻塞:通过
await非阻塞地等待操作完成,而不是阻塞整个线程。
16.4 异步编程的挑战
- 生态系统:需要选择合适的异步运行时 (runtime),如 Tokio 或 async-std。
- 学习曲线:理解
async/await、FutureTrait 和执行器的工作原理需要一定时间。 - 调试:异步代码的调用栈可能不那么直观,调试可能更具挑战性。
总结
Rust 是一门功能强大、设计精良的系统编程语言。通过其独特的所有权与借用系统,它在编译时提供了 C/C++ 的性能和低级控制,同时消除了内存安全问题。模式匹配使得处理枚举和复杂数据类型变得简洁而富有表现力。Trait 系统促进了代码的抽象和重用。生命周期机制在编译时保证了引用的有效性。而 Cargo 极大地简化了项目管理。
虽然 Rust 的学习曲线相对陡峭,特别是在理解所有权和生命周期方面,但其带来的内存安全保障、优秀的性能表现以及日渐成熟的异步编程能力,使其在网络服务、嵌入式系统、WebAssembly、命令行工具等众多领域展现出巨大的潜力,正在被越来越多的开发者和企业所采用。掌握这些核心主题是成为一名高效 Rust 程序员的关键。
