Rust

循环
for循环
for i in 0..10
0..10类似与python中的range(0, 10),左闭右开区间.
while循环
let mut i = 0;
while i < 10
与python中的while循环类似.
loop循环
let mut i = 0;
loop
死循环, 相当于python中的while True, 通过break跳出循环.
从loop循环中返回值
let mut i = 0;
let result = loop ;
println!; // 20
在loop循环中使用break的时候还可以添加一个返回值.(类似一个简化的循环函数?)
循环标签
'outer: for i in 0..10
在循环前面加上单引号',可以精确的控制循环的跳出位置.
遍历集合中的元素
let arr = ;
for v in arr
遍历数组中的元素, 类似于python中的for v in arr.
反转 range
for i in .rev
rev方法可以反转range的顺序.
for循环的enumerate方法
let arr = ;
for in arr.iter.enumerate
enumerate方法返回一个元组,包含当前元素的索引和值. 类似于python中的enumerate方法.
所有权
- Rust 中的每一个值都有一个被称为其所有者的变量.
- 一次只能有一个所有者.
- 当所有者(变量)离开作用域,这个值将被丢弃.
更多内容可以看这篇文章: gsgfs.moe/blog/4
self, &self, &mut self
self, &self, &mut self都是方法签名的关键字, 区别如下
self: 转移所有权- 适用于消费场景, 支持方法链(builder pattern)
- 如果返回自身需要重新构造
&self: 可变引用- 只读
&mut self: 可变引用- 原实例在释放前不可访问
- 适合单点修改
生命周期
生命周期是 Rust 的内存安全机制, 用于确保所有引用在使用时都是有效的, 避免垂悬引用(dangling references).
'a表示生命周期注解
// 这份代码无法通过编译
// 生命周期长的 r 引用了生命周期短的 x
显式生命周期注解
在函数声明中使用生命周期注解来声明参数的生命周期
// 表示返回值与两个输入参数的生命周期一样长
在下面的这个例子中, 因为不能多次可变借用不能通过编译.
但是在代码中想要借用的是remainder中的某个不重复的字节, 因为没有正确的区分生命周期, 编译器以为你是要将ByteIter整个借走, 而无法通过编译.
一个可行的解法是为返回值标注不同的生命周期, 区分结构体和结构体内的数据. 比如说下面的代码就可以通过编译.
Claude 3.7(AI) 给出的图解, 看下面的图可能更好理解
┌───────────────┐ 引用 ┌──────────────┐
│静态数据 b"1145" │◄────────────┤bytes.remainder│
└───────────────┘ └──────────────┘
▲ │
│ │
│ 包含在 │
│ │
│ ▼
│ ┌──────────────┐
│ │ bytes 结构体 │ ←── 被drop
│ └──────────────┘
│
│ 引用
│
┌──────┴──────┐
│ b1 和 b2 │
└─────────────┘
CPU 要烧了...
结构体
Rust中的结构体与其他语言的类似.
// `#[derive(Debug)]`是一个宏, 自动实现`Debug`特性, 用于调试输出.
// 派生 Debug 特性
但是结构体要求字段必须拥有所有权或是有生命周期.
比如下面的这份代码就会报错:
元组结构体
元组结构体可以没有字段名, 长的跟元组很像
;
let black = Color;
dbg!;
单元结构体
单元结构体没有字段, 但是可以给他添加方法
;
枚举
枚举类型可以限制变量的取值范围
枚举类型还可以携带数据
// 限制枚举成员的取值范围 0-255
为什么需要 #[derive(Clone, Copy)]
因为使用 *self 时访问的是应用指向的值, 而 as u8 需要将这个 Week 枚举值移出引用, 但是 Week 没有实现 Copy 特性, 会被运动, Rust 中不允许从共享引用中移出数据(因为会导致这个数据失效)
除了内置类型之外还可以表示一些奇奇怪怪的东西...
模式匹配
模式匹配是 Rust 相较于其他语言非常有特色的一个功能, 可以在保障安全性的前提下最大程度的简化代码
match
直接对表达式可能的各种情况进行拆分处理
比如说匹配一个枚举类型:
除此之外, 还可以用各种奇奇怪怪的方式匹配各种奇奇怪怪的东西, 比如说匹配一个 32 位有符号整形
解构
在 match 匹配的时候还可以附加要绑定的参数来直接解构这个匹配
; // 普通结构体同理
如果需要忽略部分值:
if let
if let ...表达式可以快速拆开可能的值, 如果成功拆开了会直接判断给出的条件为true (while let ...同理)
泛型
泛型是一种在编译时不知道具体类型的抽象类型.
// 添加 PartialOrd 和 Copy 特征约束, 保证 T 类型是可以比较大小并且可以复制的
泛型的常见用法, 可以实现类似 C 语言中的函数重载的效果
/// 实现&str到HttpMethod的转换
泛型约束
编译器无法确定泛型的特性, 这时就需要显示指定
use Mul;
字符串
Rust的字符串有两种类型: String和&str.
String类型是一个拥有所有权的类型, 离开作用域会被自动释放; 而&str类型是一个借用, 没有所有权, 可以指向字符串字面量或者String类型的一部分.
String类型拥有所有权, 是可变的.&str类型是一个不可变的引用, 适合只读场景.
为什么String类型不支持下标索引?
因为String类型使用UTF-8编码, 这是一种长度可变的编码, 一个字符可能占用多个字节.
- ASCII 字符(英文字母、数字)占 1 个字节
- 大多数中文字符占 3 个字节
- 一些特殊符号和表情符号可能占 4-6 个字节
使用下标索引可能会引发误解:
let s = Stringfrom;
let c = s; // 这是指第一个字节还是第一个字符?
同时, 下标索引还有效率问题, 如果需要访问某个字符, 因为编码的原因, 需要从头遍历字符串, 时间复杂度为O(n), 而不是O(1).
为什么Python中可以直接通过下标访问呢
Python中的字符串使用Unicode编码, 长度为2字节或是4字节, 每个索引都是对应一个完整的字符, 所以可以通过下标索引访问.
同样的, Python中的字符串也是不可变的, 如果需要修改字符串, 需要重新创建一个新的字符串.
=
= # 报错: 未在类型"Literal['a']"上定义"__setitem__"方法
这是两种语言的设计理念不同导致的.
Python: 高级, 易用, 使事情简单化Rust: 内存安全, 高性能, 反应真实的操作成本
同时, 字符串是一个胖指针, 在 Rust 中占用 24 字节
Rust 中的 String 类型由三部分组成, 每部分都是uszie(在 64 位系统上占用 8 个字节, 也就是 64 位)
┌─────────────────────────┐
│ String (栈上,24字节) │
├─────────┬───────┬───────┤
│ 堆指针 │ 长度 │ 容量 │
│ (8字节) │ (8字节)│(8字节) │
└─────────┴───────┴───────┘
│
▼
┌────────────────────────────┐
│ 字符串数据 (堆上) │
│ "🤯" (实际占4字节) │
└────────────────────────────┘
字符串切片
let s = Stringfrom;
let hello = &s;
let world = &s;
println!;
字符串切片的语法类似于python中的切片, 都是截取一段左闭右开区间.
字符串切片返回的是str类型, 这个类型在编译时不知道大小, 无法通过编译, 所以需要使用&符号.
字符串修改
-
转换为
Vec<char>类型
进行这的转换之后, 对于其他语言的用户来说就十分亲切了.Char类型的长度固定为4个字节, 可以通过下标进行O(1)复杂度的标索引访问.let s = Stringfrom; // 使用 char() 方法, 返回一个迭代器, 这个迭代器按照字符顺序遍历 String // 并返回一个 char 类型的值, 由 collect() 收集到一个集合中 let mut s1: = s.chars.collect; s1 = '😊'; let s2: String = s1.iter.collect; assert_eq!;
closure
closure(闭包)是一种匿名函数, 可以捕获上下文中的变量.
closure 由两部分组成: 参数列表(两条竖线中的变量)和函数体(后面的表达式).
let base = 42;
let 你好 = ; // 直接使用变量 base
assert_eq!;
对于 cpp 用户而言, 可以这样理解:
signed
返回闭包
返回一个闭包(略感震惊, 这不应该是 js, py 这些脚本语言才有的吗)
+ 'a
trait
trait(特质?)是Rust中定义共享行为的方式, 类似于其他语言中的interface(接口).
特质定义了一系列方法, 如果要实现这个特质, 需要实现这些方法.
实现一个trait:
trait 的作用域
Rust 允许在任何地方实现 trait, 如果需要使用这个 trait 提供的功能, 可能需要导入特定的模块.
use Write;
std::io::impls中的相关代码:
trait 方法可见性
在Rust中, 需要使用一个trait的方法, 那么这个trait必须在当前作用域中可见.
具体见下面的例子:
虽然A这个trait继承了std::io::Write, 但是Write中的实现对其他的部分并不是可见的, 在使用write_all方法的时候会引发错误.
这个特性可以使代码的关系更加明确, 避免隐式导入引发的命名冲突.
报错为:
no method named `write_all` found for struct `std::vec::Vec<u8>` in the current scope items from traits can only be used if the trait is in scope
trait object
trait object是Rust中的动态分发机制(dynamic dispatch), 只要实现了相同的trait, 就可以在运行时处理不同类型的值.
组成, trait object由两部分组成:
- 数据指针: 指向实际对象数据的指针
- 虚函数表指针(vtable): 包含了该
trait所需方法的函数指针表
┌─────────────┬─────────────┐
│ 数据指针 │ 虚表指针 │
└─────────────┴─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────────────┐
│ 实际数据 │ │ 虚表 (vtable) │
└─────────────┘ ├─────────────────────┤
│ 方法1的函数指针 │
│ 方法2的函数指针 │
│ Drop实现的指针 │
│ 类型大小信息 │
│ 类型对齐信息 │
└─────────────────────┘
因为trait object实际上由两个指针组成, 所以是一个fat pointer(胖指针).
通常有两种形式:
&dyn Trait: 引用形式(不可变)&mut dyn Trait: // 调用所有权形式(可变)
返回 trait
可以在函数中返回一个 trait, 让编译器自己推导具体类型, 让接口更加灵活 (说起来这一部分(Trait)是不是可以直接和泛型部分和并到一起写?)
特点:
- 类型隐藏和抽象
- 简化类型签名
- 自动生命周期绑定
例如, 返回一个哈希表的迭代器:
上面的代码等价于
迭代器
迭代器允许访问一个连续的集合, 只需要关心如何处理每个元素, 而不需要关心集合的内部结构.
同时, 迭代器是惰性的, 只有在需要的时候才会计算下一个元素.
iter方法
iter方法返回一个不可变的迭代器, 正因如此, 迭代器不会取得所有权.
而直接访问集合的写法会取得所有权, 导致集合无法再次使用.
let arr = ;
for v in arr.iter
关于所有权的进一步说明
对于实现了Copy特性的类型不使用iter方法也没有什么太大的问题(对于那些存储在栈上的数据), 但是对于其他的可变类型就会引发错误, 比如说Vec类型.
虽然i32类型实现了Copy特性, 但是Vec类型并没有没有实现Copy特性.
所以下面的代码的最后一行会引发报错.
let numbers: = ;
for i in numbers
println!;
let numbers: = vec!;
for i in numbers
// 下面一行代码无法通过编译
// println!("{:#?}", numbers); // borrow of moved value: `numbers` value borrowed here after move
next方法
跟Python类似, Rust的迭代器是通过next方法来获取下一个元素的.
有点类似鸭子类型, 这是Python等动态语言的一个特性, "只要长得像鸭子, 走路像鸭子, 就可以认为是鸭子".
手动迭代:
let arr: = ;
let mut iter = arr.into_iter;
loop
因为next方法返回的是Option类型, 所以需要手动处理Some和None两种情况.
同时, 因为next方法会改变状态数据, 需要标注为mut.
区分iter, iter_mut, into_iter
iter: 返回一个不可变的迭代器, 属于引用迭代器iter_mut: 返回一个可变的迭代器, 属于可变引用迭代器into_iter: 返回一个所有权迭代器, 会消耗所有权
迭代器适配器与消费者适配器
- 迭代器适配器: 会返回一个新的迭代器, 用于链式调用
- 消费者适配器: 会消费迭代器, 返回一个非迭代器的值
let arr: = vec!;
let new_arr: = arr
.iter
.map
.filter
.collect;
println!;
在这个例子中, map和filter方法是迭代器适配器, collect方法是消费者适配器.
实现一个迭代器
错误处理
Rust 强制要求在代码中处理所有可能的错误(相当于其他语言强制对危险操作使用 try 语句)
Result <T,E> 类型
Result<T, E>是 Rust 错误处理的核心机制, 表示操作可能的两种结果: 成功Ok(T)和失败Err(E).
一个尝试绑定端口的栗子:
use ;
/// 返回一个Result, 这个Result展开可能会有两种结果
///
/// 如果成功返回(), 表示unit类型, 相当于没有返回值
/// 如果失败返回Box<dyn Error>, 这是一个指向错误类型的指针
在 Rust 中有很多种方法可以处理错误, 常见的有: unwrap, expect, and_then...
语法糖
使用?操作符可以快速向上传播错误, 上面的栗子中的函数也可以写成
需要注意的是, ?操作符只能在返回值为 Result 或是 Option 的函数中使用
Option <T>类型
Option 与 Result 不同的是, Option 表示可能有值, 也可能没有
use ;
panic
触发 panic 会直接退出整个程序
模块
将代码分割成不同的模块, 有助于提高代码的可读性和可维护性.
mod
使用mod关键字定义一个模块
// src/main.rs
// 将自动在 src/mytest 和 src/mytest/mod.rs 中寻找
pub
使用pub关键字将函数暴露出去
// src/mytest/mod.rs
use
使用use关键字引入模块中的函数
// src/main.rs
use hi;
整体的结构像是这样:
src/
├── main.rs
└── mytest/
└── mod.rs
当然, 把mytest文件夹直接写成mytest.rs也是可以的.
在mod.rs文件中使用pub mod xxx;来导出
智能指针
Box
Box允许将数据存储在堆上.
Box还可以将动态大小的类型转变为固定大小的类型.
在Rust中几乎所有的类型都需要在编译时知道大小, 但是Box本质是一个智能指针, 指向List类型的数据, 这个指针的大小是固定的.
// 有效的链表实现
其他解决方案:
-
使用间接引用:
Rc 与 Arc
有时候一个数据可能会需要多个所有者, 但是Rust中的所有权规则不允许这种情况, 这时就需要使用Rc(reference counting).
Rc是一个引用计数智能指针, 允许多个所有者共享数据.
Arc则是Rc的多线程版本, 允许多个线程共享数据.
use Rc;
上面的代码中, Rc::clone方法为浅拷贝, 仅复制智能指针并增加引用计数, 而不会复制底层数据.
但是, Rc虽然有所有权, 但是只可以只读访问, 无法修改. (这和引用有什么区别嘛!)
如果需要修改数据, 则需要使用RefCell或是Mutex来实现.
Cell 与 RefCell
Cell和RefCell是Rust中的内部可变性机制, 可以在拥有不可变引用时修改目标数据.
内部使用了unsafe来实现这一点.
Cell: 适用于实现了Copy特性的类型.
use Cell;
RefCell: 适用于没有实现Copy特性的类型.
use RefCell;
RefCell可以在编译时跳过规则检查, 但是如果违背了规则仍然会导致运行时panic.
宏
Rust 中的宏分为两大类:
- 声明宏(declarative macros)
- 三种过程宏(procedural macros)
- 派生宏: #[derive]
- 类属性宏: 定义自定义属性
- 类函数宏: 类似函数调用的宏
声明宏
使用类似于正则匹配加上match表达式的语法进行匹配, 并执行相关代码.
// 注解, 声明这个宏被引入作用域
过程宏
Deref
Deref是一个trait, 实现Deref这个trait允许我们重载解引用运算符(*).
use Deref;
// 元组结构体, 在实现 Rust 的 Deref trait 时,可以让自定义类型行为类似于引用
;
强制转换
Deref还有一个重要功能, 将一个类型的引用传递给不同类型的时候, 如果源类型实现了Deref, 并且Deref可以进一步转换为目标类型, 那么Rust会自动进行Deref操作.
比如说这个函数需要的是一个&str类型
但是调用里传递的是一个String类型
let s = new;
hello;
那么就会自动进行转换:
&MyBox<String>->&String: 通过MyBox实现的Deref&String->&str: 通过String实现的Deref
如果不使用自动的类型转换, 那么就需要手动解引用, 类似于:
hello; // 好抽象的代码
// or
hello; // this expression creates a reference which is immediately dereferenced by the compiler
同时, 因为Deref的解析都发生在编译时, 所以不会有运行时的性能损耗.
cargo
cargo是Rust的包管理器
Cargo.toml和Cargo.lock是cargo的元数据文件, 与JavaScript的package.json和package-lock.json类似.
是否上传Cargo.lock?
- 如果是依赖库: 不要上传
- 如果是用户程序: 需要上传
如果是依赖库上传Cargo.lock会锁定依赖, 对于使用同一个上游的库, 可能会造成依赖冲突.
多线程
主线程结束后, 所有的子线程也会结束, 除非使用join方法等待子线程结束.
use ;
并发与并行
- 并发(Concurrent): 交替执行多个任务, 但是在同一时刻只有一个任务在执行
- 并行(Parallel): 同一时刻有多个任务在执行
多线程的风险
- 数据竞争: 多个线程同时访问同一块内存, 会导致数据不一致(
Rust的所有权机制可以避免数据竞争) - 死锁: 两个线程相互等待对方释放资源
- 活锁: 两个线程不断相应对方的动作, 但是无法继续执行有意义的工作
- 饥饿: 一个线程无法获得所需的资源
生命周期
闭包可以捕获当前环境的变量, 但是因为不知道这个变量什么时候会被销毁, 所以需要让线程的闭包获得所有权.
use thread;
信道 channel
信道(channel)是一种线程间通信的方式, 用于在多个线程之间传递消息.
使用std::sync::mpsc::channel创建一个信道, 返回两个端点: 发送端和接收端.
- 接收消息:
recv: 阻塞等待消息try_recv: 非阻塞等待消息(立即返回Result)
// mpsc 是多个生产者, 单个消费者的缩写(multiple producer, single consumer)
use ;
使用for循环迭代接收端, clone发送端从多个生产者发送消息:
use ;
互斥锁
互斥锁(mutex)是一种并发原语, 用于在多个线程之间共享访问数据.
- 在使用数据之前, 需要先获取锁
- 使用完数据之后, 需要释放锁
use Mutex;
原子引用计数
默认不能将所有权同时传递给多个线程, 如果需要的话, 可以使用Arc类型(原子引用计数):
use ;
在 mini-redis 这个教程中, 使用了原子引用计数加锁, 以提供异步中安全共享数据的功能:
type Db = ;
Bytes 来自 bytes 包, 进行数值克隆时不会克隆底层数据, 拥有更强的性能.
线程与异步
对比:
- 线程:
- 由操作系统调度
- 每个线程都有独立的栈空间
- 异步(async/await):
- 任务切换可手动控制
- 多任务共享一个线程
- 资源消耗:
- 每个线程都需要独立的栈空间, 会消耗更多的内存, 同时上下文开销也会更大
- async/await 在一个线程中运行多个任务, 切换开销小
- cpu 利用率:
- 多线程可以充分利用多核 cpu, 实现真正的并行
- async/await 依靠执行器, 单线程执行器只能利用单个核心
适用场景:
- 线程:
- CPU 密集型任务
- 需要隔离上下文
- 异步:
- IO 密集型任务
- 高并发场景
- 低延迟
异步
Rust中的异步编程是基于Future和async/await实现的. 异步函数是惰性的, 在调用后不会立即执行, 而是返回一个Future对象, 在需要的时候再执行. 有点类似于JavaScript中的Promise.
但是与其他语言不同的是, Rust中的await和async关键字是写在后面.
语法糖:
use Html;
async
Future
上面的代码实际上会被编译成一个返回Future的函数.
use Future;
use Html;
+ '_
Future Trail 的原型:
Pin
std::pin::Pin用于解决异步中的自引用结构问题, 用于确保某些类型在内存中的位置不会移动.
异步运行时
Rust默认不允许将main函数标记为async, 因为执行异步的代码需要一个运行时. 可以使用tokio.
为什么main函数不能是async的? 因为async函数返回的是一个Future, 正如上面说所的, Future是惰性的, 需要一个外部的await来触发执行. 但是main函数已经是整个程序的入口了, 没有东西可以来帮他await.
tokio
使用tokio的官方示例 mini-redis: github.com/tokio-rs/mini-redis
use Duration;
use ;
use ;
async
async
生命周期: 由于并不知道一个任务会在什么时候结束, 创建任务的时候生命周期必须为'static, 如果想引用外部变量可以使用Arc.
因为tokio::spawn可能会需要任务在线程间移动, 所以要求.await中调用的所有数据都实现Send特征.
比如说没有实现Send特征的Rc就会报错:
async
// ...
只需要改成Arc即可, 或者用大括号括起来, 让他生命周期提前结束.
循环读取连接中的数据帧, 并且使用异步来避免阻塞, 因为 while 循环会占用整个进程, 所以这个函数并不会被结束销毁.
type Db = ;
async
Tokio 的信息通道(channel)
Tokio 提供了多种信道来应对多种不同的场景
- mpsc: 多生产者, 单消费者
- onshot: 单生产者, 单消费者
- boradcast: 多生产者, 多消费者
- watch: 单生产者, 单消费者
std::sync::mpsc和crossbeam::channel是同步阻塞的通道, 不适合async编程, 而tokio::sync::mpsc是异步非阻塞的通道
在 await 期间持有锁
下面的代码会引发报错:
async
由于.await可能会被发送到另一个线程上执行, 但是std::sync::MutexGuard并没有实现Send特征.
可以让.await在调用前就把锁释放掉:
async
或者, 可以使用 Tokio 提供的异步锁tokio::sync::Mutex, 但是性能开销会更大.
Tokio 的异步 IO
文件异步读取:
AsyncReadExt::read()每次都会返回读取了多少字节, 如果返回的是 0, 则表示字节流已经关闭
use ;
async
文件异步写入:
use ;
async
实现一个回声服务:
服务端:
use ;
async
客户端:
use ;
async
在上面的代码中, 服务端使用TcpStream::spilt, 开销很小, 但是只能在同一个任务中使用. 客户端使用io::spilt, 内部由Arc和Mutex实现, 开销较大, 但是可以在任务间移动.
特性
零成本抽象
零成本抽象(Zero-cost abstractions)是Rust的一个重要的设计原则, 意味着抽象的代价应该为零, 比起手动编写低级代码不会因为使用抽象而导致性能下降.
- 你不使用的, 你不需要付出代价
- 你使用的, 你无法手写得更好
如何实现的:
- 编译时检查: 所有权和借用规则在编译时执行, 不需要运行时的垃圾回收
- 泛型单态化: 在编译时对每种类型都生成一个特定的函数版本
- ...
与其他语言的对比, 抽象的代价:
C++: 同样支持零成本抽象Java/C#: 运行时和垃圾回收开销Python/JavaScript: 动态类型, 有抽象开销
遇到的一些问题
rustc 版本不同
-
当一个项目前后被两个两个不同版本的编译器编译过后就会出现问题, 提示库文件错误(实际上是可以通过编译的), 并且 rust-analyzer 暴毙
具体错误信息如下found crate `scopeguard` compiled by an incompatible version of rustc the following crate versions were found: crate `scopeguard` compiled by rustc 1.88.0-nightly (d5b4c2e4f 2025-04-02) please recompile that crate using this compiler (rustc 1.88.0-nightly (934880f58 2025-04-09))解决方法很简单
cargo clean && cargo build, 然后重启 rust-analyzer 就好了
参考
Rust 程序设计语言 简体中文版
Rust | 绝对值_x 的博客
Rust 语言圣经(Rust Course)
python 之字符串(str)和编码_python str 编码-CSDN 博客
Rusty Book(锈书)
Asynchronous Programming in Rust
Rust Exercises
通过例子学 Rust 中文版
Rust 入门秘籍
The Rust Reference
Rust 中常见的有关生命周期的误解
Pin, Unpin, and why Rust needs them