rust 复习
前言
由于近期想用 rust 开发点工具,但很久没有用过 rust 了,所以回来温习一下
C++ 和go 语言相比,rust,我更喜欢你
参考:https://github.com/chapin666/Learning-Rust
基础部分
rust 是什么?
rust 是一款系统级的编程语言,你可以理解为内存安全版的 C++,同时在语言层面支持高并发,它比 C++ 好学,比 go 语言难学
rust 的三大愿景
- 高安全
- 高性能
- 高并发
vscode 环境配置
- 下载 rust,里面自带 rust-analysis,顺便下载 code-runner,这个写代码很好用

- 根据自己的习惯调快捷方式

- 配置 code-runner 的 Custom Command,用起来顺手一些

hello world
- 终端输入 cargo init 创建项目
- 直接运行即可

宏
宏是 rust 的一项很强大的特性,它是一种在编译时生成代码的“元编程”工具,说简单点就是函数的高级版,至于它和函数的区别大概如下:
| 函数 | 宏 | |
|---|---|---|
| 调用方式 | sum(1, 2) | sum!(1, 2) |
| 执行时机 | 运行时 | 编译时(编译的时候生成代码) |
| 参数灵活性 | 参数的数量和类型固定 | 可以接受任意数量、任意类型的参数(如 println!) |
| 返回类型 | 必须是一个具体的类型 | 可以返回代码片段(甚至可以返回多个函数) |
| 实现方式 | 写在函数体中 | 使用模式匹配来解析输入的代码结构 |
宏大致可以分为声明宏和过程宏
声明宏你可以看作是一个高级的正则表达式,根据你定义的匹配模式来生成代码
示例:
1 | macro_rules! introduce { |
输出:
1 | name, age, country |
说完了声明宏我们来说说过程宏,过程宏更强大,也更复杂。它本质上是一个接收 Rust 代码作为输入、输出 Rust 代码的函数。过程宏分为三类:
| 类型 | 用途 | 常见例子 |
|---|---|---|
| 派生宏(Derive) | 为结构体或枚举自动实现某个 trait | #[derive(Debug, Clone, Serialize)] |
| 属性宏(Attribute) | 为函数、结构体添加额外行为 | #[get(“/“)]( Rocket 路由宏) |
| 函数式宏(Function-like) | 类似macro_rules!,但更灵活 | 如sql!(“SELECT* FROM users”) |
宏在我看来就类似于一个高级语法糖,适当使用可以减少代码量,提升代码灵活性,但是过量的话就会导致可读性下降、编译时间增加、错误信息复杂等问题
rust 的注释
1 | // 单行注释 |
rust 数据类型
- 定义一个字符串类型的变量
1 | let a_string = "this is a string"; |
- 定义各种类型的变量
1 | fn main() { |
上⾯面的代码中,我们并没有为每一个变量量指定它们的数据类型。
Rust 编译器器会⾃自动从 等号= 右边的值 中推断出该变量量的类型。例如 Rust 会自动将双引号起来的数据推断为 字符串,把 没有小数点的数 字自动推断为整型。把 true 或 false 值推断为布尔类型。
println!() 是一个宏,而不不是一个函数,区分函数和宏的唯一办法,就是看函数名 / 宏名最后有没有感叹号“!”,如果有感叹号则是宏,没有则是函数。
println!() 宏接受两个参数:
- 第一个参数是格式化符,一般是 {},如果是复杂类型,则是 {:?}。
- 第二个参数是变量量名或者常量量名。
以上的数据类型都是标量数据类型,也叫基础数据类型,只能存储单个值。
不可变变量和可变变量
rust 默认初始化的是只读变量,它是不可变的
1 | let a = "bbb"; // 不可更改 |
如果想要初始化一个可变变量,要加个 mut 关键字
1 | let mut a = "bbb"; |
rust 中的常量
rust 中定义常量有几个注意的点:
- 必须手动指定类型
- 常量名通常是大写字母
- 通常放在行首,且用 const 关键字
- 必须有初始化的值,且不可改变
- 不能出现同名常量
1 | const A: &str = "aaa"; |
rust 变量的隐藏 / 屏蔽
在 rust 中可以重新定义一个同名变量,这样前面的同名变量将会被隐藏 / 屏蔽
1 | fn main() { |
同时同名变量还支持定义不同类型
1 | fn main() { |
rust 字符串
Rust 提供了两种字符串
- 字符串字面量是 &str,它是 rust 内置的核心数据类型。
- 字符串对象 String,它不是 rust 核心的一部分,它只是标准库中的一个公开 pub 结构体。
字符串字面量 &str
字符串字面量在编译时就知道其是字符串类型,是 rust 核心的一部分,字符串字面量 &str 是字符的集合,被硬编码赋值给一个变量
字符串字面量称之为字符串切片,因为其底层实现就是切片。
1 | fn main() { |
字符串字面量是静态的,所以它将会保存到程序运行结束
因为其是静态的,所以默认会自动加 static 关键字,不过加了之后看着有点奇怪,所以日常我们可以忽略不加
1 | fn main() { |
字符串对象
在 rust 中字符串对象并不是 rust 核心的一部分,它只是标准库中定义的一个结构体
1 | pub struct String |
字符串对象是一个长度可变的集合,且底层使用 UTF-8 编码
字符串对象在堆中分配,可以在运行时提供相应的字符串操作方法
字符串对象常用方法
| 方法 | 原型 | 说明 |
|---|---|---|
| new() | pub const fn new() -> String | 创建一个新的字符串对象 |
| to_string() | fn to_string(&self) -> String | 将字符串字面量转换为字符串对象 |
| replace() | pub fn replace<’a, P>(&’a self, from: P, to: &str) -> String | 搜索指定模式并替换 |
| as_str() | pub fn as_str(&self) -> &str | 将字符串对象转换为字符串字面量 |
| push() | pub fn push(&mut self, ch: char) | 在字符串末尾追加字符 |
| push_str() | pub fn push_str(&mut self, string: &str) | 在字符串末尾追加字符串 |
| len() | pub fn len(&self) -> usize | 返回字符串的字节长度 |
| trim() | pub fn trim(&self) -> &str | 去除字符串首尾的空白符 |
| split_whitespace() | pub fn split_whitespace(&self) -> SplitWhitespace | 根据空白符分割字符串并返回分割后的迭代器 |
| split() | pub fn split<’a, P>(&’a self, pat: P) -> Split<’a, P> | 根据指定模式分割字符串并返回分割后的迭代器。模式 P 可以是字符串字面量、字符或一个返回分割符的闭包 |
| chars() | pub fn chars(&self) -> Chars | 返回字符串所有字符组成的迭代器 |
字符串连接符 +
字符串拼接时使用的 +,实际上是调用了 add() 方法。add() 会拿走左边字符串的所有权,并借用右边的字符串,然后将两者合并成新的字符串返回。
1 | add(self, &str) -> String { |
示例:
1 | fn main() { |
格式化宏 format!
如果要把不不同的变量量或对象拼接成⼀一个字符串串,我们可以使⽤用 格式化宏 ( format! )
1 | fn main() { |
rust 运算符
rust 支持以下运算符:
- 算数运算符
- 位运算符
- 关系运算符
- 逻辑运算符
其他的运算符没什么好说的,各语言通用,我们来看一下位运算符
我们假设:A = 2(二进制 10),B = 3(二进制 11)
| 名称 | 运算符 | 说明 | 范例(A = 2,B = 3) |
|---|---|---|---|
| 位与 | & | 相同位都是 1 则返回 1,否则返回 0 | (A & B) 结果为 2 |
| 位或 | 相同位只要有一个是 1 则返回 1,否则返回 0 | ||
| 异或 | ^ | 相同位不相同则返回 1,否则返回 0 | (A ^ B) 结果为 1 |
| 位非 | ! | 把位中的 1 换成 0,0 换成 1 | (!B) 结果为 -4 |
| 左移 | << | 操作数中的所有位向左移动指定位数,右边的位补 0 | (A << 1) 结果为 4 |
| 右移 | >> | 操作数中的所有位向右移动指定位数,左边的位补 0 | (A >> 1) 结果为 1 |
rust 中的条件判断 match
rust 中条件判断有 if…else,这种我们就不多说
我们来看 rust 独有的 match,其语法格式如下:
1 | match variable_expression { |
match 语句句有返回值,它把 匹 ᯈ 值 后执⾏行行的最后⼀一条语句句的结果当作返回值。
示例:
1 | fn main() { |
rust 中的循环
rust 有三种循环:
- loop 语句句。⼀一种᯿复执⾏行行且永远不不会结束的循环。
- while 语句句。⼀一种在某些条件为真的情况下就会永远执⾏行行下去的循环。
- for 语句句。⼀一种有确定次数的循环。
所有循环都可以用 break 和 countinue 关键字
- for 循环,语法格式:
1 | for temp_variable in lower_bound..upper_bound { |
- while 循环,语法格式:
1 | while ( condition ) { |
- loop 循环
1 | loop { |
rust 函数
函数这一块没太多东西好讲的,和其他语言差不多,函数的基本语法如下:
1 | fn function_name([param1:data_type1,param2..paramN]) { |
在函数有返回值的情况下,必须要手动标注函数返回值类型,不然编译不通过!
1 | // ✅ 有返回值 → 必须标注 |
因为 rust 有一套独特的借用 / 所有权系统,所以函数值传递之后返回的是一个全新的值,如果不想返回全新的值,可以使用引用传递,对于引⽤用传递来说,传递的变量和函数参数都共同指向了同一个内存位置。
语法格式如下:
1 | fn function_name(parameter: &data_type) { |
示例:
1 | fn reverse_true(variable: &mut bool){ |
rust 元组
元组是 rust 的一种复杂类型,复杂类型和标量类型的区别就是复杂类型能存储多种类型的数据,而标量类型只能存储一种类型的数据,
元组是定义了之后,长度就不可变了的,所以定义了之后我们只能读取它
元组基础语法如下:
1 | let tuple_name:(data_type1,data_type2,data_type3) = (value1,value2,value3); |
定义元组的时候也可以忽略数据类型
1 | let tuple_name = (value1,value2,value3); |
读取元组中的值:
1 | fn main() { |
元组解构赋值:
1 | fn main() { |
rust 数组
rust 中的数组有以下特点
- 数组的定义其实就是分配一段连续的、相同数据类型的内存块。
- 数组是静态的。这意味着一旦定义和初始化,则永远不可更改它的长度。
- 数组的元素有着相同的数据类型,每一个元素都独占着数据类型大小的内存块。也就是说,数组的内存大小等于数组的长度乘以数组的数据类型大小。
- 数组中的每一个元素按照顺序依次存储,这个顺序号既代表着元素的存储位置,也是数组元素的唯一标识。我们把这个标识称之为数组下标。注意,数组下标从 0 开始。
- 填充数组中的每一个元素的过程称为数组初始化。也就是说,数组初始化就是为数组中的每一个元素赋值。
- 可以更新或修改数组元素的值,但不能删除数组元素。如果要实现删除功能,你可以将它的值赋值为 0 或其它表示删除的值。
基础语法:
1 | let variable_name:[dataType;size] = [value1,value2,value3] |
也可以省略类型标注
1 | let variable_name = [value1,value2,value3]; |
快速初始化数组,数组的长度和初始化的个数必须一致,不然编译不通过
1 | fn main() { |
也可以不定义数组的数量和类型:
1 | fn main() { |
for 循环遍历数组
1 | fn main() { |
我们也可以使用 iter() 方法为数组生成一个迭代器。
然后就可以使用 for in 直接迭代数组
1 | fn main() { |
数组默认是不可变的,和 rust 中的变量一样,如果想要可变数组,要加 mut 关键字
1 | fn main() { |
数组作为参数的话,有两种传递方式,分别是值传递和引用传递
- 数组作为值传递的话传的是值的副本,无论函数中怎么修改这个值,都不会影响原来的数组
- 数组作为引用传递的话,任何对数组的修改都会影响到这个数组本身
数组作为值传递示例:
1 | fn main() { |
输出:
1 | [2, 3, 4] |
数组作为引用传递示例:
1 | fn main() { |
输出:
1 | [2, 3, 4] |
数组有一个唯一的弱点,就是在编译的时候,它的长度必须是已知的
数组的长度必须为整数字,或者整数常量
如果数组长度使用了变量就会报错,即使是不可变的变量也不行:
1 | fn main() { |
使用常量且类型标注必须为 usize,才能编译通过,示例如下:
1 | fn main() { |
这里为什么常量的标注必须为 usize 呢?
Rust 要求数组长度和索引必须是 usize,是因为 usize 是唯一能保证在任何硬件架构下,既能覆盖所有有效内存地址,又能提供最高执行效率的整数类型。 强制把它变成 u32 会破坏这种跨平台的安全性和高效性。
进阶部分
栈和堆
编程语言把内存分为两大类,分别是栈和堆,当然编程语言并没有对内存做什么事情,只是把系统分给应用程序的内存分个类而已
栈是一种先进后出、后进先出的存储罐子,它有一个特点就是:在栈上的数据必须是已知的,也就是说一个变量或者数据要放到栈上,那么它的大小必须是在编译的时候就明确了
Rust 的所有标量类型都存储在栈上,比如 i32 在编译时就已经知道它是占 4 个字节的,而对于字符串这种符合类型,它们是在运行时才会赋值,所以只能存储在堆上
堆用于存储动态数据,就是在运行时才能确定大小的数据,堆是不受系统管理的,由用户自己管理,管理不当的话,内存溢出等问题出现的可能性就会大大增加了
rust 核心——所有权和借用
rust 的所有权有三大原则:
- 每个值在 rust 中都有一个变量,这个变量被称为它的“所有者”;
- 同一时刻,一个值只能对应一个所有权
- 当变量离开作用域了时,这个值将会被丢弃,内存被释放
这个机制杜绝了“悬空指针”和“二次释放”等经典问题。
示例:
1 | fn main() { |
如果想复制堆上的数据,需要显式调用 .clone() 方法
1 | fn main() { |
但是有个例外,就是如果值的数据类型为标量类型(实现了 Copy trait),它们是在栈上直接复制,所以不存在 move 问题,复制后原来的变量依然可用
常见的标量数据类型有 int、boolean、float、&str 等等
示例:
1 | fn main() { |
那么有时候我只是想借用一下数据,进行打印等操作,并不想转移数据的所有权,怎么办?
有的,在 rust 中通过引用 & 的方法实现借用,当然,借用也有一套严格的规则:
- 在任何时刻都只能有一个可变借用(&mut T),或者无数个不可变借用(&T),可变借用和不可变借用不可以同时存在;
- 引用必须总是有效的(不能指向被释放的内存);
这里有个小插曲,就是我在编写这段代码的时候竟然能正常运行,查了一下才知道原来在 rust 中没有使用过的变量,编译器会认为它“不存在”。
1
2
3
4
5
6
7
8
9
10
11 fn main() {
let s1 = String::from("value");
let mut a1 = String::from("value1");
let a2 = &mut a1;
let s2 = &s1;
let s3 = &s1;
let s4 = &s3;
println!("{} {} {} {}", s1, s2, s3, s4);
// 在这里开始才可以使用不可变借用
}
示例,同一时刻只能存在一个可变借用或无数个可变借用
1 | fn main() { |
示例,同一时刻只能存在一个可变借用:
1 | fn main() { |
所有权和标量数据类型
所有的标量数据类型都没有所有权的概念,在一个标量数据类型的变量赋值给另一个变量的时候,并不是所有权转让,而是把数据复制给了一个对象,简单来说就是在内存里新开辟了一个区域,存储复制过来的数据,然后把新的变量指向它,这样做的原因是因为标量数据类型的数据占用的内存不是很大。
1 | fn main(){ |
所有权只会发生在堆上分配的数据
rust 切片
rust 的切片和 python 的差不多,在我看来只是语法结构上有区别,简单来说,切片就是指向一段内存的指针,该指针可以访问指定内存块中连续区间的数据
切片可以作为函数参数,默认是不可变切片:
1 | fn main() { |
可变切片:
1 | fn main() { |
rust 结构体
数组只能存储同一类型的数据,换句话说,数组是同一类型数据的集合。
但是世界不总是那么美好的,很多东西都有其属性,属性是多方面的。
在 rust 里也一样,这时候就要用到结构体了,结构体是不同数据类型的数据的集合,包括另一个结构体。
基本语法:
1 | struct Name_of_structure { |
定义并实例化一个结构体:
1 | struct Person{ |
输出:
1 | My name is Ami, I'm 18 years old, and my weight is 50 kg |
结构体默认是不可变的,如果想要可变,要加上 mut 关键字:
1 | struct Person{ |
输出:
1 | My name is Alex, I'm 20 years old, and my weight is 65 kg |
我们还可以为结构体定义方法,结构体中的方法的作用域仅限于结构体内部,而且 rust 的结构体方法只能定义在结构体的外面,基本语法如下:
1 | struct My_struct {} |
我们可以利用传递 &self 关键字的方式指定当前方法的属主,然后我们就可以访问结构体内部元素了:
1 | struct Person{ |
输出:
1 | name: Ami |
结构体还能定义静态方法,在调用静态方法的时候无需传递 &self 关键字,一般用于实例化 / 初始化
1 | struct Person{ |
输出:
1 | name: LiLi |
rust 枚举
在我们的真实世界中,有红黄蓝绿紫色,但是在计算机中只有 0 和 1,所以我们要将我们真实世界中的语言翻译给计算机的时候,需要做一些小动作来让计算机能“听得懂”我们说话,就比如定义多个常量的方式:
1 | const RED: u8 = 0; |
但是这也太麻烦了,并且每增加一个颜色得多加一行,不利于维护,这个时候我们可以用枚举,基本语法:
1 | enum enum_name { |
按照颜色的例子,我们可以这样定义枚举:
1 |
|
输出:
1 | Blue |
#[derive(Debug)]
既然用到了这个注解,我们来解释一下它的作用,首先在源码中,如果我们不加一行注解在 enum Color 上面的话,会发生什么呢?
1 | enum Color { |
会报错,意思大概就是 enum Color 并没有实现 std::fmt::Debug 特质(trait)
trait 类似于 Java 中的接口
1 | Compiling rust v0.1.0 (C:\Users\admse\Documents\dynamic_file\rust) |
为了让编译能够通过,我们需要让我们的枚举派生或者衍生自一个已经实现 std::fmt::Debug 特质的东西,所以我们可以直接给它上面加一行 #[derive(Debug)],意思就是让我们的枚举 Color 派生自 Debug
1 |
|
输出:
1 | Blue |
Option 枚举
rust 语言核心和标准库内置了很多枚举,其中 Option 枚举是很常用的,我们经常需要跟它打交道
Option 枚举代表了可有可无的选项,它有两个枚举值 None 和 Some(T)
- None 表示无
- Some(T) 表示有
示例:
1 | fn main() { |
输出:
1 | Some(true) |
高级部分
rust 模块
模块用于将函数或结构体按照功能分组,我们通常把相似的函数,或者实现相同功能或共同功能的一个函数和结构体划分到一个模块中。
例如 network 模块里通常都是定义了网络相关的函数和结构体,rust 中的模块也和其他语言,比如 python 的包一样。
比模块更高级的分组是 crate,我们可以将多个模块放在 crate 下面。crate 是 rust 的基本编译单元。rust 中的可执行二进制文件或者一个库就是一个 crate。
可执行二进制文件和库最大的区别就是可执行二进制文件有一个 main 入口函数,而库(library crate)是一个可以在多个项目中重用的组件。
rust 模块的基本语法:
1 | mod module_name { |
rust 中默认模块都是私有的,只能在模块内使用,除非加 pub 关键字就可以在模块外面使用了
相同的,模块里的函数默认也是私有的,得加 pub 关键字才能在同一文件里使用该模块
1 | // module.rs |
接下来我们讲一下 use 关键字,它的作用是可以引入外部公开模块,或者给当前的模块省去模块限定语句,简单来说就是用于导入路径,不必每次都写完整的模块路径,这是它的基本语法:
1 | use public_module_name::function_name; |
注意我们在 use 模块的时候,一般配合 mod 关键字进行使用,示例如下:
1 | // module.rs |
1 | // main.rs |
main.rs 和 module.rs 都是在 src 目录下的
rust 编写一个库 crate 以及添加一些用例
- 我们先使用 cargo 命令初始化一个库
1 | cargo new movie_lib --lib |
创建后的目录结构:
1 | movie_lib/ |
- Cargo.toml 配置
1 | [package] |
- 我们先创建一个 add.rs 文件,里面只定义一个函数
1 | pub fn add(left: u64, right: u64) -> u64 { |
- 编辑 lib.rs 文件,这个文件用于指定库中有哪些公开模块可以用
1 | pub mod add; |
这时候我们可以看到 let result = add::add(2, 2); 这一行需要看起来不够美观,其实不然,它的意思是调用 add 模块里的 add 函数
我们可以使用 pub use 将模块重新导出,这样的话模块函数调用会显得好看很多
1 | pub mod add; |
我们也看到了 lib.rs 中有单元测试的代码,如下
1 |
|
我们可以执行 cargo test 进行单元测试,输出类似下面的话就表示程序测试通过:
1 | running 1 test |
- 接下来我们命令行输入 cargo build,如果构建成功说明编译通过了,代码基本没什么问题
1 | cargo build |
- 这个时候我们在 movie_lib 同级目录使用 cargo new movie_test 创建一个测试应用程序,此时的目录结构是这样的:
1 | ├─movie_lib |
- 在 movie_test 的 Cargo.toml 文件的 [dependencies] 下一行添加,此时的 Cargo.toml 是这样的
1 | [package] |
- 编辑 main.rs 文件
1 | extern crate movie_lib; |
extern crate movie_lib; 用于引入我们刚刚创建的库,这一行是必须的
use movie_lib::add; 这一行就不用我多说了,引入 movie_lib 库里的 add 函数
此时运行程序输出:
1 | 4 |
rust 容器(collections)
rust 的容器标准库提供了比较常用的数据结构实现,包括向量(Vector)、哈希表(HashMap)、哈希集合(HashSet)
由于数组的大小不可变,我们用它来存储一些动态的东西就比较难受了,这时候我们就要引出一个很好用的类似数组的数据结构,那就是向量(Vector),向量也是同类元素的集合,它对比数组有以下特点:
- 在堆上实现的,大小可变
- 添加元素时是在末尾插入元素,且向量会给其分配唯一的索引号
向量的基本语法:
1 | let mut v = Vec::new(); |
向量的初始化方式:
1 | fn main() { |
向量的常用方法:
| 方法 | 签名 | 说明 |
|---|---|---|
| new() | pub fn new() -> Vec |
创建一个空的向量实例 |
| push() | pub fn push(&mut self, value: T) | 将某个值T 添加到向量的末尾 |
| remove() | pub fn remove(&mut self, index: usize) -> T | 删除并返回指定下标处的元素 |
| contains() | pub fn contains(&self, x: &T) -> bool | 判断向量是否包含某个值 |
| len() | pub fn len(&self) -> usize | 返回向量中的元素个数 |
1 | fn main() { |
接下来我们讲讲哈希表(HashMap),哈希表是键值对的集合,其有以下特点:
- 不允许有名字重复的键,但允许有相同的值
- 键值对不保证排列顺序
基本语法:
1 | let mut hm = HashMap::new(); |
hashmap 拥有许多的方法,我们举几个常用的方法:
| 方法 | 签名 | 说明 |
|---|---|---|
| insert() | pub fn insert(&mut self, k: K, v: V) -> Option |
插入/更新一个键值对到哈希表中。如果键已存在则返回旧值,否则返回None |
| len() | pub fn len(&self) -> usize | 返回哈希表中键值对的个数 |
| get() | pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V> | 根据键从哈希表中获取对应的值 |
| iter() | pub fn iter(&self) -> Iter<’_, K, V> | 返回哈希表键值对的无序迭代器,迭代器元素类型为(&’a K, &’a V) |
| contains_key() | pub fn contains_key<Q: ?Sized>(&self, k: &Q) -> bool | 如果哈希表中存在指定的键则返回true,否则返回false |
| remove() | pub fn remove_entry<Q: ?Sized>(&mut self, k: &Q) -> Option<(K, V)> | 从哈希表中删除并返回指定的键值对 |
往 hashmap 中插入元素:
1 | fn main() { |
另一种插入元素的方式,算是一种语法糖吧:
1 | use std::collections::HashMap; |
获取 hashmap 指定键的值
1 | use std::collections::HashMap; |
输出:
1 | Some(1) |
迭代 hashmap
1 | use std::collections::HashMap; |
输出:
1 | c: 3 |
注意:对于容器迭代我们都有多种写法,如果学过其他语言的话,比如 python,我们就可能会常常会写成这样:
1
2
3
4
5
6
7 fn main() {
let v = vec![1, 2, 3, 4, 5, 6];
for element in v{
println!("{}", element);
}
// println!("{:?}", v); // 报错,所有权已经交给 for 循环
}但是在 rust 中因为所有权系统的原因,最好不要像上面这样写,我们最好用 iter() 方法,其返回的是元素的引用,不会占用所有权
1
2
3
4
5
6
7 fn main() {
let v = vec![1, 2, 3, 4, 5, 6];
for element in v.iter(){
println!("{}", element);
}
println!("{:?}", v); // 编译通过
}
好了,现在讲讲哈希集合(HashSet),所谓集合,其最大特点就是没有重复值,它也是不能保证元素排列顺序的
哈希集合的相关方法都定义在 std::collections 模块中,基本语法:
1 | let hs = HashSet::new(); |
常用方法:
| 方法 | 签名 | 说明 |
|---|---|---|
| insert() | pub fn insert(&mut self, value: T) -> bool | 插入一个值到集合中。如果集合已存在该值则插入失败,返回false |
| len() | pub fn len(&self) -> usize | 返回集合中的元素个数 |
| get() | pub fn get<Q: ?Sized>(&self, value: &Q) -> Option<&T> | 根据指定的值获取集合中相应值的引用 |
| iter() | pub fn iter(&self) -> Iter<’_, T> | 返回集合中所有元素组成的无序迭代器,迭代器元素的类型为&’a T |
| contains() | pub fn contains<Q: ?Sized>(&self, value: &Q) -> bool | 判断集合是否包含指定的值 |
| remove() | pub fn remove<Q: ?Sized>(&mut self, value: &Q) -> bool | 从集合中删除指定的值,返回是否删除成功 |
其他的不多讲,HashSet 的食用方式和上面向量、哈希表没太大区别
rust 错误处理
rust 将错误分为可恢复错误和不可恢复错误,在 rust 中没有异常这个概念,与之相似的是可恢复的错误,比如说打开文件的时候提示文件未找到
可恢复错误会返回一个 Result 类型的枚举,不可恢复错误则会调用 panic! 宏
| 类型 | 描述 | 范例 |
|---|---|---|
| 可恢复错误 (Recoverable) | 可以被捕捉和处理,相当于其他语言的异常(Exception) | Result<T, E> 枚举 |
| 不可恢复错误 (Unrecoverable) | 不可捕捉,会导致程序崩溃退出 | panic! 宏 |
panic! 会导致程序立即退出,并返回退出原因,基本语法:
1 | panic!( string_error_msg ) |
而 Result 枚举用于可恢复错误,其定义如下:
1 | enum Result<T,E> { |
Result 包含两个值:OK 和 Err
T 和 E 都是泛型,T 是指程序执行成功返回的类型,E 是指程序执行失败返回的类型
我对于这个枚举比较熟悉,所以例子省略了
虽然 Result 枚举很好用,但是有时候使用的太多的话,会导致程序代码冗余,可读性下降,有时候我们就是只想要某个值,而错误了直接退出程序就 ok,所以 rust 语言的开发者在标准库中定义了 unwrap() 和 expect()
| 方法 | 原型 | 说明 |
|---|---|---|
| unwrap() | unwrap(self) -> T | 如果self 是Ok 或Some,则返回包含的值;否则调用panic!() 立即退出程序 |
| expect() | expect(self, msg: &str) -> T | 如果self 是Ok 或Some,则返回包含的值;否则调用panic!() 并输出自定义的错误信息后退出 |
unwrap 和 expect 不仅可以用来处理 Result 枚举,还可以用来处理 Option 枚举
rust 泛型
泛型就是可以在运行时指定数据类型的机制
泛型可以保证数据安全和类型安全,同时能够减少代码量,而且其可以应用在多种类型的数据上,比如向量、哈希表等等
例如变量 a,其类型可以是 i32、String,我们就可以将它的泛型指定为 T
泛型还可以用在结构体上,比如所有一个结构体 bing_struct,其里面的 value 变量可以指定另一个结构体 small_struct
1 | fn main() { |
rust 特质(trait)
rust 中没有接口和类的概念,但提供了 trait 这个概念用来代替接口和类,当然你也可以把 trait 当作接口来看待,它们虽然从概念上有差别,但是用起来都是差不多的,其基本语法如下:
1 | trait some_trait { |
我们要实现这个 trait 的时候用 impl …关键字,如果我们要为某个结构体实现某些 trait,就用 impl … for …,基本语法如下:
1 | impl some_trait for structure_name { |
注意:特质的方法是结构体的成员方法,因此第⼀一个参数是 &self。
示例:
1 | fn main() { |
输出:
1 | The book id is 5, and the name is Monkey king |
泛型函数
泛型页可以用在函数中,泛型函数指的是函数的参数是泛型,基本语法:
1 | fn function_name<T[:trait_name]>(param1:T, [other_params]) { |
示例代码,意思就是函数参数可以是任何实现了 Display 特质的变量:
1 | use std::fmt::Display; |
rust IO 操作
rust 通过两个特质来组织 IO 输入输出
- read 特质用于读
- write 特质用于写
我们先看 read 特质,先用该特质从标准输入里读取内容,然后进行输出
示例如下,从命令行读取输入,然后进行打印:
1 | fn main() { |
read_line 会保留字符中的换行符
我们再来看看 write 特质,我们从用 stdout 向标准输出写入内容。
1 | use std::io::Write; |
接下来我们来看看命令行参数,命令行参数指的是就通过终端或命令行或 shell 传递给程序的函数
在 rust 中是通过 std::env::args() 来输出所有传递给命令行的参数,示例:
1 | use std::env::args; |
输出:
1 | 参数一 |
rust 文件操作
rust 标准库提供了大量与文件有关的模块
rust 用结构体 File 来描述 / 展现一个文件
所有 File 结构体的操作方法都会返回一个 Result 枚举
文件相关操作:
| 模块/方法 | 方法签名 | 说明 |
|---|---|---|
| std::fs::File::open() | pub fn open<P: AsRef |
静态方法,以只读模式打开文件 |
| std::fs::File::create() | pub fn create<P: AsRef |
静态方法,以可写模式打开文件。文件存在则清空旧内容,不存在则新建 |
| std::fs::remove_file() | pub fn remove_file<P: AsRef |
从文件系统中删除指定文件 |
| std::fs::OpenOptions | pub fn append(&mut self, append: bool) -> &mut OpenOptions | 设置文件打开模式为追加写入 |
IO 相关操作:
| 模块/方法 | 方法签名 | 说明 |
|---|---|---|
| std::io::Write::write_all() | fn write_all(&mut self, buf: &[u8]) -> Result<()> | 将缓冲区中的所有内容写入输出流 |
| std::io::Read::read_to_string() | fn read_to_string(&mut self, buf: &mut String) -> Result |
读取所有内容并转换为字符串,追加到buf 中,返回读取的字节数 |
首先我们来试试打开文件:
1 | use std::io::Read; |
因为文件不存在,所以报错了,输出如下:
1 | 当前工作目录: c:\Users\user\Documents\rust\movie_test |
在工作目录创建一个 file.txt,并写点东西在里面,再次运行输出如下:
1 | 当前工作目录: c:\Users\user\Documents\rust\movie_test |
创建文件,如果文件存在的话会被新文件覆盖
但是如果使用 std::fs::File::create_new ,文件存在的话将不会创建成功,会报错 文件创建失败: Os { code: 80, kind: AlreadyExists, message: “The file exists.” }
示例如下:
1 | fn main() { |
输出如下:
1 | File { handle: 0xac, path: "\\\\?\\C:\\Users\\user\\Documents\\rust\\movie_test\\data.txt" } |
那么我们如何写入文件呢,我们写文件用的是 std::io::writes 模块的 write_all() 函数,这个函数是用于向输出流写入内容
因为文件流也是输出流的一种,所以该函数也可以用于向文件写入内容
这个函数的原型如下:
1 | fn write_all(&mut self, buf: &[u8]) -> Result<()> |
文件写入示例:
1 | use std::io::Write; |
输出:
1 | 文件写入完成 |
data.txt 文件内容:
1 | 谷歌的网址是: google.com |
说完写入文件,我们来说说如何读取文件,一般来说在 rust 中读取文件分为两个步骤:
- 使用 open() 打开一个文件
- 使用 read_to_string() 函数从文件中读取所有内容并转为字符串
open 函数已经有示例了,我们来说 read_to_string() 函数
read_to_string() 函数用于从一个文件中读取剩余所有内容,并追加到 buf 中,如果读取成功则返回字节数,读取失败则抛出错误,其原型如下:
1 | fn read_to_string(&mut self, buf: &mut String) -> Result |
示例:
1 | use std::io::{Read}; |
输出:
1 | 文件成功打开, 其内容如下: |
那如果我们想追加文件内容呢?
在 rust 中并没有提供相关的函数用于追加内容到文件末尾,但是我们可以使用 append() 用于将文件的打开模式设置为追加,就可以实现追加文件内容这个功能了
函数 append() 在模块 std::fs::OpenOptions 中定义,其原型如下:
1 | pub fn append(&mut self, append: bool) -> &mut OpenOptions |
追加文件内容示例:
1 | use std::io::Write; |
输出:
1 | 文件内容追加成功 |
此时 data.txt 的内容:
1 | 谷歌的网址是: google.com |
还有就是删除文件,在 rust 中使用 remove_file() 方法删除文件
但是要注意,删除可能会失败,而且即使结果为 OK,也有可能不会立即删除
删除 data.txt 示例:
1 | use std::{fs}; |
执行后 data.txt 将被删除
练习:使用标准库实现一个文件复制功能
由于 rust 标准库并不自带文件复制相关函数,刚好可以当作练习,
现在我们工作目录下有个 file.txt 文件,其内容是 This is the file content. ,我们要将其复制到 data.txt,现在自己逐步利用标准库来实现一下:
- 首先我们知道 std::env::args()可以获取命令行参数
1 | fn main() { |
执行结果如下:
1 | Args { inner: ["C:\\Users\\user\\Documents\\rust\\movie_test\\target\\debug\\movie_test.exe", "args1", "args2", "args3"] } |
- 我们可以用 next() 方法获取下一个参数,第一个参数是程序名,我们可以跳过它
1 | fn main() { |
输出如下:
1 | First arg: C:\Users\admse\Documents\dynamic_file\rust\movie_test\target\debug\movie_test.exe |
- 我们这一步的思路就是:创建一个 4kb 的缓冲区数组,按照缓存数组大小,循环读取源文件内容,写入目标文件
1 | use std::io::{Read, Write}; |
- cargo build,然后命令行输入 .\target\debug\movie_test.exe file.txt data.txt,此时的 file.txt 内容:
1 | This is the file content. |
data.txt 的内容:
1 | This is the file content. |
- 练习完成!
rust 在 vscode 调试代码
- 在 vscode 下载 CodeLLDB 扩展
- 按 ctrl+shift+D 快捷键,然后点击 vscode 左侧的 “创建 launch.json 文件”
- 手动复制 launch.json 内容,”args”: [“file.txt”, “data.txt”],为程序的命令行参数,自己改成自己的
1 | { |
- 直接摁 F5 即可开始调试
rust 练习:猜数字游戏
- 首先初始化一个项目,–bin 是构建一个二进制文件,还有一个 –lib 是创建一个 rust 库的意思,即使我们不输入 –bin 参数,默认的话也是带 –bin 的
1 | cargo new guess-number-game --bin |
- 我们要安装一个第三方库,用于随机数字,命令行输入
1 | cargo add rand |
然后我们就可以看见 Cargo.toml 的 dependencies 段多了一行 rand = “0.10.1”
1 | [package] |
- 我们来分析一下猜数字的逻辑
1 | 1.游戏开始时随机一个数字 |
- 按照逻辑写出代码
1 | fn get_guess() -> u8{ |
- 练习完成
rust 迭代器 Iterator
迭代器主要用来遍历集合。
如果把集合比喻成一缸水,迭代器就是水瓢
rust 中的集合,比如向量、数组、哈希表都实现了迭代器
rust 中的迭代器都要实现标准库中定义的 Iterator 特质
Iterator 特质有两个函数必须实现:
- 一个是 iter(),用于返回一个迭代器对象。迭代器中的值我们称之为项
- 一个是 next(),返回迭代器中的下一个元素,如果迭代到了集合的末尾,则返回 None
rust 有三种类型的迭代器,分别是:
| 特性 | iter() | iter_mut() | into_iter() |
|---|---|---|---|
| 元素类型 | &T | &mut T | T(或&T/&mut T) |
| 获取所有权 | ❌ | ❌ | ✅(消耗集合) |
| 修改元素 | ❌ | ✅ | ✅(如果获得所有权) |
| 集合仍可用 | ✅ | ✅ | ❌ |
| 可多次调用 | ✅ | ✅(受借用规则限制) | ❌ |
| 适用场景 | 只读遍历 | 修改元素 | 转移所有权/消费集合 |
实际使用的话:
- 只需要读取 -> 用 iter()
- 需要修改元素 -> 用 iter_mut()
- 需要转移所有权(如转换为其他类型)-> 用 into_iter()
rust 闭包
以下是对于闭包的解释:
- 闭包就是在一个函数内创建立即调用另一个函数
- 闭包是一个匿名函数,没有函数名称
- 闭包可以赋值给一个变量,这样的话这个变量就是闭包的名称
- 闭包不用声明返回值,但却可以拥有返回值,并且使用最后一条语句的执行结果作为返回值
- 闭包在有些地方成为“内联函数”,这种特性使得其可以访问外层函数里的变量
对于只执行一次的函数,最好就是用闭包来作为代替方案。
闭包的基本语法:
1 | |parameter| { |
示例,判断输入的参数是不是偶数:
1 | fn main() { |
输出:
1 | 2 is even ? true |
闭包还可以访问外层函数的变量:
1 | fn main() { |
输出:
1 | 100 |
rust 智能指针
rust 是系统级的语言,既然是系统级的语言,就离不开指针
但 rust 又是现代的语言,现代的语言都会尽可能的抛弃指针,默认把数据存储在栈上
如果想要把数据存在堆上,就要在堆上开辟内存,就要用到指针
作为系统级的语言,rust 提供了在堆上存储数据的能力,并把这个能力弱化并封装到了 Box 中。
这种把栈上的数据搬到堆上的能力,我们称之为装箱
rust 中的某些数据类型,比如向量和字符串对象默认就是把数据存储在堆上的
rust 把指针封装为以下两大特质,当一个结构体实现了下面的接口后,它们就不再是普通的结构体了
| 特质 | 操作符 | 返回类型 | 可否修改 | 调用时机 |
|---|---|---|---|---|
| Deref | * | &T | ❌ 只读 | 解引用时 |
| Drop | 隐式调用 | - | - | 离开作用域时 |
接下来我们介绍以下 Box,Box 指针也称之为装箱,允许我们将数组存储在堆上而不是栈上。
Box 指针没有额外开销,因为它仅仅是将数据存储在堆上而已。
访问 Box 上的具体数据需要解引用,也就是星号操作符,示例:
1 | fn main() { |
输出:
1 | true |
接下来我们说一下 Deref 特质和 Drop 特质
Deref 特质需要我们实现 deref() 方法, deref() 方法从某方面说是借用 self 对象,并返回一个指向内部数据的指针。
也就是说 deref() 方法返回的是一个指向内部结构体的指针。
示例如下:
1 | use std::ops::Deref; |
解释一下部分代码片段的意思:
impl
是什么意思? - 这是 Rust 的泛型参数声明,表示 impl 块针对任意类型T 都有效。
- 相当于说:无论 MyBox 里面装的是 i32、String 还是其他任何类型,我都为它实现 Deref
type Target = T; 是什么意思?
- 这是 关联类型(Associated Type) 的语法,用于指定 Deref trait 的”目标类型”。
- Deref trait 要求你必须指定解引用后得到的目标类型是什么
- 这里声明 Target = T,意思就是:解引用MyBox
得到的是T 类型,换句话说:*my_box 返回的类型是 T
fn deref(&self) -> &Self::Target { &self.0 } 是什么意思?
- 这是 Deref trait 要求实现的唯一方法,定义了解引用时的具体行为
- &self.0 表示返回元组第 0 个元素的引用
关于这一段疑惑比较多,所以还是单独拎出来解释一下:
1 | fn deref(&self) -> &Self::Target { |
| 语法 | 含义 |
|---|---|
| &self | 不可变引用方式接收self(不获取所有权) |
| -> &Self::Target | 返回一个不可变引用,指向的目标类型是T |
| Self::Target | 就是上面定义的type Target = T |
| &self.0 | 取MyBox 元组的第 0 个元素(即内部存储的值),并返回它的引用 |
关于首字母大写 Self 和首字母小写的 self,其也有本质区别:
| 写法 | 含义 | 类型 |
|---|---|---|
| Self(大写 S) | 类型名,代表当前类型本身 | 类型(如MyBox |
| self(小写 s) | 实例,代表当前对象的值 | 值(如&self、self) |
接下来我们说一下 Drop 特质,当一个结构体实现了该特质的时候,该结构体在离开作用域的时候会触发 drop() 方法
话不多说,看示例:
1 | use std::ops::Deref; |
输出:
1 | true |
rust 多线程和并发编程
多线程的概念就不多说了,懂得都懂
一个程序至少有一个进程,当然也可以有多个进程
一个进程至少有一个主线程,除了主线程之外的线程都叫做子线程
多线程是指在主线程之外创建子线程,让子线程和主线程同时运行,完成各自的任务
rust 支持多线程并发编程
rust 语言标准库中的 std::thread 模块用于多线程编程,包括创建线程、管理线程和结束线程
创建一个线程,可以使用 std::thread::spawn() 方法,其函数原型如下:
1 | pub fn spawn<F, T>(f: F) -> JoinHandle<T> |
参数 f 就是一个闭包,是线程要执行的代码
多线程示例代码:
1 | use std::{thread, time::Duration}; |
输出:
1 | Number 1 from the main thread |
咦,执行结果好像出问题了?
嗯,好像是,但又好像不是。
因为当主线程执行结束,程序会自动关闭所有创建出来的衍生子线程,无论它们是否执行完毕。
在上面的代码中,我们调用 thread::sleep() 强制线程休眠一段时间,这允许不同线程交替执行。
虽然线程休眠时会自动让出 CPU,但并不保证其他线程一定会被执行。这完全取决于操作系统的线程调度策略,所以导致上面的程序输出结果是随机的
这里还有个问题,就是说一定要用 thread::sleep() 才能使主线程和子线程交替运行吗?
不一定!thread::sleep 不是实现交替运行的唯一方式,但它是让交替运行可见的常用方法。
让我们先看看如果没有调用 sleep 会怎么样:
1
2
3
4
5
6
7
8
9
10
11
12
13 use std::thread;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("子线程: {}", i);
}
});
for i in 1..5 {
println!("主线程: {}", i);
}
}输出:
1
2
3
4
5 主线程: 1
主线程: 2
主线程: 3
主线程: 4
// 程序退出,子线程可能根本没机会执行
- 主线程不 sleep,会飞速执行完毕(微秒级)
- 子线程还没来得及被调度,主线程就结束了
- 程序退出,子线程被强制终止
所以 sleep 的作用如下:
- 主动让出 CPU:当前线程放弃执行权
- 给予调度机会:操作系统可以选择执行其他线程
- 减缓执行速度:让交替执行在视觉上更明显
但我们可以不用 sleep,用 sleep 只是可以让多线程视觉效果更明显,仅此而已,我们还可以用 yield_now 和 join,其区别如下:
方式 行为 适用场景 thread::sleep(dur) 休眠指定时间,保证让出 CPU 模拟延迟、降低 CPU 占用 thread::yield_now() 主动让出 CPU,立即准备运行 协作式多任务 join() 阻塞等待线程结束 必须等待结果
在多线程中主线程一旦执行完成,程序会立即退出,不会等待子线程,那么我们如何解决这个问题呢?
这个时候就要加入线程句柄 join(),这是 rust 标准库提供的方法,用于将子线程加入主线程等待队列
join() 的原型如下:
1 | spawn<F, T>(f: F) -> JoinHandle<T> |
我们优化一下原来的代码:
1 | use std::{thread, time::Duration}; |
输出:
1 | Number 1 from the main thread |
rust 异步编程
同步编程和异步编程的区别:
1 | // 同步(Synchronous):按顺序执行,阻塞等待 |
核心概念对比:
| 同步 | 异步 |
|---|---|
| 阻塞等待 I/O 完成 | 等待时让出 CPU |
| 一个任务完成才做下一个 | 可以并发执行多个任务 |
| 简单直观 | 性能高,但复杂度高 |
| 适合 CPU 密集型任务 | 适合 I/O 密集型任务(网络、文件、数据库) |
rust 的异步生态:
1 | Rust 异步生态 |
在 rust 中使用异步的话需要运行时,因为还没有正式的官方核心标准库
1 | // ❌ 错误:async 函数不能直接运行 |
rust 异步编程示例:
1 | use tokio::time::{sleep, Duration}; |
输出:
1 | 顺序执行结果: 任务1完成, 任务2完成 |
并发 http 请求:
1 | use std::time::Instant; |
输出:
1 | https://httpbin.org/delay/3 响应状态: 503 Service Unavailable |
- 标题: rust 复习
- 作者: Nevolar
- 创建于 : 2026-06-29 01:33:52
- 更新于 : 2026-06-29 01:33:53
- 链接: https://blog.freeaes.com/2026/06/f9f15ccf5b44.html
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。