Rust

Rust! 启动!

循环

for循环

for i in 0..10 {
    println!("{}", i);
}

0..10类似与python中的range(0, 10),左闭右开区间.

while循环

let mut i = 0;
while i < 10 {
    println!("{}", i);
    i += 1;
}

python中的while循环类似.

loop循环

let mut i = 0;
loop {
    println!("{}", i);
    i += 1;
    if i >= 10 {
        break;
    }
}

死循环, 相当于python中的while True, 通过break跳出循环.


loop循环中返回值

let mut i = 0;
let result = loop {
    i += 1;
    if i == 10 {
        break i * 2;
    }
};
println!("{}", result); // 20

loop循环中使用break的时候还可以添加一个返回值.(类似一个简化的循环函数?)

循环标签

'outer: for i in 0..10 {
    'inner: for j in 0..10 {
        if i == 5 && j == 5 {
            break 'outer;
        }
        println!("i: {}, j: {}", i, j);
    }
}

在循环前面加上单引号',可以精确的控制循环的跳出位置.

遍历集合中的元素

let arr = [1, 2, 3, 4, 5];
for v in arr {
    println!("{}", v);
}

遍历数组中的元素, 类似于python中的for v in arr.

反转 range

for i in (0..10).rev() {
    println!("{}", i);
}

rev方法可以反转range的顺序.

for循环的enumerate方法

let arr = [1, 2, 3, 4, 5];
for (i, v) in arr.iter().enumerate() {
    println!("index: {}, value: {}", i, v);
}

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
fn main() {
    let  r;             // 'a
                        //  |
    {                   //  |  'b
        let x = 5;      //  |   |
        r = &x;         //  |   |
    }                   //  |  'b
                        //  |
    println!("{r}");    // 'a
}

显式生命周期注解

在函数声明中使用生命周期注解来声明参数的生命周期

// 表示返回值与两个输入参数的生命周期一样长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

在下面的这个例子中, 因为不能多次可变借用不能通过编译.
但是在代码中想要借用的是remainder中的某个不重复的字节, 因为没有正确的区分生命周期, 编译器以为你是要将ByteIter整个借走, 而无法通过编译.

struct ByteIter<'a> {
    remainder: &'a [u8],
}

impl<'a> ByteIter<'a> {
    fn next(&mut self) -> Option<&u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}

fn main() {
    let mut bytes = ByteIter { remainder: b"1145" };
    let b1 = bytes.next();
    let b2 = bytes.next(); // second mutable borrow occurs here
    assert_eq!(b1, b2);
}

一个可行的解法是为返回值标注不同的生命周期, 区分结构体和结构体内的数据. 比如说下面的代码就可以通过编译.

struct ByteIter<'a> {
    remainder: &'a [u8],
}

impl<'a> ByteIter<'a> {
    // 'b 是self的生命周期; 'a 是remainder的生命周期
    // 该方法返回的是 'a, 是remainder的生命周期
    fn next<'b>(&'b mut self) -> Option<&'a u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}

fn main() {
    let mut bytes = ByteIter { remainder: b"1145" };
    let b1 = bytes.next();
    let b2 = bytes.next();
    drop(bytes); // drop 掉结构体
    assert_eq!(b1, b2); // 依然有效
}

Claude 3.7(AI) 给出的图解, 看下面的图可能更好理解

┌───────────────┐     引用      ┌──────────────┐
│静态数据 b"1145" │◄────────────┤bytes.remainder│
└───────────────┘              └──────────────┘
       ▲                             │
       │                             │
       │                       包含在 │
       │                             │
       │                             ▼
       │                      ┌──────────────┐
       │                      │ bytes 结构体  │ ←── 被drop
       │                      └──────────────┘
       │ 引用
┌──────┴──────┐
│  b1 和 b2   │
└─────────────┘

CPU 要烧了...

结构体

Rust中的结构体与其他语言的类似.

// `#[derive(Debug)]`是一个宏, 自动实现`Debug`特性, 用于调试输出.
#[derive(Debug)] // 派生 Debug 特性
struct User {
    id: u32,
    name: String,
    email: String,
}

fn main() {
    let u1 = User {
        id: 1,
        name: "".to_string(),
        email: "test@test.com".to_string(),
    };

    dbg!(u1);
}

但是结构体要求字段必须拥有所有权或是有生命周期.
比如下面的这份代码就会报错:

struct User {
    id: u32,
    name: &str,
    email: &str,
}

元组结构体

元组结构体可以没有字段名, 长的跟元组很像

#[derive(Debug)]
struct Color(u8, u8, u8);

let black = Color(0, 0, 0);
dbg!(black);

单元结构体

单元结构体没有字段, 但是可以给他添加方法

struct Greet;

impl Greet {
    fn hello() {
        println!("hello");
    }
}

枚举

枚举类型可以限制变量的取值范围

enum Gender {
    Male,
    Female,
}

fn is_man(g: Gender) -> bool { // g 只有 Male 和 Female 两种取值
    match g {
        Gender::Male => true,
        Gender::Female => false,
    }
}

fn main() {
    assert!(is_man(Gender::Male));
}

枚举类型还可以携带数据

#[repr(u8)] // 限制枚举成员的取值范围 0-255
#[derive(Clone, Copy)]
enum Week {
    Monday = 1, // 1
    Tuesday,    // 2
    Wednesday,  // 3
    Thursday,   // 4
    Friday,     // 5
    Saturday,   // 6
    Sunday,     // 7
}

impl Week {
    fn is_weekend(&self) -> bool {
        *self as u8 > 5
    }
}

fn main() {
    assert_eq!(Week::Sunday as i32, 7);
    assert!(Week::is_weekend(&Week::Saturday))
}

为什么需要 #[derive(Clone, Copy)]
因为使用 *self 时访问的是应用指向的值, 而 as u8 需要将这个 Week 枚举值移出引用, 但是 Week 没有实现 Copy 特性, 会被运动, Rust 中不允许从共享引用中移出数据(因为会导致这个数据失效)

除了内置类型之外还可以表示一些奇奇怪怪的东西...

enum Json {
    Null,
    Boolean(bool),
    Number(f64),
    String(String),
    Array(Vec<Json>),
    Object(std::collections::HashMap<String, Json>),
}

模式匹配

模式匹配是 Rust 相较于其他语言非常有特色的一个功能, 可以在保障安全性的前提下最大程度的简化代码

match

直接对表达式可能的各种情况进行拆分处理

比如说匹配一个枚举类型:

enum Gender {
    Male,
    Female,
}

fn get_pronoun(who: Gender) -> String {
    match who {
        Gender::Male => "he".to_string(),
        Gender::Female => "she".to_string(),
    }
}

除此之外, 还可以用各种奇奇怪怪的方式匹配各种奇奇怪怪的东西, 比如说匹配一个 32 位有符号整形

fn handler_num(num: i32) -> String {
    match num {
        0 => "zero".to_string(),              // 单值匹配
        1..=4 => "small".to_string(),         // 范围匹配(1~4)
        5 | 6 => "medium".to_string(),        // 多值匹配
        n @ 7..=9 => format!("large: {}", n), // 绑定到变量n
        _ => "unknown".to_string(),           // 通配符
    }
}

解构

在 match 匹配的时候还可以附加要绑定的参数来直接解构这个匹配

struct Nums(i32, i32, i32); // 普通结构体同理

fn main() {
    let new_nums = Nums(1, 2, 3);

    match new_nums {
        Nums(x, y, z) => {
            println!("{}, {}, {}", x, y, z)
        }
        _ => {
            println!("Others")
        }
    }
}

如果需要忽略部分值:

fn main() {
    let (x, _, z) = (1, 2, 3); // 忽略单个值
    let (x, ..) = (1, 2, 3);   // 忽略后面所有值
}

if let

if let ...表达式可以快速拆开可能的值, 如果成功拆开了会直接判断给出的条件为true (while let ...同理)

fn main() {
    let a: Option<&str> = Some("1145");
    let b: Option<&str> = None;
    if let Some(s) = a {
        println!("{}", s);
    }
    if let Some(s) = b {
        // 这里的代码不会被执行
        println!("{}", s);
    }
}

泛型

泛型是一种在编译时不知道具体类型的抽象类型.

// 添加 PartialOrd 和 Copy 特征约束, 保证 T 类型是可以比较大小并且可以复制的
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];
    for item in list.iter() {
        // 比较 item 和 largest
        if item > &largest {
            largest = *item; // 将 item 解引用并赋值给 largest
        }
    }
    largest
}

fn main() {
    let nums = [1, 2, 3, 4, 5];
    let result = largest(&nums);
    println!("最大值是: {}", result);
}

泛型的常见用法, 可以实现类似 C 语言中的函数重载的效果

pub enum HttpMethod {
    Get,
    Post,
    NoSupport,
}

impl HttpMethod {
    /// 只要支持转化为HttpMethod就可以作为这个函数的参数
    /// 'method' can be anything which can into `HttpMethod`
    pub fn is_support<T: Into<HttpMethod>>(method: T) -> bool {
        let m: HttpMethod = method.into();
        m != HttpMethod::NoSupport
    }
}

/// 实现&str到HttpMethod的转换
impl From<&str> for HttpMethod {
    fn from(value: &str) -> Self {
        match value {
            "GET" => HttpMethod::Get,
            "POST" => HttpMethod::Post,
            _ => HttpMethod::NoSupport,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_method_is_support() {
        assert!(HttpMethod::is_support("GET"));
        assert!(HttpMethod::is_support(HttpMethod::Get));
    }
}

泛型约束

编译器无法确定泛型的特性, 这时就需要显示指定

use std::ops::Mul;

struct Rectangle<T> {
    a: T,
    b: T,
}

impl<T> Rectangle<T>
where
    T: Mul<Output = T> + Copy,
{
    fn area(&self) -> T {
        self.a * self.b
    }
}

fn main() {
    let r = Rectangle { a: 2, b: 3 };
    println!("{}", r.area());
}

字符串

Rust的字符串有两种类型: String&str. String类型是一个拥有所有权的类型, 离开作用域会被自动释放; 而&str类型是一个借用, 没有所有权, 可以指向字符串字面量或者String类型的一部分.

  • String类型拥有所有权, 是可变的.
  • &str类型是一个不可变的引用, 适合只读场景.

为什么String类型不支持下标索引?

因为String类型使用UTF-8编码, 这是一种长度可变的编码, 一个字符可能占用多个字节.

  • ASCII 字符(英文字母、数字)占 1 个字节
  • 大多数中文字符占 3 个字节
  • 一些特殊符号和表情符号可能占 4-6 个字节

使用下标索引可能会引发误解:

let s = String::from("你好");
let c = s[0]; // 这是指第一个字节还是第一个字符?

同时, 下标索引还有效率问题, 如果需要访问某个字符, 因为编码的原因, 需要从头遍历字符串, 时间复杂度为O(n), 而不是O(1).

为什么Python中可以直接通过下标访问呢

Python中的字符串使用Unicode编码, 长度为2字节或是4字节, 每个索引都是对应一个完整的字符, 所以可以通过下标索引访问.

同样的, Python中的字符串也是不可变的, 如果需要修改字符串, 需要重新创建一个新的字符串.

s = "a"
s[0] = "b" # 报错: 未在类型"Literal['a']"上定义"__setitem__"方法

这是两种语言的设计理念不同导致的.

  • Python: 高级, 易用, 使事情简单化
  • Rust: 内存安全, 高性能, 反应真实的操作成本

同时, 字符串是一个胖指针, 在 Rust 中占用 24 字节

fn main() {
    let s = "🤯".to_string();
    println!("{:p}, {}, {}", &s, s.capacity(), size_of::<String>());
    // 输出: 0x7fff4f7b77d8, 4, 24
}

Rust 中的 String 类型由三部分组成, 每部分都是uszie(在 64 位系统上占用 8 个字节, 也就是 64 位)

┌─────────────────────────┐
│ String (栈上,24字节)     │
├─────────┬───────┬───────┤
│ 堆指针   │ 长度   │ 容量   │
│ (8字节) │ (8字节)│(8字节) │
└─────────┴───────┴───────┘
┌────────────────────────────┐
│ 字符串数据 (堆上)            │
│ "🤯" (实际占4字节)          │
└────────────────────────────┘

字符串切片

let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{}, {}", hello, world);

字符串切片的语法类似于python中的切片, 都是截取一段左闭右开区间.
字符串切片返回的是str类型, 这个类型在编译时不知道大小, 无法通过编译, 所以需要使用&符号.

字符串修改

  • 转换为Vec<char>类型
    进行这的转换之后, 对于其他语言的用户来说就十分亲切了. Char类型的长度固定为4个字节, 可以通过下标进行O(1)复杂度的标索引访问.

     let s = String::from("你好👋");
     // 使用 char() 方法, 返回一个迭代器, 这个迭代器按照字符顺序遍历 String
     // 并返回一个 char 类型的值, 由 collect() 收集到一个集合中
     let mut s1: Vec<char> = s.chars().collect();
     s1[2] = '😊';
     let s2: String = s1.iter().collect();
     assert_eq!(s2, "你好😊");
    

closure

closure(闭包)是一种匿名函数, 可以捕获上下文中的变量.
closure 由两部分组成: 参数列表(两条竖线中的变量)和函数体(后面的表达式).

let base = 42;
let 你好 = |x| x + base; // 直接使用变量 base
assert_eq!(你好(1), 43);

对于 cpp 用户而言, 可以这样理解:

#include "iostream"

signed main() {
    int base = 42;
    auto 你好 = [&](int x) -> int { return base + x; };
    std::cout << 你好(1) << std::endl;
    return 0;
}

返回闭包

返回一个闭包(略感震惊, 这不应该是 js, py 这些脚本语言才有的吗)

fn warp<'a>(s: &'a str) -> impl Fn(&str) -> bool + 'a {
    move |new_s| new_s == s
}

fn main() {
    let a = warp("Hi"); // `a` is a closure
    println!("{:?}", a("Hi")); // call `a`

    // 上面的代码也可以写作一行
    println!("{:?}", warp("Hi")("Hi"));
}

trait

trait(特质?)是Rust中定义共享行为的方式, 类似于其他语言中的interface(接口).
特质定义了一系列方法, 如果要实现这个特质, 需要实现这些方法.

实现一个trait:

trait Hi {
    const WORD: &str;
    fn greet(&self);
    fn reply(&self, s: &Student);
}

struct Student {
    name: String,
}

impl Hi for Student {
    const WORD: &str = "你好";
    fn greet(&self) {
        println!("{}", Student::WORD);
    }
    fn reply(&self, s: &Student) {
        println!("{} too, {}", Student::WORD, s.name);
    }
}

fn main() {
    let s1 = Student {
        name: "法外狂徒".to_string(),
    };
    let s2 = Student {
        name: "张三".to_string(),
    };
    s1.greet();
    s2.reply(&s1);
}

trait 的作用域

Rust 允许在任何地方实现 trait, 如果需要使用这个 trait 提供的功能, 可能需要导入特定的模块.

use std::io::Write;

fn main() {
    let mut buf: Vec<u8> = Vec::new();
    // write_all() 在 Write 中实现
    buf.write_all(b"buf").unwrap();
}

std::io::impls中的相关代码:

#[stable(feature = "rust1", since = "1.0.0")]
impl<A: Allocator> Write for Vec<u8, A> {
    // ...

    #[inline]
    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
        self.extend_from_slice(buf);
        Ok(())
    }

    // ...
}

trait 方法可见性

Rust中, 需要使用一个trait的方法, 那么这个trait必须在当前作用域中可见.

具体见下面的例子:

trait A: std::io::Write {
    fn mytest(&self);
}

impl A for Vec<u8> {
    fn mytest(&self) {
        println!("hi");
    }
}

fn main() {
    let mut buf: Vec<u8> = Vec::new();
    // 下面这行代码无法通过编译
    // buf.write_all(b"buf").unwrap();
    buf.mytest();
}

虽然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 objectRust中的动态分发机制(dynamic dispatch), 只要实现了相同的trait, 就可以在运行时处理不同类型的值.

组成, trait object由两部分组成:

  • 数据指针: 指向实际对象数据的指针
  • 虚函数表指针(vtable): 包含了该trait所需方法的函数指针表
┌─────────────┬─────────────┐
│ 数据指针     │ 虚表指针     │
└─────────────┴─────────────┘
       │               │
       ▼               ▼
┌─────────────┐  ┌─────────────────────┐
│ 实际数据     │  │ 虚表 (vtable)        │
└─────────────┘  ├─────────────────────┤
                 │ 方法1的函数指针      │
                 │ 方法2的函数指针      │
                 │ Drop实现的指针       │
                 │ 类型大小信息         │
                 │ 类型对齐信息         │
                 └─────────────────────┘

因为trait object实际上由两个指针组成, 所以是一个fat pointer(胖指针).

通常有两种形式:

  • &dyn Trait: 引用形式(不可变)
  • &mut dyn Trait: // 调用所有权形式(可变)
struct A {
    id: String,
}

trait Show {
    fn id(&self) -> &str;
    fn show(&self) {
        println!("{}", self.id());
    }
}

trait X: Show {
    fn x(&self) {
        println!("X");
    }
}

trait Y: Show {
    fn y(&self) {
        println!("Y");
    }
}

impl A {
    fn a() {
        println!("A");
    }
}

impl X for A {}

impl Y for A {}

impl Show for A {
    fn id(&self) -> &str {
        &self.id
    }
}

fn main() {
    let a: &dyn X = &A {
        id: "114514".to_string(),
    };
    a.x(); // vtable 访问方法
    a.show(); // 通过方法访问数据
    // a.y(); // 不可用
    // a.a(); // 不可用
}

返回 trait

可以在函数中返回一个 trait, 让编译器自己推导具体类型, 让接口更加灵活 (说起来这一部分(Trait)是不是可以直接和泛型部分和并到一起写?)

特点:

  • 类型隐藏和抽象
  • 简化类型签名
  • 自动生命周期绑定

例如, 返回一个哈希表的迭代器:

pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
    self.hash_map.iter()
}

上面的代码等价于

pub fn iter(&self) -> std::collections::hash_map::Iter<'_, String, String> {
    self.hash_map.iter()
}

迭代器

迭代器允许访问一个连续的集合, 只需要关心如何处理每个元素, 而不需要关心集合的内部结构.
同时, 迭代器是惰性的, 只有在需要的时候才会计算下一个元素.

iter方法

iter方法返回一个不可变的迭代器, 正因如此, 迭代器不会取得所有权.
而直接访问集合的写法会取得所有权, 导致集合无法再次使用.

let arr = [1, 2, 3, 4, 5];
for v in arr.iter() {
    println!("{}", v);
}

关于所有权的进一步说明 对于实现了Copy特性的类型不使用iter方法也没有什么太大的问题(对于那些存储在栈上的数据), 但是对于其他的可变类型就会引发错误, 比如说Vec类型.
虽然i32类型实现了Copy特性, 但是Vec类型并没有没有实现Copy特性.
所以下面的代码的最后一行会引发报错.

let numbers: [i32; 4] = [1, 2, 3, 4];
for i in numbers {
    println!("{}", i);
}
println!("{:#?}", numbers);

let numbers: Vec<i32> = vec![1, 2, 3, 4];
for i in numbers {
    println!("{}", i);
}
// 下面一行代码无法通过编译
// println!("{:#?}", numbers); // borrow of moved value: `numbers` value borrowed here after move

next方法

Python类似, Rust的迭代器是通过next方法来获取下一个元素的.
有点类似鸭子类型, 这是Python等动态语言的一个特性, "只要长得像鸭子, 走路像鸭子, 就可以认为是鸭子".

手动迭代:

let arr: [i32; 5] = [1, 2, 3, 4, 5];
let mut iter = arr.into_iter();

loop {
    match iter.next() {
        Some(v) => {
            println!("{v}")
        }
        None => {
            println!("end");
            break;
        }
    }
}

因为next方法返回的是Option类型, 所以需要手动处理SomeNone两种情况.
同时, 因为next方法会改变状态数据, 需要标注为mut.

区分iter, iter_mut, into_iter

  • iter: 返回一个不可变的迭代器, 属于引用迭代器
  • iter_mut: 返回一个可变的迭代器, 属于可变引用迭代器
  • into_iter: 返回一个所有权迭代器, 会消耗所有权

迭代器适配器与消费者适配器

  • 迭代器适配器: 会返回一个新的迭代器, 用于链式调用
  • 消费者适配器: 会消费迭代器, 返回一个非迭代器的值
let arr: Vec<i32> = vec![-2, -1, 0, 1, 2];
let new_arr: Vec<i32> = arr
    .iter()
    .map(|&v| i32::abs(v))
    .filter(|v| *v < 2)
    .collect();

println!("{:#?}", new_arr);

在这个例子中, mapfilter方法是迭代器适配器, collect方法是消费者适配器.

实现一个迭代器

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    // 必须实现的方法
    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }

    // 其他默认实现的方法...
}

fn main() {
    let mut counter = Counter::new();

    assert_eq!(counter.next(), Some(1));
    assert_eq!(counter.next(), Some(2));
    assert_eq!(counter.next(), Some(3));
    assert_eq!(counter.next(), None);
}

错误处理

Rust 强制要求在代码中处理所有可能的错误(相当于其他语言强制对危险操作使用 try 语句)

Result <T,E> 类型

Result<T, E>是 Rust 错误处理的核心机制, 表示操作可能的两种结果: 成功Ok(T)和失败Err(E).

一个尝试绑定端口的栗子:

use std::{error::Error, net::TcpListener};

/// 返回一个Result, 这个Result展开可能会有两种结果
///
/// 如果成功返回(), 表示unit类型, 相当于没有返回值
/// 如果失败返回Box<dyn Error>, 这是一个指向错误类型的指针
fn test_port(s: &str) -> Result<(), Box<dyn Error>> {
    match TcpListener::bind(s) {
        Ok(_) => Ok(()),
        Err(e) => Err(Box::new(e)),
    }
}

fn main() {
    // 尝试本地的8080端口, 并直接使用`unwrap()`展开Result, 如何失败会直接panic, 中断整个程序
    test_port("127.0.0.1:8080").unwrap();
}

在 Rust 中有很多种方法可以处理错误, 常见的有: unwrap, expect, and_then...

语法糖

使用?操作符可以快速向上传播错误, 上面的栗子中的函数也可以写成

fn test_port(s: &str) -> Result<(), Box<dyn Error>> {
    TcpListener::bind(s)?;
    Ok(())
}

需要注意的是, ?操作符只能在返回值为 Result 或是 Option 的函数中使用

Option <T>类型

Option 与 Result 不同的是, Option 表示可能有值, 也可能没有

use std::{fs::File, io::Read, path::Path};

fn find_file(path: &str) -> Option<File> {
    let path = Path::new(path);
    // 不是文件就返回空值
    if !path.is_file() {
        return None;
    }
    File::open(path).ok() // ok方法可以将Result转换为Option
}

fn main() {
    let file_name = "./1.in";
    // 模式匹配
    if let Some(mut file) = find_file(file_name) {
        println!("found file '{}': ", file_name);
        let mut content = String::new();
        file.read_to_string(&mut content).unwrap();
        println!("{}", content);
    }
}

panic

触发 panic 会直接退出整个程序

fn main() {
    let a = [1];
    if !a.is_empty() {
        panic!(); // 使用panic!宏
    }
    println!("这行不会被输出");
}

模块

将代码分割成不同的模块, 有助于提高代码的可读性和可维护性.

mod

使用mod关键字定义一个模块

// src/main.rs
// 将自动在 src/mytest 和 src/mytest/mod.rs 中寻找
mod mytest;

fn main() {}

pub

使用pub关键字将函数暴露出去

// src/mytest/mod.rs
pub fn hi() {
    println!("hi");
}

use

使用use关键字引入模块中的函数

// src/main.rs
use mytest::hi;

mod mytest;

fn main() {
    hi();
}

整体的结构像是这样:

src/
  ├── main.rs
  └── mytest/
      └── mod.rs

当然, 把mytest文件夹直接写成mytest.rs也是可以的.

mod.rs文件中使用pub mod xxx;来导出

智能指针

Box

Box允许将数据存储在堆上.

fn main() {
    // 静态数组, 数据存储在栈上
    let a = [0; 100];
    let b = a;
    println!("{a:?}");
    println!("{b:?}");

    // 使用 Box 分配到堆上
    let a = Box::new([0; 100]);
    let b = a;
    // 因为所有权发生了转移, 下面这行会报错
    // println!("{a:?}");
    println!("{b:?}");
}

Box还可以将动态大小的类型转变为固定大小的类型.
Rust中几乎所有的类型都需要在编译时知道大小, 但是Box本质是一个智能指针, 指向List类型的数据, 这个指针的大小是固定的.

// 有效的链表实现
struct Node {
    value: i32,
    next: Option<Box<Node>>, // Box 指针,大小固定
}

其他解决方案:

  • 使用间接引用:

    struct Node<'a> {
        value: i32,
        next: &'a Node<'a>,
    }
    

Rc 与 Arc

有时候一个数据可能会需要多个所有者, 但是Rust中的所有权规则不允许这种情况, 这时就需要使用Rc(reference counting).
Rc是一个引用计数智能指针, 允许多个所有者共享数据.
Arc则是Rc的多线程版本, 允许多个线程共享数据.

use std::rc::Rc;

fn main() {
    let a = Rc::new(114);
    let b = Rc::clone(&a);

    // 对底层数据的使用导致计数增加
    assert_eq!(2, Rc::strong_count(&a));
    assert_eq!(2, Rc::strong_count(&b));
}

上面的代码中, Rc::clone方法为浅拷贝, 仅复制智能指针并增加引用计数, 而不会复制底层数据.
但是, Rc虽然有所有权, 但是只可以只读访问, 无法修改. (这和引用有什么区别嘛!)
如果需要修改数据, 则需要使用RefCell或是Mutex来实现.

Cell 与 RefCell

CellRefCellRust中的内部可变性机制, 可以在拥有不可变引用时修改目标数据.
内部使用了unsafe来实现这一点.

Cell: 适用于实现了Copy特性的类型.

use std::cell::Cell;

fn main() {
    let s = Cell::new("hi");
    println!("{}", s.get());
    s.set("hi!");
    println!("{}", s.get());
}

RefCell: 适用于没有实现Copy特性的类型.

use std::cell::RefCell;

fn main() {
    let s = RefCell::new("hi".to_string());

    let s1 = s.borrow();
    println!("{}", s1);

    // 同一时刻只允许一个借用, 如果注释掉下面这行, 会 panic
    drop(s1);

    let mut s2 = s.borrow_mut();
    *s2 = "hi!".to_string();
    println!("{}", s2);
}

RefCell可以在编译时跳过规则检查, 但是如果违背了规则仍然会导致运行时panic.

Rust 中的宏分为两大类:

  • 声明宏(declarative macros)
  • 三种过程宏(procedural macros)
    • 派生宏: #[derive]
    • 类属性宏: 定义自定义属性
    • 类函数宏: 类似函数调用的宏

声明宏

使用类似于正则匹配加上match表达式的语法进行匹配, 并执行相关代码.

#[macro_export] // 注解, 声明这个宏被引入作用域
macro_rules! my_vec {
    // 与正则表达式类似
    ($($x:expr),*) => {
        // 被替换的代码
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )* // 匹配若干次
            temp_vec
        }
    };
}

fn main() {
    let a = my_vec![1, 1, 4];
    dbg!(a);
}

过程宏

Deref

Deref是一个trait, 实现Deref这个trait允许我们重载解引用运算符(*).

use std::ops::Deref;

// 元组结构体, 在实现 Rust 的 Deref trait 时,可以让自定义类型行为类似于引用
#[derive(Debug, PartialEq)]
struct MyBox<T>(T);

impl<T> MyBox<T> {
    pub fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let a = MyBox::new(10);
    let b = &a;
    // 使用 *b 的时候其实就是变成了 *(b.deref())
    // 调用 deref 函数后返回一个 T 类型的指针, 再对这个指针解引用就是实际的值了
    assert_eq!(a, *b);
}

强制转换

Deref还有一个重要功能, 将一个类型的引用传递给不同类型的时候, 如果源类型实现了Deref, 并且Deref可以进一步转换为目标类型, 那么Rust会自动进行Deref操作.

比如说这个函数需要的是一个&str类型

fn hello(s: &str) {
    println!("Hello, {s}");
}

但是调用里传递的是一个String类型

let s = MyBox::new(String::from("Rust"));
hello(&s);

那么就会自动进行转换:

  1. &MyBox<String> -> &String: 通过MyBox实现的Deref
  2. &String -> &str: 通过String实现的Deref

如果不使用自动的类型转换, 那么就需要手动解引用, 类似于:

hello(&(*(*s))); // 好抽象的代码
// or
hello(&s.deref().deref()); // this expression creates a reference which is immediately dereferenced by the compiler

同时, 因为Deref的解析都发生在编译时, 所以不会有运行时的性能损耗.

cargo

cargoRust的包管理器

Cargo.tomlCargo.lockcargo的元数据文件, 与JavaScriptpackage.jsonpackage-lock.json类似.

是否上传Cargo.lock?

  • 如果是依赖库: 不要上传
  • 如果是用户程序: 需要上传

如果是依赖库上传Cargo.lock会锁定依赖, 对于使用同一个上游的库, 可能会造成依赖冲突.

多线程

主线程结束后, 所有的子线程也会结束, 除非使用join方法等待子线程结束.

use std::{thread, time::Duration};

fn main() {
    let handle = thread::spawn(|| {
        for i in 0..10 {
            println!("{i}"); // 输出到5就没了
            thread::sleep(Duration::from_millis(2));
        }
    });

    // handle.join().unwrap();

    for i in 0..10 {
        println!("{i}");
        thread::sleep(Duration::from_millis(1));
    }
}

并发与并行

  • 并发(Concurrent): 交替执行多个任务, 但是在同一时刻只有一个任务在执行
  • 并行(Parallel): 同一时刻有多个任务在执行

多线程的风险

  • 数据竞争: 多个线程同时访问同一块内存, 会导致数据不一致(Rust的所有权机制可以避免数据竞争)
  • 死锁: 两个线程相互等待对方释放资源
  • 活锁: 两个线程不断相应对方的动作, 但是无法继续执行有意义的工作
  • 饥饿: 一个线程无法获得所需的资源

生命周期

闭包可以捕获当前环境的变量, 但是因为不知道这个变量什么时候会被销毁, 所以需要让线程的闭包获得所有权.

use std::thread;

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // 下面这行代码会报错
    // let handle = thread::spawn(|| println!("{:?}", v));

    let handle = thread::spawn(move || println!("{:?}", v));

    handle.join().unwrap();
}

信道 channel

信道(channel)是一种线程间通信的方式, 用于在多个线程之间传递消息.
使用std::sync::mpsc::channel创建一个信道, 返回两个端点: 发送端和接收端.

  • 接收消息:
    • recv: 阻塞等待消息
    • try_recv: 非阻塞等待消息(立即返回Result)
// mpsc 是多个生产者, 单个消费者的缩写(multiple producer, single consumer)
use std::{sync::mpsc::channel, thread::spawn};

fn main() {
    let (tx, rx) = channel();

    spawn(move || {
        let val = String::from("Hi");
        tx.send(val).unwrap();
        // send() 会消耗所有权
        // println!("{val}");
    });

    let received = rx.recv().unwrap();
    println!("Get: '{received}'");
}

使用for循环迭代接收端, clone发送端从多个生产者发送消息:

use std::{sync::mpsc, thread, time::Duration};

fn main() {
    let (tx, rx) = mpsc::channel();

    let vals = vec![
        String::from("Hi"),
        String::from("from"),
        String::from("the"),
        String::from("thread"),
    ];

    let tx1 = tx.clone();
    let vals1 = vals.clone();
    thread::spawn(move || {
        for val in vals1 {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_micros(300));
        }
    });

    thread::spawn(move || {
        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_micros(300));
        }
    });

    for msg in rx {
        println!("{msg}");
    }
}

互斥锁

互斥锁(mutex)是一种并发原语, 用于在多个线程之间共享访问数据.

  • 在使用数据之前, 需要先获取锁
  • 使用完数据之后, 需要释放锁
use std::sync::Mutex;

fn main() {
    // Mutex<T> 智能指针
    let data = Mutex::new(5);

    {
        let mut num = data.lock().unwrap(); // 获得锁
        *num = 6;
    }

    println!("{data:?}");
}

原子引用计数

默认不能将所有权同时传递给多个线程, 如果需要的话, 可以使用Arc类型(原子引用计数):

use std::{
    sync::{Arc, Mutex},
    thread,
};

fn main() {
    let cnt = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let cnt_clone = Arc::clone(&cnt); // 原子引用计数
        let handle = thread::spawn(move || {
            let mut num = cnt_clone.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *cnt.lock().unwrap());
}

在 mini-redis 这个教程中, 使用了原子引用计数加锁, 以提供异步中安全共享数据的功能:

type Db = Arc<Mutex<HashMap<String, Bytes>>>;

Bytes 来自 bytes 包, 进行数值克隆时不会克隆底层数据, 拥有更强的性能.

线程与异步

对比:

  • 线程:
    • 由操作系统调度
    • 每个线程都有独立的栈空间
  • 异步(async/await):
    • 任务切换可手动控制
    • 多任务共享一个线程
  1. 资源消耗:
    • 每个线程都需要独立的栈空间, 会消耗更多的内存, 同时上下文开销也会更大
    • async/await 在一个线程中运行多个任务, 切换开销小
  2. cpu 利用率:
    • 多线程可以充分利用多核 cpu, 实现真正的并行
    • async/await 依靠执行器, 单线程执行器只能利用单个核心

适用场景:

  • 线程:
    • CPU 密集型任务
    • 需要隔离上下文
  • 异步:
    • IO 密集型任务
    • 高并发场景
    • 低延迟

异步

Rust中的异步编程是基于Futureasync/await实现的. 异步函数是惰性的, 在调用后不会立即执行, 而是返回一个Future对象, 在需要的时候再执行. 有点类似于JavaScript中的Promise.
但是与其他语言不同的是, Rust中的awaitasync关键字是写在后面.

语法糖:

use trpl::Html;

async fn page_title(url: &str) -> Option<String> {
    let response = trpl::get(url).await;
    let response_text = response.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

Future

上面的代码实际上会被编译成一个返回Future的函数.

use std::future::Future;
use trpl::Html;

fn page_title(url: &str) -> impl Future<Output = Option<String>> + '_ {
    async move {
        let text = trpl::get(url).await.text().await;
        Html::parse(&text)
            .select_first("title")
            .map(|title| title.inner_html())
    }
}

Future Trail 的原型:

pub trait Future {
    type Output;
    fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output>;
}

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 std::time::Duration;

use mini_redis::{Connection, Frame};
use tokio::{
    net::{TcpListener, TcpStream},
    spawn,
    time::sleep,
};

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap();

    loop {
        let (socket, _) = listener.accept().await.unwrap();
        spawn(async move { process(socket).await }); // 创建一个异步任务, 防止阻塞
    }
}

async fn process(socket: TcpStream) {
    let mut connection = Connection::new(socket);

    if let Some(frame) = connection.read_frame().await.unwrap() {
        println!("GOT: {:?}", frame);

        sleep(Duration::from_secs(3)).await;

        let response = Frame::Error("unimplemented".to_string());
        connection.write_frame(&response).await.unwrap();
    }
}

生命周期: 由于并不知道一个任务会在什么时候结束, 创建任务的时候生命周期必须为'static, 如果想引用外部变量可以使用Arc.

因为tokio::spawn可能会需要任务在线程间移动, 所以要求.await中调用的所有数据都实现Send特征.
比如说没有实现Send特征的Rc就会报错:

#[tokio::main]
async fn main() {
    // ...

    loop {
        let (socket, _) = listener.accept().await.unwrap();
        spawn(async move {
            let rc = rc::new(1145);
            println!("{:?}", rc);
            process(socket).await
        });
    }
}

// ...

只需要改成Arc即可, 或者用大括号括起来, 让他生命周期提前结束.

循环读取连接中的数据帧, 并且使用异步来避免阻塞, 因为 while 循环会占用整个进程, 所以这个函数并不会被结束销毁.

type Db = Arc<Mutex<HashMap<String, Bytes>>>;
async fn process(socket: TcpStream, db: Db) {
    use mini_redis::Command::{self, Get, Set};

    let mut connection = Connection::new(socket);

    // 阻塞接收数据
    while let Some(frame) = connection.read_frame().await.unwrap() {
        let response = match Command::from_frame(frame).unwrap() {
            // 如果是 set 操作
            Set(cmd) => {
                let mut db = db.lock().unwrap(); // 先获得锁
                db.insert(cmd.key().to_string(), cmd.value().clone());
                Frame::Simple("OK".to_string())
            }
            // 如果是 get 操作
            Get(cmd) => {
                let db = db.lock().unwrap();
                if let Some(valve) = db.get(cmd.key()) {
                    Frame::Bulk(valve.clone())
                } else {
                    Frame::Null
                }
            }
            // 其他
            cmd => Frame::Error(format!("unimplemented {:?}", cmd)),
        };

        // 向连接中写入数据
        connection.write_frame(&response).await.unwrap();
    }
}

Tokio 的信息通道(channel)

Tokio 提供了多种信道来应对多种不同的场景

  • mpsc: 多生产者, 单消费者
  • onshot: 单生产者, 单消费者
  • boradcast: 多生产者, 多消费者
  • watch: 单生产者, 单消费者

std::sync::mpsccrossbeam::channel是同步阻塞的通道, 不适合async编程, 而tokio::sync::mpsc是异步非阻塞的通道

在 await 期间持有锁

下面的代码会引发报错:

async fn process(mutex: &Mutex<i32>) {
    let mut lock: MutexGuard<i32> = mutex.lock().unwrap();
    *lock += 1;
    other_async().await;
}

由于.await可能会被发送到另一个线程上执行, 但是std::sync::MutexGuard并没有实现Send特征.

可以让.await在调用前就把锁释放掉:

async fn process(mutex: &Mutex<i32>) {
    {
        let mut lock: MutexGuard<i32> = mutex.lock().unwrap();
        *lock += 1;
    }
    other_async().await;
}

或者, 可以使用 Tokio 提供的异步锁tokio::sync::Mutex, 但是性能开销会更大.

Tokio 的异步 IO

文件异步读取:
AsyncReadExt::read()每次都会返回读取了多少字节, 如果返回的是 0, 则表示字节流已经关闭

use tokio::{fs::File, io::AsyncReadExt};

#[tokio::main]
async fn main() {
    let mut f = File::open("1.in").await.unwrap();
    let mut buf = [0; 10]; // 缓冲区
    let mut data = Vec::new(); // 读取到的数据

    loop {
        // AsyncReadExt::read() 每次都会返回读取了多少位
        let n = f.read(&mut buf).await.unwrap();
        if n == 0 {
            break;
        }
        for i in buf {
            data.push(i);
        }
    }

    let content = String::from_utf8_lossy(&data);
    println!("{}", content);
}

文件异步写入:

use tokio::{fs::File, io::AsyncWriteExt};

#[tokio::main]
async fn main() {
    let mut file = File::create("1.out").await.unwrap();

    let n = file.write("🤔".as_bytes()).await.unwrap();
    println!("Wrote {} bytes", n);
}

实现一个回声服务:

服务端:

use tokio::{io, net::TcpListener};

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:6321").await.unwrap();

    loop {
        let (mut socket, _) = listener.accept().await.unwrap();

        tokio::spawn(async move {
            // 需要两个可变引用, 拆分成一个读一个写
            let (mut rd, mut wr) = socket.split();
            // 直接从 rd 复制到 wr
            io::copy(&mut rd, &mut wr).await.unwrap();
        });
    }
}

客户端:

use tokio::{
    io::{self, AsyncReadExt, AsyncWriteExt},
    net::TcpStream,
};

#[tokio::main]
async fn main() {
    let socket = TcpStream::connect("127.0.0.1:6321").await.unwrap();
    let (mut rd, mut wr) = io::split(socket);

    // 创建一个异步任务, 在后台自动写入
    tokio::spawn(async move {
        wr.write_all(b"hello\r\n").await.unwrap();
    });

    // 从服务端获取反馈
    loop {
        let mut buf = vec![0; 10];
        let n = rd.read(&mut buf).await.unwrap();

        if n == 0 {
            break;
        }

        let message = String::from_utf8_lossy(&buf[..n]);
        println!("GOT: {}", message);
    }
}

在上面的代码中, 服务端使用TcpStream::spilt, 开销很小, 但是只能在同一个任务中使用. 客户端使用io::spilt, 内部由ArcMutex实现, 开销较大, 但是可以在任务间移动.

特性

零成本抽象

零成本抽象(Zero-cost abstractions)是Rust的一个重要的设计原则, 意味着抽象的代价应该为零, 比起手动编写低级代码不会因为使用抽象而导致性能下降.

  1. 你不使用的, 你不需要付出代价
  2. 你使用的, 你无法手写得更好

如何实现的:

  • 编译时检查: 所有权和借用规则在编译时执行, 不需要运行时的垃圾回收
  • 泛型单态化: 在编译时对每种类型都生成一个特定的函数版本
  • ...

与其他语言的对比, 抽象的代价:

  • 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