问答环节

1.const x = 1;和let x = 1有何区别呢,const申请的是全局变量所以会一直有效对吗?

const 声明的常量具有全局作用域,但它们不能直接在函数内部声明。常量通常用于定义整个程序中使用的值,如配置常量或数学常量。
let 声明的变量具有局部作用域,它们只能在声明它们的代码块或函数内部使用。
const 声明的常量没有生命周期的概念,因为它们在编译时就已经确定,并且存储在程序的二进制代码中。
let 声明的变量具有生命周期,它们的生命周期由 Rust 的所有权系统管理。变量的生命周期从声明点开始,到它们离开作用域结束。

2.#[derive(Serialize)]到底是怎么工作的?

在 Rust 中,#[derive(Serialize)] 是一个属性宏(attribute macro),用于自动结构体或枚举类型实现 serde::Serialize trait。这个 trait 是来自于 serde 库的,它是一个流行的 Rust 序列化框架,允许你将 Rust 数据结构转换为各种格式,如 JSON、YAML、XML 等。
当你在结构体或枚举上使用 #[derive(Serialize)] 时,Rust 编译器会自动生成实现 Serialize trait 所需的代码。这意味着你可以在不手动编写序列化逻辑的情况下,将你的数据结构转换为字符串或其他序列化格式。
如果结构体不在这个宏下声明就需要手动实现序列化方法
反序列化和序列化一般会同时声明:

use serde_derive::{Deserialize, Serialize}; 
#[derive(Serialize)]
struct SubscriptionMsg {
    time: u64,
    channel: &'static str,
    event: &'static str,
    payload: Vec<&'static str>,
}

// WebSocket服务器返回的数据结构
#[derive(Deserialize, Debug)]
struct WebSocketResponse {
    // 根据实际情况定义
    result: bool,
}
use serde::{Deserialize};

#[derive(Deserialize)]
struct User {
    name: String,
    age: u8,
    email: Option<String>,
}

fn main() {
    let data = r#"
    {
        "name": "Alice",
        "age": 30,
        "email": "alice@example.com"
    }"#;

    let user: User = serde_json::from_str(data).unwrap();
    println!("Name: {}", user.name);
    println!("Age: {}", user.age);
    println!("Email: {}", user.email.unwrap_or_default());
}

3.为什么使用connect_async建立websocket连接的时候是let (ws_stream, _) =connect_async(WS_URL).await?;而不是let ws_stream = connect_async(WS_URL).await?;

解答:
在这种情况下,使用 let (ws_stream, _) = connect_async(WS_URL).await?; 的形式是为了忽略 connect_async 返回的连接结果的一部分。具体来说,connect_async 函数返回一个 Result,其中包含了建立的 WebSocket 连接(WebSocketStream),以及一个用于处理连接结果的对象(通常是 ClientHandshake)。在 let (ws_stream, ) = connect_async(WS_URL).await?; 中, 是一个通配符,用于匹配 ClientHandshake 对象。由于我们通常不需要使用 ClientHandshake 对象,因此可以使用 _ 进行忽略。
这里有关的内容是模式匹配

4.let (mut write, mut read) = ws_stream.split();这个是在干什么呢?

这行代码在 Rust 中的 WebSocket 编程中常用于分离 WebSocket 连接的读写部分,以便于分别进行写入和读取操作。
具体来说,split() 方法是 WebSocketStream 类型的一个方法,用于将一个完整的 WebSocket 连接分为两个部分:一个负责写入数据的部分,一个负责读取数据的部分。
在这行代码中,let (mut write, mut read) = ws_stream.split(); 将 ws_stream 分为 write 和 read 两个部分,分别代表写入和读取数据的操作。mut 关键字表示这两个变量是可变的,因为在通常情况下,你会对这两个部分进行读写操作。
分离 WebSocket 连接的读写部分可以方便地进行并发操作,比如在一个线程中进行数据的读取,而在另一个线程中进行数据的写入,这样可以提高程序的并发性能。

5.Utc::now().timestamp() as u64什么意思?

这段代码是一个例子,它使用了特定编程语言或框架提供的函数或方法。看起来像是Rust语言中的一行代码。
Utc::now() 是Rust语言中 chrono 库的一部分,用于获取当前的UTC时间。
.timestamp() 是 chrono 库提供的 DateTime 结构体的方法之一,它将日期时间转换为UNIX时间戳,即从1970年1月1日UTC开始的秒数。
as u64 是类型转换,它将UNIX时间戳从默认的 i64 类型(有符号64位整数)转换为 u64 类型(无符号64位整数),因为UNIX时间戳可以是负数,但通常我们更关心非负数。
综合起来,这行代码的作用是获取当前UTC时间,并将其转换为一个64位无符号整数(u64),表示自1970年1月1日以来的秒数

6.什么是 tokio_tungstenite::tungstenite::protocol::Message;

tokio_tungstenite::tungstenite::protocol::Message 是 Rust 编程语言中的一个类型,它来自于 tokio_tungstenite crate,是一个基于 Tokio 的 WebSocket 库。这个类型代表了 WebSocket 连接中可以发送或接收的不同类型的消息。通常,它包括以下变体:
Text(String): 表示文本消息。
Binary(Vec): 表示二进制消息。
Ping(Vec): 表示 ping 控制帧。
Pong(Vec): 表示 pong 控制帧。
Close(Option): 表示关闭控制帧,可选地包含关闭原因。
这些变体封装了 WebSocket 连接中可以传输的不同类型的数据。

7.r# #中间包裹json数据是为了什么?

json数据中会有""这样的符号,会造成歧义,如果包裹在r##里面就直接当作字符串去进行处理这个json数据,否则的话则需要使用转义字符来防止歧义的发生。

let data = r#"
    {
        "name": "Alice",
        "age": 30,
        "email": "alice@example.com"
    }
"#;

let data = "{\"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\"}";

上下两段的效果是等价的

8.&'static str

不可变性:由于字符串字面量是不可变的,所以 &'static str 引用也是不可变的。你不能通过这个引用来修改字符串的内容。
生命周期:'static 生命周期是 Rust 中所有其他生命周期的超集。这意味着任何 'static 生命周期的引用都可以在任何其他生命周期上下文中安全使用,无需担心生命周期问题。
内存管理:因为字符串字面量存储在程序的二进制文件中,所以它们占用的空间是固定的,并且在程序的整个生命周期内都不会改变。这使得 &'static str 非常适合用于那些需要长期存在且不会改变的数据。
性能:由于不需要在运行时分配和释放内存,使用 &'static str 可以提高性能,尤其是在创建大量字符串引用时。

9.什么叫做工作窃取?

每一个工作单元都有一个任务队列,如果自己的任务做完了难道让cpu空闲吗?这显然是不合理的,因此去访问其他工作单元的任务队列进行并发执行,当然在这个过程中需要注意mutex对任务进行加锁防止产生冲突。

10.pin的某些用法

pin可以把数据固定在stack中,观察如下代码

use tokio::pin;

async fn my_async_fn() {
    // async logic here
}

#[tokio::main]
async fn main() {
    let future = my_async_fn();
    pin!(future);

    (&mut future).await;
}

这个future被固定在了stack中,按理说一个future只能够被.await调用一次,但是一旦固定了这个future,就可以被await多次调用。
future是惰性的,如果你不使用await进行调用的话,他的一系列操作是不会执行的

11. select!的简单理解

首先观察如下代码

use tokio;

// 假设这是我们想要并发执行的两个异步任务
async fn task1() -> Result<(), Box<dyn std::error::Error>> {
    tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
    Ok(())
}

async fn task2() -> Result<(), Box<dyn std::error::Error>> {
    tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 启动两个异步任务
    let handle1 = tokio::spawn(task1());
    let handle2 = tokio::spawn(task2());

    // 使用 select! 宏等待这两个任务中的任何一个完成
    select! {
        _ = handle1 => {
            println!("Task 1 completed");
        },
        _ = handle2 => {
            println!("Task 2 completed");
        },
    }

    // 这里我们不需要再次等待 handle1 或 handle2,因为 select! 宏已经处理了其中一个任务的完成
    // 如果我们想要等待另一个任务完成,我们可以再次使用 select! 宏或者 simply await the remaining handle

    // 例如,等待另一个任务完成
    if handle1.is_alive() {
        handle1.await.unwrap();
    } else {
        handle2.await.unwrap();
    }

    Ok(())
}

select会等待某个任务的先执行完成并进行相应处理。
1. 竞态条件处理
在某些情况下,你可能需要在多个异步操作之间进行选择,例如,当两个异步任务同时尝试访问共享资源时,你可能需要决定哪个任务应该先执行。select! 宏可以帮助你实现这种逻辑,确保资源在任何给定时间只被一个任务访问。
2. 并发流处理
当你使用 Stream 处理数据流时,你可能想要同时处理多个数据流。select! 宏允许你等待来自多个数据流的事件,并根据哪个流先产生数据来做出决策。
3. 服务多个客户端
在编写网络服务时,你可能需要同时处理多个客户端的连接。使用 select! 宏,你可以等待来自多个客户端的操作,并为每个操作提供服务,而不会阻塞其他客户端。
4. 超时处理
select! 宏可以用于实现超时逻辑。例如,你可以同时等待一个异步操作完成和一个超时发生,然后根据哪个先发生来执行相应的操作。
5. 并行任务执行
当你需要并行执行多个任务并根据它们的结果做出决策时,select! 宏可以非常有用。你可以启动多个异步任务,并使用 select! 宏等待它们中的任何一个完成。
==select!==是异步的,在等待的过程中并不会影响其他进程的进行。

12.与select几乎同父异母的join

当你有多个 Future 需要完成,并且需要它们的输出来继续执行后续操作时,join! 宏允许你等待所有的 Future 完成并收集它们的结果。
看如下代码

async fn future1() -> i32 {
    // 异步操作返回一个 i32
    42
}

async fn future2() -> String {
    // 异步操作返回一个 String
    "Hello".to_string()
}

#[tokio::main]
async fn main() {
    let result1 = future1();
    let result2 = future2();
    let (res1, res2) = tokio::join!(result1, result2);

    println!("Result 1: {}, Result 2: {}", res1, res2);
}

join和select的区别在于,select在遇到一个任务结束时就会结束,但是join会等待语句中的任务全部完成。
join同样是异步的,也就是等待任务执行的过程中并不会阻塞其他任务执行

13、关于lazy_static

lazy_static 是 Rust 语言中的一个 crate,它允许你在结构体中创建静态变量,而这些变量将会在第一次被使用时进行初始化这个特性在需要延迟加载或者减少启动时间的场景中非常有用,尤其是当你有一个重量级的初始化过程或者初始化过程依赖于运行时环境的时候。

use lazy_static::lazy_static;

lazy_static! {
    static ref MY_STATIC: Vec<i32> = {
        let mut v = Vec::new();
        for i in 0..1000 {
            v.push(i);
        }
        v
    };
}

fn main() {
    // 访问静态变量,这将触发初始化
    println!("First element is: {}", MY_STATIC[0]);
}

14.unwrap_or_else

unwrap_or_else 是 Rust 标准库中 Result 和 Option 类型提供的一个方法,它为错误处理提供了一种灵活的方式。当调用 unwrap_or_else 方法时,如果 Result 或 Option 是 Ok 或 Some,则返回其值;如果是 Err 或 None,则执行提供的函数,并返回该函数的返回值。
在这里插入图片描述

15 mpsc的rx为什么是mut?

在 Rust 中,mpsc(多生产者单消费者)通道的接收端 Receiver 被设计为可变的(mut),这是因为接收操作可能会消耗(consume)通道中的消息,从而改变通道的状态。在 Rust 的所有权和借用规则下,这样的操作需要可变性。
当一个消息被发送到 mpsc 通道时,它被放入一个内部缓冲区。接收端 Receiver 负责从这个缓冲区中取出消息。每次取出消息后,该消息就不再存在于通道中了,因此接收端必须具有可变性来改变这个状态。因此,Receiver 必须是可变的,以便能够从通道中移除消息并更新通道的内部状态。这是 Rust 语言设计的一部分,旨在通过所有权和借用规则来保证内存安全和线程安全。

16.如果接收端的rx被多线程共享,多线程访问队列时会同时获取同一份数据吗?

当一个 mpsc 通道的接收端 rx 被多个线程共享时,每个线程都会拥有 rx 的一个独立克隆(clone),这些克隆都是对同一个底层队列的引用。然而,尽管多个线程可以同时接收来自通道的消息,但每次从通道接收消息的操作是原子的,这意味着在任何给定时刻,只有一个线程可以从通道中接收消息。
这是因为 mpsc 接收端 Receiver 的实现通常会使用内部锁或其他同步机制来确保并发安全。当一个线程尝试从 Receiver 中接收消息时,它会尝试获取锁;如果锁已经被其他线程持有,则当前线程会被阻塞,直到锁可用。这样可以保证消息不会被多个线程同时读取,从而避免了数据竞争和不一致的状态。

17.println(“{}”,x)和println(“{}”,x)的区别?

在 Rust 中,println! 宏是用来打印输出的,它可以使用不同的格式化占位符来指定如何显示输出的值。当你使用 {:?} 作为格式化占位符时,它会调用值的 Debug 格式方法,而使用 {} 作为格式化占位符时,它会调用值的 Display 格式方法。
对于字符串切片(&str 类型),Debug 格式方法会将字符串包围在双引号中,并在前面加上一个反斜杠,这是因为 Debug 格式旨在提供一种稳定且易于识别的输出,用于调试和开发目的。所以当你使用 {:?} 打印字符串时,它会以 “BTC_USDT” 的形式显示,这里的反斜杠是转义字符,表示后面的双引号是字符串的一部分,而不是字符串的结束。
另一方面,Display 格式方法旨在提供一种适合终端用户查看的输出。它通常会省略调试信息中的额外信息,如双引号和转义字符。因此,当你使用 {} 打印字符串时,它会直接显示字符串的内容,即 “BTC_USDT”。

18.我的collect方法构建一个由(f64,f64)这个元组构成的vector数组,collect方法可以收集None吗

实际上,collect方法并不会收集None值。在Rust中,collect方法是用来将迭代器中的元素收集到某种集合类型的。当迭代器中的元素是Option类型时,collect方法会将Some(T)值收集到集合中,而None值会被忽略,不会包含在最终的集合中。

19.rust CircularQueue

在Rust语言中,CircularQueue是一种队列数据结构,它实现了循环缓冲的功能。队列是一种先进先出(FIFO)的数据结构,而循环队列则是在队列的基础上增加了循环利用空间的特性。当队列满了之后,新加入的元素会覆盖掉最老的元素,从而实现循环。

CircularQueue::with_capacity(30)是一个创建具有30个元素容量的循环队列的表达式。这里的with_capacity方法用于创建一个具有特定容量的循环队列,而不是默认的容量。

20. hashset

一种非线程安全的,用于快速查找的结构,hashmap存储的是键值对,但是这个hashset可以只保存key,用于判断存在性问题。

21.join_all

用于等待所有任务的完成。

22.tokio::spawn和 tokio::task::spawn有什么区别

tokio::task::spawn已经是旧时代的残党了。

23.STATE: AtomicI8 = AtomicI8::new(-1);

在 Rust 中,AtomicI8 是一个原子类型,它封装了一个 i8(8位有符号整数)并提供了一组原子操作。AtomicI8 保证了封装的值在多线程环境中被安全地读取和修改,即使多个线程尝试同时修改它,也不会导致数据竞争。

23.发送端和接收端分离后,rsv.next().await获得的数据。

rsv.next().await获得的数据是Option类型的,首先用Some进行提取,提取出的msg是Result类型的,再用Ok进行提取获得Message类型的,再进行模式匹配进行处理。
看一下message这个枚举

pub enum Message {
    Text(String),
    Binary(Vec<u8>),
    Ping(Vec<u8>),
    Pong(Vec<u8>),
    Close(Option<CloseFrame<'static>>),
    Frame(Frame),	
}

24.hashmap的is_empty

在 Rust 的标准库中,HashMap 结构体提供了一个 is_empty 方法,用于检查 HashMap 是否不包含任何键值对。
is_empty返回的是一个bool类型

25.group_by

group_by是将迭代的内容进行分组,看下面的代码

use itertools::Itertools; // 引入 Itertools trait

fn main() {
    // 创建一个示例数据集
    let data = vec![
        "apple", "banana", "cherry",
        "dates", "fig", "grape",
        "kiwi"
    ];

    // 使用 group_by 根据字符串长度进行分组
    let grouped_data: Vec<(usize, Vec<&str>)> = data
        .iter()  // 创建迭代器
        .group_by(|item| item.len())  // 根据字符串长度进行分组
        .into_iter()
        .map(|(key, group)| {
            (key, group.collect())  // 收集每个分组的数据
        })
        .collect();

    // 打印分组结果
    for (length, items) in &grouped_data {
        println!("Words with length {}: {:?}", length, items);
    }
}

首先需要知道group_by的结果是一个元组(key,group),而group又是一个迭代器
上述代码得到的结果是这样的:

Words with length 5: ["apple", "cherry"]
Words with length 6: ["banana", "grape"]
Words with length 5: ["dates"]
Words with length 3: ["fig"]
Words with length 4: ["kiwi"]

26.into_iter

在 Rust 中,into_iter 是一个迭代器适配器,它将某些集合类型的所有权转移给迭代器。它属于 IntoIterator trait,许多标准库集合类型(如 Vec、String、HashMap<K, V> 等)都实现了这个 trait。

当你调用 into_iter 方法时,你会得到一个迭代器,它遍历原始集合中的每个元素,并且每次迭代时都会消耗集合中的一个元素。这意味着原始的集合不再可用,因为它的所有元素都被转移到迭代器中了。

Logo

欢迎加入西安开发者社区!我们致力于为西安地区的开发者提供学习、合作和成长的机会。参与我们的活动,与专家分享最新技术趋势,解决挑战,探索创新。加入我们,共同打造技术社区!

更多推荐