• CS110L(Rust)


    1.Rust 语法总结

    数值类型
    • 有符号整数: i8, i16, i32, i64
    • 无符号整数: u8, u16, u32, u64
    变量声明
    • 声明变量:
      1. let i = 0; // 类型推断
      2. let n: i32 = 1; // 显式类型声明
    • 可变变量:
      1. let mut n =0;
      2. n = n + 1;
    字符串 

    注意,let s: str = "Hello world"; 是不正确的,因为 str 类型不能单独使用。它必须通过引用(&str)来使用。 

    集合
    • 动态数组(向量):
      1. let mut v: Vec<i32> = Vec::new();
      2. v.push(1);
      3. v.push(0);
    • 固定大小数组:
    • 在 Rust 中,所有变量在使用之前必须初始化。这是为了防止未初始化变量引起的未定义行为。因此,您不能声明一个未初始化的数组或变量。

      1. // 创建一个可变数组 `arr`,包含4个 `i32` 类型的元素,将所有元素初始化为0
      2. let mut arr: [i32; 4] = [0; 4];
      3. // 或者,可以逐个初始化每个元素
      4. let mut arr: [i32; 4] = [0, 0, 0, 0];
      5. // 修改数组的元素
      6. arr[0] = 0;
      7. arr[1] = 1;
      8. arr[2] = 2;
      9. arr[3] = 3;
    循环
    • 迭代器循环:
      1. // 使用 for 循环迭代向量中的元素
      2. for i in v.iter() {
      3. println!("{}", i); // 打印每个元素
      4. }

    • while 循环:
      1. while i < 9 {
      2. i += 1;
      3. println!("i = {}", i); // 打印每次递增后的值
      4. }
    • 无限循环:
      1. fn main() {
      2. let mut i = 0; // 初始化一个可变变量 i,初始值为 0
      3. loop {
      4. i += 1; // 每次循环迭代将 i 的值增加 1
      5. if i > 10 { // 检查 i 是否大于 10
      6. break; // 如果 i 大于 10,则退出循环
      7. }
      8. }
      9. println!("Final value of i: {}", i); // 打印 i 的最终值
      10. }

    函数
    1. fn sum(a: i32, b: i32) -> i32 {
    2. a + b
    3. }
    • 声明函数:
      1. fn sum(a: i32, b: i32) -> i32 {
      2. a + b
      3. }
    表达式
    • 三元表达式:
      let x = if someBool { 2 } else { 4 }
    输入输出

    (1)确保所有缓冲区中的数据都被写入到标准输出(通常是终端或控制台)中

    io::stdout().flush().unwrap();

    (2)read_line 方法从标准输入读取用户输入并将其存储到 guess 中。如果读取失败,程序会崩溃并显示错误信息 "读取输入失败."。

    1. let mut guess = String::new();
    2. io::stdin().read_line(&mut guess).expect("读取输入失败.");
    操作符
    (1)? 操作符

    当一个函数返回 ResultOption 类型时,可以使用 ? 操作符来自动处理这些结果。如果结果是 Ok,则返回其中的值;如果是 Err,则返回错误并退出当前函数。

    1. fn read_file_lines(filename: &str) -> Result<Vec<String>, io::Error> {
    2. // 尝试打开文件
    3. let file = File::open(filename)?;
    4. // 如果成功打开文件,继续执行;如果失败,返回错误并退出函数
    5. }
    定义结构体
    在 Rust 中,定义结构体类型时,我们声明了结构体的字段及其类型,而不是创建具体的实例。因此,不需要使用 letlet mut 这样的关键字。letlet mut关键字用于创建变量,而不是定义类型。

    在这段代码中,我们定义了一个名为 Node 的泛型结构体类型,它包含三个字段:

    • elem:类型为 T,表示节点存储的值。
    • next:类型为 Link,表示下一个节点的引用。
    • prev:类型为 Link,表示前一个节点的引用。

    这个定义仅仅是声明了 Node 结构体的形状,并没有创建任何实际的 Node 实例。

    impl

    Rust 中的 impl 块类似于其他编程语言中的 class 定义,但有一些关键的不同之处。

    类似点
    1. 方法定义

      • impl 块中定义的方法类似于在类中定义的方法。
      • 你可以定义实例方法和静态方法(Rust 中称为关联函数)。
    2. 封装

      • Rust 的 impl 块可以用于封装数据和行为,类似于类。
    不同点
    1. 数据和行为的分离

      • 在 Rust 中,数据(通过结构体或枚举)和行为(通过 impl 块)是分开的。
      • 在类中,数据和行为通常是在一个定义中。
    2. 没有继承

      • Rust 没有类的继承。相反,它使用特性(traits)来实现多态性。
      • 类系统通常有继承和多态性机制。
    3. 所有权和借用

      • Rust 强调所有权和借用,确保内存安全。
      • 类系统通常使用垃圾回收(如 Java)或手动内存管理(如 C++)。
    1. struct Rectangle {
    2. width: u32,
    3. height: u32,
    4. }
    5. impl Rectangle {
    6. fn new(width: u32, height: u32) -> Rectangle {
    7. Rectangle { width, height }
    8. }
    9. fn area(&self) -> u32 {
    10. self.width * self.height
    11. }
    12. }
    13. fn main() {
    14. let rect = Rectangle::new(30, 50);
    15. println!("The area of the rectangle is {} square pixels.", rect.area());
    16. }

    2.进阶用法

    数组和切片的方法

    1. iter(): 返回一个迭代器,该迭代器生成对数组或切片中每个元素的引用。

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

    2. iter_mut(): 返回一个可变迭代器,该迭代器生成对数组或切片中每个元素的可变引用。

      1. let mut a = [1, 2, 3];
      2. for elem in a.iter_mut() {
      3. *elem += 1;
      4. }

    3. enumerate(): 返回一个迭代器,该迭代器生成一个元组,包含每个元素的索引和值。

      1. let a = [1, 2, 3];
      2. for (i, elem) in a.iter().enumerate() {
      3. println!("Index: {}, Value: {}", i, elem);
      4. }

    4. map(): 返回一个迭代器,该迭代器应用一个函数于每个元素。

      1. let a = [1, 2, 3];
      2. let b: Vec<_> = a.iter().map(|x| x + 1).collect();

    5. filter(): 返回一个迭代器,该迭代器只包含满足特定条件的元素。

      1. let a = [1, 2, 3];
      2. let b: Vec<_> = a.iter().filter(|&&x| x > 1).collect();

    6. zip(): 返回一个迭代器,该迭代器生成两个迭代器中相应元素组成的元组。

      1. let a = [1, 2, 3];
      2. let b = [4, 5, 6];
      3. for (x, y) in a.iter().zip(b.iter()) {
      4. println!("a: {}, b: {}", x, y);
      5. }

    7. for_each(): 对每个元素应用一个函数(类似于 map,但不返回新的迭代器)。

      1. let a = [1, 2, 3];
      2. a.iter().for_each(|x| println!("{}", x));

    闭包

    闭包是一种匿名函数,它允许捕获调用者作用域中的值,可以赋值给变量,也可以作为参数传递给其他函数。闭包在许多现代编程语言中作为核心特性被广泛使用。

    示例代码:

    1. fn main() {
    2. let x = 1;
    3. let sum = |y| x + y;
    4. assert_eq!(3, sum(2));
    5. }

    闭包 sum 捕获了变量 x 并对其进行了操作。

    使用闭包简化代码

    传统函数实现:

    1. fn muuuuu(intensity: u32) -> u32 {
    2. println!("muuuu.....");
    3. thread::sleep(Duration::from_secs(2));
    4. intensity
    5. }
    6. fn workout(intensity: u32, random_number: u32) {
    7. // 根据 intensity 调整健身动作
    8. }

    函数变量实现:

    1. fn muuuuu(intensity: u32) -> u32 {
    2. println!("muuuu.....");
    3. thread::sleep(Duration::from_secs(2));
    4. intensity
    5. }
    6. fn workout(intensity: u32, random_number: u32) {
    7. let action = muuuuu;
    8. // 根据 intensity 调整健身动作
    9. }

    闭包实现:

    1. fn workout(intensity: u32, random_number: u32) {
    2. let action = || {
    3. println!("muuuu.....");
    4. thread::sleep(Duration::from_secs(2));
    5. intensity
    6. };
    7. // 根据 intensity 调整健身动作
    8. }

    通过闭包可以简化代码,并捕获外部变量,使得代码更具灵活性。

    闭包的语法和类型推导

    闭包的定义形式如下:

    1. |param1, param2| {
    2. 语句1;
    3. 语句2;
    4. 返回表达式
    5. }

    类型推导示例:

    1. let sum = |x: i32, y: i32| -> i32 {
    2. x + y
    3. };

    不标注类型的闭包声明更简洁:

    let sum = |x, y| x + y;
    

    结构体中的闭包

    在 Rust 中,实现一个简易缓存的设计可以通过使用结构体和闭包来实现。(闭包应该作为一个变量传递,并且使用泛型和特征约束来指定它的类型。

    1. struct Cacher<T>
    2. where
    3. T: Fn(u32) -> u32,
    4. {
    5. query: T,
    6. value: Option<u32>,
    7. }
    核心概念
    1. 闭包与特征约束:

      • T: Fn(u32) -> u32 表示 T 是一个实现了 Fn(u32) -> u32 特征的类型,这意味着 query 是一个闭包或函数,接受一个 u32 类型的参数并返回一个 u32 类型的值。
      • 每个闭包都有其唯一的类型,即使它们的签名相同。
    2. 结构体设计:

      • query 字段是一个闭包,用于获取值。
      • value 字段用于存储缓存的值,初始为 None
    实现方法

    Cacher 结构体实现方法:

    1. impl<T> Cacher<T>
    2. where
    3. T: Fn(u32) -> u32,
    4. {
    5. fn new(query: T) -> Cacher<T> {
    6. Cacher {
    7. query,
    8. value: None,
    9. }
    10. }
    11. fn value(&mut self, arg: u32) -> u32 {
    12. match self.value {
    13. Some(v) => v,
    14. None => {
    15. let v = (self.query)(arg);
    16. self.value = Some(v);
    17. v
    18. }
    19. }
    20. }
    21. }
    主要步骤
    1. 创建缓存实例:

      • 使用 Cacher::new 创建新的缓存实例,传入一个闭包或函数作为 query
    2. 查询缓存值:

      • value 方法首先检查 self.value 是否已有缓存值。
      • 如果没有,调用 query 获取新值,并将其存储在 self.value 中。
    泛型扩展

    为了支持其他类型(如 &str),可以将 u32 替换为泛型类型 E

    1. struct Cacher<T, E>
    2. where
    3. T: Fn(E) -> E,
    4. {
    5. query: T,
    6. value: Option<E>,
    7. }
    8. impl<T, E> Cacher<T, E>
    9. where
    10. T: Fn(E) -> E,
    11. E: Copy,
    12. {
    13. fn new(query: T) -> Cacher<T, E> {
    14. Cacher {
    15. query,
    16. value: None,
    17. }
    18. }
    19. fn value(&mut self, arg: E) -> E {
    20. match self.value {
    21. Some(v) => v,
    22. None => {
    23. let v = (self.query)(arg);
    24. self.value = Some(v);
    25. v
    26. }
    27. }
    28. }
    29. }

    闭包的特征(重要)

    1. FnOnce:转移所有权
    • 语法move || { ... }
    • 解释move 关键字用于指示闭包获取其捕获变量的所有权。
    • 示例
      1. fn main() {
      2. let x = String::from("Hello");
      3. let consume_x = move || {
      4. println!("{}", x);
      5. // x 被转移到闭包中,闭包执行后 x 的所有权已被消耗
      6. };
      7. consume_x(); // 第一次调用成功
      8. // consume_x(); // 再次调用会报错,因为 x 的所有权已被消耗
      9. }
    2. FnMut:可变借用
    • 语法|mut param| { ... }
    • 解释:通过 mut 关键字在参数中表示可变借用,允许闭包内部修改捕获的变量。
    • 示例
      1. fn main() {
      2. let mut x = 0;
      3. let mut add_to_x = |y| {
      4. x += y;
      5. };
      6. add_to_x(5); // x 现在是 5
      7. add_to_x(3); // x 现在是 8
      8. }
    3. Fn:不可变借用
    • 语法|| { ... }
    • 解释:不需要特殊标记,默认情况下,闭包捕获变量是不可变借用。
    • 示例
      1. fn main() {
      2. let x = 10;
      3. let print_x = || {
      4. println!("{}", x);
      5. };
      6. print_x(); // 打印 10
      7. print_x(); // 可以多次调用,因为 x 只是被不可变地借用
      8. }

    闭包的生命周期

    1. 捕获变量的生命周期

      • 闭包捕获的变量在闭包的生命周期内必须是有效的。
      • 如果闭包捕获的是借用(&),则闭包的生命周期不能超过被借用变量的生命周期。
    2. 闭包的生命周期

      • 闭包的生命周期不能超过其捕获变量的生命周期。
      • 使用 move 关键字可以将变量的所有权移动到闭包中,使得变量的生命周期延长到与闭包相同。
    3. 生命周期标注

      • 在复杂情况下,可以显式地标注闭包和捕获变量的生命周期,以确保它们之间的关系是有效的。

    示例总结

    以下是一个示例,展示了闭包捕获变量并在闭包生命周期内使用:

    1. fn main() {
    2. let closure = create_closure();
    3. println!("Closure result: {}", closure(5));
    4. }
    5. fn create_closure() -> impl Fn(i32) -> bool {
    6. let x = 10;
    7. move |z| z < x
    8. }
    • x 被闭包捕获并使用,通过 move 关键字将其所有权移动到闭包中。
    • 闭包的生命周期因此延长,可以在 create_closure 函数结束后继续有效。

    (2)all 方法

    1. fn all(&mut self, f: F) -> bool
    2. where
    3. F: FnMut(Self::Item) -> bool,

    它接受一个闭包 f 作为参数,并对迭代器中的每个元素应用这个闭包。all 方法会返回一个布尔值:

    • 如果所有元素都满足闭包 f 的条件,则返回 true
    • 如果任何一个元素不满足闭包 f 的条件,则返回 false

    (3)迭代器

    let chars_left = vec![false, true, false, true];
    
    原理:

    创建迭代器:

    let iter = chars_left.iter();
    

    迭代示例:

    1. let first = iter.next();  // Some(&false)
    2. let second = iter.next(); // Some(&true)
    3. let third = iter.next();  // Some(&false)
    4. let fourth = iter.next(); // Some(&true)
    5. let none = iter.next();   // None

    用法:
    • 创建迭代器:通过调用集合的 iteriter_mutinto_iter 方法创建迭代器。
    • 遍历:使用 for 循环或 while let 语句。
    • 常用方法
      • 转换mapfilterenumeratezip
      • 收集collectfold
      • 检查allany
    • 链式调用:将多个迭代器方法链式调用以实现复杂的数据处理。
    • (1)转换
      • map():对每个元素应用一个函数,返回一个新的迭代器。
      • filter():过滤符合条件的元素,返回一个新的迭代器。
      • enumerate():为迭代器中的每个元素生成一个索引,返回 (索引, 元素) 对。
      • zip():将两个迭代器合并为一个新的迭代器,生成 (元素1, 元素2) 对。
    • (2)收集
      • collect():将迭代器的所有元素收集到一个集合类型中,通常是一个向量(Vec)。
      • fold():将迭代器的所有元素通过一个累积函数聚合为一个值。
    • (3)检查
      • all():检查是否所有元素都满足一个条件。
      • any():检查是否有任意元素满足一个条件。
    • (4)链式调用

            将多个迭代器方法链式调用,以实现复杂的数据处理。例如:过滤、映射和收集的组合。

     例子:
    1. fn main() {
    2. let vec = vec![1, 2, 3, 4, 5];
    3. // 使用迭代器遍历元素
    4. for val in vec.iter() {
    5. println!("{}", val);
    6. }
    7. // 使用链式调用过滤和映射元素,然后收集结果
    8. let processed: Vec<i32> = vec.iter()
    9. .filter(|&&x| x % 2 == 0) // 过滤出偶数
    10. .map(|&x| x * 2) // 将每个偶数乘以 2
    11. .collect(); // 收集结果到一个向量
    12. println!("{:?}", processed); // 输出: [4, 8]
    13. }

     (4)枚举

    同一化类型
    实际项目简化片段

    在实际项目中,通常需要处理多种类型的长连接。例如,我们有一个 WEB 服务,需要接受用户的长连接,这些连接可能是 TcpStreamTlsStream。为了用同一个函数处理这两种连接,我们可以使用枚举来简化代码。

    假设我们有以下代码:

    1. fn new(stream: TcpStream) {
    2. let mut s = stream;
    3. if tls {
    4. s = negotiate_tls(stream);
    5. }
    6. // websocket 是一个 WebSocket<TcpStream> 或者 WebSocket<native_tls::TlsStream<TcpStream>> 类型
    7. websocket = WebSocket::from_raw_socket(s, ...);
    8. }
    使用枚举类型来简化处理

    通过使用枚举类型,我们可以将 TcpStreamTlsStream 统一化处理:

    1. enum WebSocketStream {
    2. Tcp(TcpStream),
    3. Tls(native_tls::TlsStream<TcpStream>),
    4. }
    5. fn new(stream: WebSocketStream) {
    6. match stream {
    7. WebSocketStream::Tcp(tcp_stream) => {
    8. // 处理 TcpStream
    9. let websocket = WebSocket::from_raw_socket(tcp_stream, ...);
    10. }
    11. WebSocketStream::Tls(tls_stream) => {
    12. // 处理 TlsStream
    13. let websocket = WebSocket::from_raw_socket(tls_stream, ...);
    14. }
    15. }
    16. }

    通过这种方式,我们可以将 TcpStreamTlsStream 封装在一个枚举类型 WebSocketStream 中,并在同一个函数 new 中处理它们,简化了代码逻辑。

    枚举类型
    (1)Option类型

    在 Rust 中,Option 类型是一种枚举,用于表示一个值可能存在(Some)或者不存在(None

    1. fn main() {
    2. let numbers = vec![1, 2, 3];
    3. let empty: Vec<i32> = Vec::new();
    4. match get_first_element(&numbers) {
    5. Some(value) => println!("第一个元素是: {}", value),
    6. None => println!("数组为空"),
    7. }
    8. match get_first_element(&empty) {
    9. Some(value) => println!("第一个元素是: {}", value),
    10. None => println!("数组为空"),
    11. }
    12. }
    (2)Result类型

    Result:

    • 用于表示一个操作的成功或失败。
    • 在 Rust 中,Result 枚举类型需要两个类型参数:

    • Result:表示操作的结果。
      • Ok(T):表示操作成功,包含类型 T 的值。
      • Err(E):表示操作失败,包含类型 E 的错误信息。
      1. fn read_file_lines(filename: &str) -> Result<Vec<String>, io::Error> {
      2. let file = File::open(filename)?;
      3. let reader = BufReader::new(file);
      4. let mut lines = Vec::new();
      5. for line in reader.lines() {
      6. let line = line?;
      7. lines.push(line);
      8. }
      9. Ok(lines)
      10. }

     使用 Ok 包装一个值时,你实际上是在创建一个 Result 类型的实例,表示操作成功,并返回该值作为 Result 的成功变体。

    模式匹配

    (5)match 表达式

    match 表达式是 Rust 中用于模式匹配的强大工具。它可以根据不同的模式执行不同的代码分支。

    1. match value {
    2. pattern1 => expr1,
    3. pattern2 => expr2,
    4. _ => expr3, // 通配模式,匹配所有其他情况
    5. }

    (6)读取文件 

    在 Rust 中读取文件的流程通常包括以下步骤:

    1. 导入必要的模块:包括文件系统和 I/O 操作的模块。
    2. 打开文件:使用 std::fs::File::open 方法打开文件,并处理可能的错误。
    3. 创建缓冲读取器(可选):如果逐行读取文件内容,可以使用 std::io::BufReader 创建一个缓冲读取器。
    4. 读取文件内容:根据需要选择读取文件内容的方法,例如逐行读取、一次性读取到字符串、一次性读取到字节数组等。
    5. 处理文件内容:对读取到的文件内容进行处理。
    6. 错误处理:在读取和处理文件内容的过程中,处理可能的错误。
    方法一:逐行读取文件内容

    关键点:使用 BufReaderlines 方法逐行读取文件

    1. use std::fs::File;
    2. use std::io::{self, BufRead, BufReader};
    3. fn read_file_lines(filename: &str) -> Result<Vec<String>, io::Error> {
    4. let file = File::open(filename)?; // 打开文件
    5. let reader = BufReader::new(file); // 创建缓冲读取器
    6. reader.lines().collect() // 逐行读取并收集结果
    7. }
    8. fn main() {
    9. match read_file_lines("example.txt") {
    10. Ok(lines) => lines.iter().for_each(|line| println!("{}", line)),
    11. Err(e) => eprintln!("Error reading file: {}", e),
    12. }
    13. }
    方法二:一次性读取整个文件内容到字符串

    关键点:使用 read_to_string 方法一次性读取整个文件内容

    1. use std::fs::File;
    2. use std::io::{self, Read};
    3. fn read_file_to_string(filename: &str) -> Result<String, io::Error> {
    4. let mut file = File::open(filename)?; // 打开文件
    5. let mut contents = String::new();
    6. file.read_to_string(&mut contents)?; // 读取文件内容到字符串
    7. Ok(contents)
    8. }
    9. fn main() {
    10. match read_file_to_string("example.txt") {
    11. Ok(contents) => println!("{}", contents),
    12. Err(e) => eprintln!("Error reading file: {}", e),
    13. }
    14. }
    方法三:一次性读取整个文件内容到字节数组

    关键点:使用 read_to_end 方法一次性读取整个文件内容到字节数组

    1. use std::fs::File;
    2. use std::io::{self, Read};
    3. fn read_file_to_bytes(filename: &str) -> Result<Vec<u8>, io::Error> {
    4. let mut file = File::open(filename)?; // 打开文件
    5. let mut contents = Vec::new();
    6. file.read_to_end(&mut contents)?; // 读取文件内容到字节数组
    7. Ok(contents)
    8. }
    9. fn main() {
    10. match read_file_to_bytes("example.txt") {
    11. Ok(contents) => println!("{:?}", contents),
    12. Err(e) => eprintln!("Error reading file: {}", e),
    13. }
    14. }
    方法四:使用 std::fs::read_to_string 直接读取整个文件到字符串

    关键点:使用 fs::read_to_string 直接读取文件内容到字符串

    1. use std::fs;
    2. fn read_file_to_string(filename: &str) -> Result<String, std::io::Error> {
    3. fs::read_to_string(filename) // 直接读取文件内容到字符串
    4. }
    5. fn main() {
    6. match read_file_to_string("example.txt") {
    7. Ok(contents) => println!("{}", contents),
    8. Err(e) => eprintln!("Error reading file: {}", e),
    9. }
    10. }
    方法五:使用 std::fs::read 直接读取整个文件到字节数组

    关键点:使用 fs::read 直接读取文件内容到字节数组

    1. use std::fs;
    2. fn read_file_to_bytes(filename: &str) -> Result<Vec<u8>, std::io::Error> {
    3. fs::read(filename) // 直接读取文件内容到字节数组
    4. }
    5. fn main() {
    6. match read_file_to_bytes("example.txt") {
    7. Ok(contents) => println!("{:?}", contents),
    8. Err(e) => eprintln!("Error reading file: {}", e),
    9. }
    10. }
    总结
    1. 逐行读取文件内容:使用 BufReaderlines 方法。
    2. 一次性读取整个文件内容到字符串:使用 read_to_string 方法。
    3. 一次性读取整个文件内容到字节数组:使用 read_to_end 方法。
    4. 直接读取整个文件到字符串:使用 fs::read_to_string 方法。
    5. 直接读取整个文件到字节数组:使用 fs::read 方法。

    RefCell 概括

    RefCell 是 Rust 提供的一种类型,用于在不可变的上下文中实现内部可变性。它允许你在运行时执行借用检查,以确保安全地修改数据。这在某些数据结构(如链表)和特定场景(如闭包或异步编程)中非常有用。

    核心特点
    1. 内部可变性

      • 允许在不可变的上下文中修改数据。
      • 使用 borrow() 获取不可变引用。
      • 使用 borrow_mut() 获取可变引用。
    2. 运行时借用检查

      • 在借用时进行运行时检查,确保借用规则不被违反。
      • 如果在借用过程中违反规则,会导致运行时错误。
    3. 典型用法

      • 适用于实现复杂数据结构,如链表、图等需要相互引用的结构。
      • 适用于跨越函数的借用,尤其在闭包和异步编程中。
    示例代码
    1. use std::cell::RefCell;
    2. let x = RefCell::new(5);
    3. {
    4. let y = x.borrow();
    5. println!("y: {}", *y); // 输出: y: 5
    6. }
    7. {
    8. let mut z = x.borrow_mut();
    9. *z = 10;
    10. println!("x: {}", x.borrow()); // 输出: x: 10
    11. }

    Rc 的核心作用概括

    Rc(Reference Counted)是 Rust 提供的一种智能指针,允许多个所有者共享同一个数据

    核心特点
    1. 共享所有权

      • 允许多个变量同时拥有同一个数据。
      • 适用于需要在多个地方访问和使用同一个数据的场景。
    2. 自动管理内存

      • 通过引用计数管理数据的生命周期。
      • 当最后一个引用被删除时,数据会自动释放。
    3. 单线程环境

      • 只能在单线程环境中使用。
      • 如果需要在多线程环境中共享数据,使用 Arc(Atomic Reference Counted)。
    使用场景
    1. 数据共享

      • 例如,在树或图数据结构中,多个节点可以共享同一个子节点。
    2. 不可变数据

      • 通常用于共享不可变数据,因为 Rc 默认不允许多个可变引用。
      • 如果需要修改数据,可以结合 RefCell 使用。
    示例代码
    1. use std::rc::Rc;
    2. fn main() {
    3. let data = Rc::new(5); // 创建一个 Rc 指针,包含数据 5
    4. let data1 = Rc::clone(&data); // 创建 data 的克隆引用
    5. let data2 = Rc::clone(&data); // 创建 data 的另一个克隆引用
    6. println!("Reference count: {}", Rc::strong_count(&data)); // 输出: 3
    7. println!("data: {}", data);
    8. println!("data1: {}", data1);
    9. println!("data2: {}", data2);
    10. }
    Rc 的销毁时机

    对于 Rc(Reference Counted)智能指针,当一个 Rc 实例超出其作用域时,引用计数会自动减少。如果引用计数减少到零,Rc 管理的数据将被释放。

    1. use std::cell::RefCell;
    2. use std::rc::Rc;
    3. fn main() {
    4. {
    5. let data = Rc::new(RefCell::new(5)); // 创建一个包含 RefCell 的 Rc 指针
    6. println!("Initial reference count: {}", Rc::strong_count(&data)); // 输出: 1
    7. {
    8. let data1 = Rc::clone(&data); // 克隆 Rc 指针,引用计数增加到 2
    9. println!("Reference count after creating data1: {}", Rc::strong_count(&data)); // 输出: 2
    10. {
    11. let data2 = Rc::clone(&data); // 再次克隆 Rc 指针,引用计数增加到 3
    12. println!("Reference count after creating data2: {}", Rc::strong_count(&data)); // 输出: 3
    13. *data2.borrow_mut() = 10; // 修改数据
    14. println!("Modified data through data2: {}", data.borrow()); // 输出: 10
    15. } // data2 超出作用域,引用计数减少到 2
    16. println!("Reference count after data2 goes out of scope: {}", Rc::strong_count(&data)); // 输出: 2
    17. } // data1 超出作用域,引用计数减少到 1
    18. println!("Reference count after data1 goes out of scope: {}", Rc::strong_count(&data)); // 输出: 1
    19. } // data 超出作用域,引用计数减少到 0,数据被释放
    20. // 由于 data 已经被释放,不能再访问它
    21. }

    字符串操作

    字符串替换
    1. replace
    • 适用类型: String&str
    • 参数:
      • 第一个参数是要被替换的字符串。
      • 第二个参数是新的字符串。
    • 功能: 替换所有匹配的字符串,返回一个新的字符串。

    示例代码:

    1. fn main() {
    2. let string_replace = String::from("I like rust. Learning rust is my favorite!");
    3. let new_string_replace = string_replace.replace("rust", "RUST");
    4. dbg!(new_string_replace);
    5. }

    运行结果:

    new_string_replace = "I like RUST. Learning RUST is my favorite!"
    
    2. replacen
    • 适用类型: String&str
    • 参数:
      • 前两个参数与 replace 方法相同。
      • 第三个参数表示替换的次数。
    • 功能: 替换指定次数的匹配字符串,返回一个新的字符串。

    示例代码:

    1. fn main() {
    2. let string_replace = "I like rust. Learning rust is my favorite!";
    3. let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
    4. dbg!(new_string_replacen);
    5. }

    运行结果:

    new_string_replacen = "I like RUST. Learning rust is my favorite!"
    
    3. replace_range
    • 适用类型: 仅适用于 String
    • 参数:
      • 第一个参数是要替换的字符串范围(Range)。
      • 第二个参数是新的字符串。
    • 功能: 直接在原字符串上替换指定范围内的内容,不返回新的字符串。

    示例代码:

    1. fn main() {
    2. let mut string_replace_range = String::from("I like rust!");
    3. string_replace_range.replace_range(7..8, "R");
    4. dbg!(string_replace_range);
    5. }

    运行结果:

    string_replace_range = "I like Rust!"
    
    字符串删除方法
    1. truncate
    • 功能: 从指定位置开始删除字符串中从该位置到结尾的全部字符。
    • 特性: 直接操作原字符串,无返回值。如果指定位置不在字符边界上,则会发生错误。

    示例代码:

    1. fn main() {
    2. let mut string_truncate = String::from("测试truncate");
    3. string_truncate.truncate(3);
    4. dbg!(string_truncate);
    5. }

    运行结果:

    string_truncate = "测"
    
    2. clear
    • 功能: 清空字符串,删除字符串中的所有字符。
    • 特性: 直接操作原字符串,相当于 truncate() 方法参数为 0。

    示例代码:

    1. fn main() {
    2. let mut string_clear = String::from("string clear");
    3. string_clear.clear();
    4. dbg!(string_clear);
    5. }

    运行结果:

    string_clear = ""
    
    字符串连接 
    1. 使用 ++= 操作符
    • 要求:

      • 右边的参数必须为字符串切片引用(&str)。
      • 调用 + 操作符相当于调用了标准库中的 add 方法。
    • 特性:

      • 返回一个新的字符串。
      • 变量声明可以不需要 mut 关键字修饰。
      • 左边的字符串所有权会被转移。

    示例代码:

    1. fn main() {
    2. let string_append = String::from("hello ");
    3. let string_rust = String::from("rust");
    4. let result = string_append + &string_rust; // string_append 的所有权被转移
    5. let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
    6. result += "!!!";
    7. println!("连接字符串 + -> {}", result);
    8. }

    运行结果:

    连接字符串 + -> hello rust!!!!
    
    • 所有权转移示例:

      1. fn main() {
      2. let s1 = String::from("hello,");
      3. let s2 = String::from("world!");
      4. let s3 = s1 + &s2; // s1 的所有权被转移
      5. assert_eq!(s3, "hello,world!");
      6. // println!("{}", s1); // 这行代码会报错,因为 s1 的所有权已被转移
      7. }
    • 连续连接示例:

      1. let s1 = String::from("tic");
      2. let s2 = String::from("tac");
      3. let s3 = String::from("toe");
      4. // String = String + &str + &str + &str + &str
      5. let s = s1 + "-" + &s2 + "-" + &s3;

    s1 这个变量通过调用 add() 方法后,所有权被转移到 add() 方法里面, add() 方法调用后就被释放了,同时 s1 也被释放了。再使用 s1 就会发生错误。 

    2. 使用 format!
    • 适用: String&str
    • 特性: 类似于 print! 的用法,生成一个新的字符串。

    示例代码:

    1. fn main() {
    2. let s1 = "hello";
    3. let s2 = String::from("rust");
    4. let s = format!("{} {}!", s1, s2);
    5. println!("{}", s);
    6. }

    运行结果:

    hello rust!
    

    3.Rust特性

    (1)所有权

    所有权机制是Rust用来管理内存的一种系统,它确保了内存安全性并防止了许多常见的编程错误。以下是所有权机制的核心概念和规则:

    1. 所有权规则
    • 每个值在Rust中都有一个所有者

      • 所有者是一个变量,只有一个变量可以是某个值的所有者。
    • 值在任一时刻只能有一个所有者

      • 当所有者变量超出作用域时,该值将被自动清理。
    • 当所有者离开作用域时,该值将被丢弃

      • Rust在所有者超出作用域时自动调用drop函数来释放内存。
    2. 所有权转移(Move)
    • (1)非 Copy trait 的类型赋值
      1. let s = String::from("hello");
      2. let s1 = s;
      • 在这个例子中,s的栈上的数据所有权被转移给 s1(堆上数据仍然不变,移动语义),因此在之后使用s1会导致编译错误。
    • (2)self 作为参数

    方法或函数以 self 作为参数时,会获取调用者的所有权,调用后原变量失效。

    • (3) 非引用参数

    类似地,函数以非引用类型参数接收变量时,也会获取其所有权。

    • (4)克隆
    1. let s = String::from("hello");
    2. let s1 = s.clone();

    在Rust中,使用clone方法可以进行深拷贝。深拷贝会复制堆上的数据,并在栈上创建一个新的所有权指向这块堆内存。结果是栈上和堆上都有独立的拷贝,因此两个变量互不影响。 

    • (5) Copy trait 的类型
    • 整数类型 (i32, u32, 等)
    • 浮点数类型 (f32, f64)
    • 布尔类型 (bool)
    • 字符类型 (char)
    • 元组(如果元组内的所有元素都实现了 Copy trait)
    • (6)无法copy的类型
    • 非Copy类型

      • 复杂类型如String不实现Copy特性,因为它们涉及更复杂的内存管理。
      • 不能对不实现Copy的类型进行直接赋值拷贝。
    • 解决方案

      • 使用引用:对于无法实现Copy的类型,可以通过引用来解决所有权冲突。
        1. fn main() {
        2. let x = (1, 2, (), "hello".to_string());
        3. let y = (&x.0, &x.1, &x.2, &x.3);
        4. println!("{:?}, {:?}", x, y);
        5. }

    3. 借用(Borrowing)

    引用的可变性决定了你是否可以通过引用来修改所引用的值。

    • (1)不可变借用
      • 可以有多个不可变引用,但不能同时有可变引用。
      1. let s = String::from("hello");
      2. let r1 = &s; // 不可变引用 r1
      3. let r2 = &s; // 不可变引用 r2
      4. println!("r1: {}, r2: {}", r1, r2); // 可以同时使用多个不可变引用
    • (2)可变借用
      • 同一时间只能有一个可变引用,且不能同时存在不可变引用。
      1. let mut s = String::from("hello");
      2. let r1 = &mut s; // 可变引用 r1
      3. r1.push_str(", world");
      4. println!("{}", r1); // r1 修改了 s 的内容
    1. 唯一的可变引用:在任何给定的时间点,一个变量只能有一个可变引用(&mut)。
    2. 不可变引用与可变引用互斥:在有可变引用存在时,不允许同时存在不可变引用(&)。反之,在存在不可变引用时,不允许存在可变引用。

    这些规则确保了在访问和修改数据时不会出现竞争条件。

    (3)注意:

    1.不允许在存在不可变引用时修改原始变量 
    1. fn main() {
    2. let mut s = String::from("hello"); // 可变变量 `s` 被创建
    3. let ref1 = &s; // 创建对 `s` 的不可变引用 `ref1`
    4. s = String::from("goodbye"); // 尝试修改 `s` 的值
    5. println!("{}", ref3.to_uppercase()); // 使用 `ref3` 打印 `s` 的值
    6. }

    一种修复方法是将 println! 语句移动到修改 s 之前,确保在修改 s 之前,所有的不可变引用都已经被使用完毕。例如:

    1. fn main() {
    2. let mut s = String::from("hello");
    3. let ref1 = &s;
    4. let ref2 = &ref1;
    5. let ref3 = &ref2;
    6. println!("{}", ref3.to_uppercase()); // 在修改 `s` 之前使用 `ref3`
    7. s = String::from("goodbye"); // 现在可以安全地修改 `s`
    8. }
    2. 悬垂引用

    返回的是一个局部变量的引用,函数作用域结束后,变量销毁

    1. fn drip_drop() -> &String {
    2. let s = String::from("hello world!");
    3. return &s;
    4. }

    修改方法:直接返回所有权

    1. fn drip_drop() -> String {
    2. let s = String::from("hello world!");
    3. return s;
    4. }
     3.借用检查器错误

    v[0]返回一个引用,试图将向量中元素的引用赋值给一个所有权变量 

    1. fn main() {
    2. let s1 = String::from("hello");
    3. let mut v = Vec::new();
    4. v.push(s1);
    5. let s2: String = v[0]; // 试图移动元素的所有权
    6. println!("{}", s2);
    7. }

     解决方法:(仅读取)

    1. fn main() {
    2. let s1 = String::from("hello");
    3. let mut v = Vec::new();
    4. v.push(s1);
    5. let s2: &String = &v[0];
    6. println!("{}", s2);
    7. }
    4.切片

    切片(slice)是Rust中对数组、字符串等集合部分数据的引用。它具有以下核心特性:

    1. 引用类型:不拥有数据所有权,只是借用数据的一部分。
    2. 不可变和可变:支持不可变切片(&[T])和可变切片(&mut [T])。
    3. 高效:避免数据拷贝,直接引用原数据。
    4. 安全:编译时和运行时边界检查,防止越界访问和数据竞争。
    (1)不可变切片
    1. fn main() {
    2. let arr = [1, 2, 3, 4, 5];
    3. let slice = &arr[1..4]; // 引用数组的部分数据
    4. println!("{:?}", slice); // 输出 [2, 3, 4]
    5. }
    (2)可变切片
    1. fn main() {
    2. let mut arr = [1, 2, 3, 4, 5];
    3. let slice = &mut arr[1..3]; // 可变切片,引用数组的部分数据
    4. slice[0] = 10;
    5. println!("{:?}", arr); // 输出 [1, 10, 3, 4, 5]
    6. }
    (3)字符串切片
    1. fn main() {
    2. let s = String::from("hello, world");
    3. let hello = &s[0..5]; // 引用字符串的部分数据
    4. println!("{}", hello); // 输出 "hello"
    5. }
    (4)切片操作
    1. fn main() {
    2. let arr = [1, 2, 3, 4, 5];
    3. let slice = &arr[1..4];
    4. println!("Length: {}", slice.len()); // 输出 "Length: 3"
    5. println!("First element: {:?}", slice.first()); // 输出 "First element: Some(2)"
    6. }

    5. 数据竞争的避免
    • Rust的借用检查器在编译时强制执行借用规则,以确保在任何给定时间只有一个可变引用或多个不可变引用,从而避免数据竞争。
    5. 生命周期
    • Rust通过生命周期标注来确保引用的有效性,防止悬空引用。
       

    (2)显式使用引用操作符

    常见情况

    1. 创建引用

      • 当你需要创建一个变量的引用时,需要显式地使用 &
      1. let x = 5;
      2. let y = &x; // 创建对 x 的不可变引用
      3. let z = &mut x; // 创建对 x 的可变引用(需要 x 是可变的)
    2. 函数参数传递引用

      • 当你定义一个函数,并希望它接收一个引用作为参数时,需要显式地使用 &
      1. fn print_value(value: &i32) {
      2. println!("{}", value);
      3. }
      4. let x = 10;
      5. print_value(&x); // 传递 x 的引用
    3. 解引用

      • 当你需要从一个引用中获取实际值时,需要显式地使用 *
      1. let x = 5;
      2. let y = &x;
      3. println!("{}", *y); // 解引用 y 获取 x 的值

    注意和习惯

    基本类型
    为什么要手动设置变量可变性
    • Rust支持可变和不可变变量,提供了灵活性和安全性,性能优化
    • 将无需改变的变量声明为不可变,可以提升运行性能,避免多余的运行时检查。
    变量命名
    • 使用下划线开头的变量名,可以忽略未使用变量的警告。
    • let表达式可以用于变量解构,从复杂变量中匹配出部分内容。
    整形溢出处理
    • Rust提供了多种方法显式处理整型溢出,如wrapping_*checked_*overflowing_*saturating_*
    • wrapping_* 方法

      • 描述:当发生溢出时,值会按照二进制补码环绕(wrap around)。这意味着溢出后的结果将从最低有效位开始重新计算。
      • 用法wrapping_addwrapping_subwrapping_mul等。
      • 示例
        1. let x: u8 = 255;
        2. let y = x.wrapping_add(1); // y == 0
      • 核心:溢出后环绕,继续计算,不会引发程序错误。
    • checked_* 方法

      • 描述:当发生溢出时,返回一个None,否则返回Some(结果)。适合需要检测并处理溢出的情况。
      • 用法checked_addchecked_subchecked_mul等。
      • 示例
        1. let x: u8 = 255;
        2. if let Some(y) = x.checked_add(1) {
        3. // 不会执行
        4. } else {
        5. println!("溢出检测到");
        6. }
      • 核心:通过返回Option类型来检测和处理溢出。
    • overflowing_* 方法

      • 描述:返回一个包含计算结果和布尔值的元组,布尔值指示是否发生溢出。
      • 用法overflowing_addoverflowing_suboverflowing_mul等。
      • 示例
        1. let x: u8 = 255;
        2. let (y, overflowed) = x.overflowing_add(1); // y == 0, overflowed == true
      • 核心:提供溢出后的结果,并显式指示溢出是否发生。
    • saturating_* 方法

      • 描述:当发生溢出时,值会被夹紧到类型的最大或最小值。适合需要确保结果在一定范围内的情况。
      • 用法saturating_addsaturating_subsaturating_mul等。
      • 示例
        1. let x: u8 = 255;
        2. let y = x.saturating_add(1); // y == 255
      • 核心:溢出后结果被限制在合法范围内(最大或最小值)。
    数字字面量下划线

    在Rust中,数字字面量中的下划线(_)可以用于增加可读性,它们不会影响数值的实际值。

    示例:1_000.000_1 表示 1000.0001

    Rust字符

    Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。Unicode 值的范围从 U+0000 ~ U+D7FF 和 U+E000 ~ U+10FFFF

    由于 Unicode 都是 4 个字节编码,因此字符类型也是占用 4 个字节:

    单元类型()

    在Rust中,单元类型 () 表示空值或空元组,通常用于函数不返回任何值的情况。尽管逻辑上是空的,但它在内存中占用的大小为0字节。使用 std::mem::size_of_val 可以确认这一点,例如 assert!(size_of_val(&unit) == 0);,这保证了 unit 的内存占用为0,体现了Rust中零大小类型(ZST)的概念和用途。

    所有权
    永远不会返回的函数(发散函数)
    • 发散函数:返回类型为!,表示函数永远不会正常返回控制权。
    • 实现方法
      1. 无限循环:使用loop {}创建一个永不退出的循环。
      2. panic!:触发一个恐慌,使程序中止。
      3. std::process::exit:立即终止程序并返回指定的状态码。

    1. // 方法一:使用无限循环
    2. fn never_return_fn() -> ! {
    3. loop {
    4. // 无限循环,永远不会返回
    5. }
    6. }
    7. // 方法二:调用panic!
    8. fn never_return_fn() -> ! {
    9. panic!("This function never returns!");
    10. }
    11. // 方法三:使用std::process::exit
    12. use std::process;
    13. fn never_return_fn() -> ! {
    14. process::exit(1); // 退出程序并返回状态码1
    15. }
    当所有权转移时,可变性也可以随之改变。
    1. let x = 5; // 不可变变量
    2. let mut y = x; // 所有权转移,y 变为可变
    3. y += 1; // 修改 y 的值
    部分移动
    • 部分移动

      • 解构时可以同时移动变量的一部分,并借用另一部分。
      • 被移动部分的所有权转移,原变量不能再使用该部分。
      • 被借用部分仍然可以通过引用使用。
    • 原变量状态

      • 整体不能再使用,因为部分所有权已转移。
      • 未转移所有权的部分仍可通过引用使用。
    1. fn main() {
    2. let t = (String::from("hello"), String::from("world"));
    3. let _s = t.0;
    4. // 仅修改下面这行代码,且不要使用 `_s`
    5. println!("{:?}", t.1);
    6. }
    Ref和&的区别
    • 使用场景

      • & 是直接引用,用于创建一个指向某个值的引用,适用于任何需要引用的地方。
      • ref 主要在模式匹配中使用,用于方便地在模式匹配过程中获取某个值的引用。
    • 代码简洁性和可读性

      • 使用 & 创建引用时,代码逻辑清晰,直接指向某个值,易于理解。
      • 使用 ref 在模式匹配中创建引用,可以使模式匹配的代码更加简洁和直观,避免了在模式匹配外部手动创建引用的繁琐。

    当你在模式匹配中需要创建多个嵌套值的引用时,ref 可以大大简化代码的编写和阅读。例如:

    1. struct Point {
    2. x: i32,
    3. y: i32,
    4. }
    5. let p = Point { x: 10, y: 20 };
    6. match p {
    7. Point { ref x, ref y } => {
    8. // 在这里 x 和 y 都是引用
    9. println!("x: {}, y: {}", x, y);
    10. }
    11. }

    在这个例子中,使用 ref 可以直接在模式匹配中创建 xy 的引用。如果不使用 ref,你需要手动创建引用,这样会使代码变得更复杂:

    1. struct Point {
    2. x: i32,
    3. y: i32,
    4. }
    5. let p = Point { x: 10, y: 20 };
    6. let Point { x, y } = p;
    7. let x_ref = &x;
    8. let y_ref = &y;
    9. println!("x: {}, y: {}", x_ref, y_ref);
    借用规则
    • 不可变引用与可变引用:Rust 不允许同时存在不可变引用和可变引用,以确保内存安全。
    • 生命周期管理:在可变借用之前,必须完成所有对数据的不可变引用。
    错误原因
    • 调用 s.clear() 时存在对 s 的不可变引用 ch,违反借用规则。
    修正代码

    确保在修改原数据前处理完所有对数据的引用:

    1. fn main() {
    2. let mut s = String::from("hello world");
    3. // 获取第一个字符的不可变引用
    4. let ch = first_character(&s);
    5. println!("the first character is: {}", ch);
    6. // 清空字符串,在使用完不可变引用之后
    7. s.clear();
    8. }
    9. fn first_character(s: &str) -> &str {
    10. &s[..1]
    11. }

    字符串 
    进行切片时需要注意字符边界

    UTF-8 编码的字符可能占用多个字节,切片操作必须在字符边界上进行,否则程序会崩溃。

    核心要点:

    1. UTF-8 编码:字符可能占用1至4个字节,汉字通常占3个字节。
    2. 字符边界:切片索引必须对齐到字符边界。
    3. 避免崩溃:切片时需确保索引在字符边界,否则会导致程序崩溃。
    4. 使用工具:可以使用 .char_indices() 方法获取字符边界,确保切片安全。

    示例:

    1. let s = "中国人";
    2. let a = &s[0..3]; // 正确的切片,取第一个汉字 "中"
    3. println!("{}", a); // 输出 "中"
    String 与 &str 的转换
    • 从 &str 到 String:

      • String::from("hello, world")
      • "hello, world".to_string()
    • 从 String 到 &str:

      • &s
      • &s[..]
      • s.as_str()

    这背后的原理是 Rust 的 Deref 隐式强制转换。

    不允许直接通过索引访问字符串中的字符
    • 底层存储:Rust 字符串底层是字节数组 [u8]
    • UTF-8 编码:字符占用不同字节数,索引必须对齐字符边界。
    • 多种表示方式:Rust 提供多种字符串表示方式,适合不同需求。
    • 索引限制:为了安全和性能考虑,Rust 不允许直接索引字符串。
    • 遍历字符串:使用 charsbytes 方法遍历字符串,确保正确处理字符和字节。
    字符串转义与原样字符串

    Rust 提供了多种方法来处理字符串中的转义字符和原样字符串。以下是详细说明及核心概括。

    1. 转义字符
    • ASCII 转义: 使用 \x 后跟两个十六进制数来表示 ASCII 字符。
    • Unicode 转义: 使用 \u 后跟花括号中的 Unicode 码点来表示 Unicode 字符。

    示例代码:

    1. fn main() {
    2. // 通过 \ + 字符的十六进制表示,转义输出一个字符
    3. let byte_escape = "I'm writing \x52\x75\x73\x74!";
    4. println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);
    5. // \u 可以输出一个 unicode 字符
    6. let unicode_codepoint = "\u{211D}";
    7. let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";
    8. println!(
    9. "Unicode character {} (U+211D) is called {}",
    10. unicode_codepoint, character_name
    11. );
    12. // 换行了也会保持之前的字符串格式
    13. // 使用\忽略换行符
    14. let long_string = "String literals
    15. can span multiple lines.
    16. The linebreak and indentation here ->\
    17. <- can be escaped too!";
    18. println!("{}", long_string);
    19. }

    运行结果:

    1. What are you doing? (\x3F means ?) I'm writing Rust!
    2. Unicode character ℝ (U+211D) is called "DOUBLE-STRUCK CAPITAL R"
    3. String literals
    4. can span multiple lines.
    5. The linebreak and indentation here -><- can be escaped too!
    2. 原样字符串
    • 原样字符串: 使用 r## 包围字符串,忽略转义字符。
    • 带有双引号的字符串: 使用多个 # 包围字符串,以处理双引号和其他复杂情况。

    示例代码:

    1. fn main() {
    2. // 保持字符串的原样输出
    3. println!("{}", "hello \\x52\\x75\\x73\\x74");
    4. // 原样字符串
    5. let raw_str = r"Escapes don't work here: \x3F \u{211D}";
    6. println!("{}", raw_str);
    7. // 包含双引号的原样字符串
    8. let quotes = r#"And then I said: "There is no escape!""#;
    9. println!("{}", quotes);
    10. // 使用多个 # 处理复杂情况
    11. let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    12. println!("{}", longer_delimiter);
    13. }

    运行结果:

    1. hello \x52\x75\x73\x74
    2. Escapes don't work here: \x3F \u{211D}
    3. And then I said: "There is no escape!"
    4. A string with "# in it. And even "##!
    操作 UTF-8 字符串
    1. 遍历字符

    如果你想以 Unicode 字符的方式遍历字符串,可以使用 chars 方法:

    示例代码:

    1. fn main() {
    2. for c in "中国人".chars() {
    3. println!("{}", c);
    4. }
    5. }

    输出:

    2. 遍历字节

    如果你想查看字符串的底层字节数组,可以使用 bytes 方法:

    示例代码:

    1. fn main() {
    2. for b in "中国人".bytes() {
    3. println!("{}", b);
    4. }
    5. }

    输出:

    1. 228
    2. 184
    3. 173
    4. 229
    5. 155
    6. 189
    7. 228
    8. 186
    9. 186
    3. 获取子串

    由于 UTF-8 字符串的变长特性,从中获取子串相对复杂。Rust 标准库无法直接支持按字符边界提取子串。需要使用第三方库(如 utf8_slice)来实现。

    示例代码:

    1. fn main() {
    2. let s = "holla中国人नमस्ते";
    3. // 使用第三方库 utf8_slice 提取子串
    4. let sub = utf8_slice::slice(s, 5, 8);
    5. println!("{}", sub); // 输出: 中国
    6. }
    str和&str
    • 字符串字面量类型:

      • 字符串字面量的类型是 &str
    • str 类型的使用:

      • str 类型表示字符串的不可变视图,通常无法直接使用。
      • 可以使用 &str 来处理字符串的引用。
    • Box&str 的转换:

      • 使用 Boxstr 类型存储在堆上,通过引用 &Box 转换为 &str
    数组和元组
    元组过长错误
    1. 问题:Rust 默认只支持最多12个元素的元组,超过会导致编译错误。

    2. 解决方案

      • 使用数组:适用于元素类型相同的情况。
      • 使用结构体:适用于元素类型不同的情况。
      • 分解元组:将长元组分解成多个短元组。
    非基础类型数组的所有权问题
    • 基础类型与复杂类型区别

      • 基础类型(如 i32)支持 Copy 特性,可以通过 [value; n] 语法初始化。
      • 复杂类型(如 String)不支持 Copy 特性,需要逐个创建元素。
    • 解决方法:使用 std::array::from_fn 函数,通过闭包生成每个元素,避免所有权问题。

    切片大小
    • 切片:是对数组部分的引用,包含一个指针和长度。
    • 64 位系统:指针和长度各占 8 字节,因此切片总大小为 16 字节。
    错误示例修正

    原代码错误是将切片大小误认为 8 字节。应将 assert! 中的值修改为 16。

    修正代码
    1. fn main() {
    2. let arr: [char; 3] = ['中', '国', '人'];
    3. let slice = &arr[..2];
    4. // 修改数字 `8` 让代码工作
    5. assert!(std::mem::size_of_val(&slice) == 16);
    6. }

    结构体
    结构体更新语法
    • 结构体更新语法:允许基于现有结构体实例创建新实例,只需指定改变的字段,其余字段自动从现有实例中获取。
    • 所有权转移:结构体更新语法会转移所有权,涉及 Copy 特征的字段会被拷贝,不涉及 Copy 特征的字段会发生所有权转移。
    示例代码

    传统方式:

    1. let user2 = User {
    2. active: user1.active,
    3. username: user1.username,
    4. email: String::from("another@example.com"),
    5. sign_in_count: user1.sign_in_count,
    6. };

    结构体更新语法:

    1. let user2 = User {
    2. email: String::from("another@example.com"),
    3. ..user1
    4. };
    所有权转移与 Copy 特征
    • Copy 特征boolu64 等实现了 Copy 特征的类型在赋值时会被拷贝,不会转移所有权。
    • Copy 类型:如 String 等在赋值时会转移所有权,导致原结构体实例中的对应字段不能再被使用。
    例子
    1. struct User {
    2. active: bool,
    3. username: String,
    4. email: String,
    5. sign_in_count: u64,
    6. }
    7. fn main() {
    8. let user1 = User {
    9. email: String::from("someone@example.com"),
    10. username: String::from("someusername123"),
    11. active: true,
    12. sign_in_count: 1,
    13. };
    14. let user2 = User {
    15. email: String::from("another@example.com"),
    16. ..user1
    17. };
    18. // 使用了 Copy 特征的字段可以继续使用
    19. println!("{}", user1.active); // 依然有效
    20. // 使用了所有权转移的字段将导致错误
    21. // println!("{:?}", user1); // 错误:user1 的 username 字段所有权已被转移
    22. }
    结构体内存布局
    1. struct File {
    2. name: String,
    3. data: Vec<u8>,
    4. }

    从图中可以清晰地看出 File 结构体两个字段 name 和 data 分别拥有底层两个 [u8] 数组的所有权(String 类型的底层也是 [u8] 数组),通过 ptr 指针指向底层数组的内存地址,这里你可以把 ptr 指针理解为 Rust 中的引用类型。

    该图片也侧面印证了:把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段

    Rust 元组结构体
    定义
    • 语法:
      struct 结构体名(字段类型1, 字段类型2, ...);
      
      示例:
      1. struct Color(i32, i32, i32);
      2. struct Point(i32, i32, i32);
    使用场景
    • 当需要一个整体名称,但不关心内部字段的名称时。
    • 适用于明确且简单的字段组合,如 3D 坐标点 (x, y, z)。
    #[derive(Debug)] 打印结构体
    核心概念
    • #[derive(Debug)] 注解: 使结构体或枚举自动实现 Debug 特征,从而能够使用调试格式进行打印。
    实现步骤
    1. 为结构体派生 Debug 特征:

      1. #[derive(Debug)]
      2. struct Rectangle {
      3. width: u32,
      4. height: u32,
      5. }
    2. 调试打印:

      • 使用 dbg! 宏打印调试信息到标准错误输出 stderr
        1. let rect1 = Rectangle {
        2. width: dbg!(30 * scale),
        3. height: 50,
        4. };
        5. dbg!(&rect1);
      • 使用 println! 宏在标准输出 stdout 中打印调试信息:
        println!("{:?}", rect1);
        

    枚举 
    • 枚举类型定义
      • 枚举类型用于定义一组相关的值。
      • 不能在枚举中使用浮点数作为值。
    • 枚举值比较
      • 不同枚举类型中的值不能直接比较。
      • 使用 as 操作符将枚举值转换为整数类型后进行比较。
    1. // 修复错误
    2. enum Number {
    3. Zero,
    4. One,
    5. Two,
    6. }
    7. enum Number1 {
    8. Zero = 0,
    9. One,
    10. Two,
    11. }
    12. // C语言风格的枚举定义
    13. enum Number2 {
    14. Zero = 0,
    15. One = 1,
    16. Two = 2,
    17. }
    18. fn main() {
    19. // 通过 `as` 可以将枚举值强转为整数类型
    20. assert_eq!(Number::One as i32, Number1::One as i32);
    21. assert_eq!(Number1::One as i32, Number2::One as i32);
    22. }
    初始化枚举
    1. enum Message {
    2. Quit,
    3. Move { x: i32, y: i32 },
    4. Write(String),
    5. ChangeColor(i32, i32, i32),
    6. }
    • 使用字段名初始化

      • 初始化结构体样式的枚举成员时,必须使用字段名。
      let msg1 = Message::Move { x: 1, y: 2 };
      
    • 确保类型正确

      • 对于包含数据的枚举成员,确保数据类型正确。
      let msg2 = Message::Write(String::from("hello, world!"));
      

    • Option 类型处理
      • Option 是 Rust 中用来表示可能为空的值的类型。
      • 通过 unwrap_or 方法可以为 Option 提供一个默认值,从而避免类型不匹配的问题。
    下划线 _ 是一个通配符

    用来匹配任何值但不绑定该值。这意味着当我们不需要使用该值时,可以用 _ 来忽略它。

    • 模式匹配与解构
      • unwrap_or

        • 用于从 Option 中提取值,如果 OptionNone,则返回提供的默认值。
        • 示例:
          let value = some_option.unwrap_or(0);
      • unwrap_or_else

        • 类似于 unwrap_or,但接受一个闭包,在 OptionNone 时执行该闭包并返回结果。
        • 示例:
          1. let value = some_option.unwrap_or_else(|| {
          2. println!("No value found, using default");
          3. 0
          4. });
      • expect

        • 用于从 Option 中提取值,如果 OptionNone,则程序 panic 并输出自定义错误消息。
        • 示例:
          let value = some_option.expect("Expected a value but found None");
          
    流程控制

    break 可以在循环中返回一个值,允许在退出循环时传递该值,从而简化特定条件下的值传递。

    1. fn main() {
    2. let mut n = 1;
    3. let result = loop {
    4. let square = n * n;
    5. if square > 100 {
    6. break square; // 返回第一个大于100的平方数
    7. }
    8. n += 1;
    9. };
    10. println!("The first square greater than 100 is {}", result);
    11. }

    模式匹配(match

    使用 match 表达式优雅地处理 Option 的所有可能情况,避免直接使用 unwrap 可能带来的 panic 问题。

    1. match some_option {
    2. Some(value) => println!("Value is: {}", value),
    3. None => println!("No value found"),
    4. }

    使用模式 &mut V 去匹配一个可变引用时,你需要格外小心,因为匹配出来的 V 是一个值,而不是可变引用

    1. fn main() {
    2. let mut v = String::from("hello,");
    3. let r = &mut v;
    4. match r {
    5. // &mut value => value.push_str(" world!")
    6. value => value.push_str(" world!")
    7. }
    8. }
    泛型
    泛型示例:
    1. fn print_arrayDebug, const N:usize>(arr:[T;N]) {
    2. println!("{:?}", arr);
    3. }
    类型大小

    1.&str包含指针和字符串长度,两个字段共16个字节

    2.String包含指针,字符串长度,容量,三个字段共24个字节

    3.Rust中char均为4个字节(UTF-8)

    1. #![allow(incomplete_features)]
    2. #![feature(generic_const_exprs)]
    3. fn check_size<T>(val: T)
    4. where
    5. Assert<{ core::mem::size_of::<T>() < 768 }>: IsTrue,
    6. {
    7. //...
    8. }
    9. // 修复 main 函数中的错误
    10. fn main() {
    11. check_size([0u8; 767]);
    12. check_size([0i32; 191]);
    13. check_size(["hello你好"; 47]); // size of &str ?
    14. check_size([(); ].map(|_| "hello你好".to_string())); // size of String?
    15. check_size(['中'; 191]); // size of char ?
    16. }
    17. pub enum Assert<const CHECK: bool> {}
    18. pub trait IsTrue {}
    19. impl IsTrue for Assert<true> {}

    特征

    特征

    1.孤儿规则的定义

    孤儿规则规定:如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的。这意味着:

    1. 如果你定义了一个特征,你可以为任何类型实现这个特征。
    2. 如果你定义了一个类型,你可以为这个类型实现任何特征。
    3. 但是,你不能为两个都不在你当前作用域中的类型和特征实现一个新的实现。

    不能做的:

    1. impl Display for String {
    2. fn fmt(&self, f: &mut Formatter) -> Result {
    3. write!(f, "{}", self)
    4. }
    5. }

    解释:Display 特征和 String 类型都定义在标准库中,而不是在当前作用域中。因此,孤儿规则禁止我们在当前作用域中为 String 实现 Display 特征。 

    2. 返回impl Trait

    这种 impl Trait 形式的返回值,在返回的真实类型非常复杂,你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型)非常有用,此时就可以用 impl Trait 的方式简单返回 

    但这种返回值方式只能有一个具体的类型

    1. fn returns_summarizable() -> impl Summary {
    2. Weibo {
    3. username: String::from("sunface"),
    4. content: String::from(
    5. "m1 max太厉害了,电脑再也不会卡",
    6. )
    7. }
    8. }
    3.使用 Output 关联类型

    可以在以下几种情况下特别有用:

    1. 当乘法运算的结果类型与操作数类型不同。
    2. 在泛型编程中,通过类型约束确保返回的结果类型与预期一致。
    4.impl Trait作为函数参数

    通过 impl Trait 的方式来指定实现了该特征的参数:该参数能接受的类型必须要实现指定的特征。

    5.函数特征传参注意

    (1)注意传入的参数需要实现相应特征

    1. struct Pair<T> {
    2. x: T,
    3. y: T,
    4. }
    5. impl<T> Pair<T> {
    6. fn new(x: T, y: T) -> Self {
    7. Self {
    8. x,
    9. y,
    10. }
    11. }
    12. }
    13. impl<T: std::fmt::Debug + PartialOrd> Pair<T> {
    14. fn cmp_display(&self) {
    15. if self.x >= self.y {
    16. println!("The largest member is x = {:?}", self.x);
    17. } else {
    18. println!("The largest member is y = {:?}", self.y);
    19. }
    20. }
    21. }
    22. #[derive(Debug, PartialEq, PartialOrd)]
    23. struct Unit(i32);
    24. fn main() {
    25. let pair = Pair{
    26. x: Unit(1),
    27. y: Unit(3)
    28. };
    29. pair.cmp_display();
    30. }

     特征对象

    1.对象安全

    不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,它的对象才是安全的:

    • 方法的返回类型不能是 Self
    • 方法没有任何泛型参数
    2.Self和self

    在 Rust 中,有两个self,一个指代当前的实例对象,一个指代特征或者方法类型的别名:

    1. trait Draw {
    2. fn draw(&self) -> Self;
    3. }
    4. #[derive(Clone)]
    5. struct Button;
    6. impl Draw for Button {
    7. fn draw(&self) -> Self {
    8. return self.clone()
    9. }
    10. }
    11. fn main() {
    12. let button = Button;
    13. let newb = button.draw();
    14. }
    3.返回impl Trait 多个类型

    通过 impl Trait 返回多个类型是不被允许的,但是我们可以返回一个 dyn 特征对象来解决问题。 

    1. trait Bird {
    2. fn quack(&self) -> String;
    3. }
    4. struct Duck;
    5. impl Duck {
    6. fn swim(&self) {
    7. println!("Look, the duck is swimming")
    8. }
    9. }
    10. struct Swan;
    11. impl Swan {
    12. fn fly(&self) {
    13. println!("Look, the duck.. oh sorry, the swan is flying")
    14. }
    15. }
    16. impl Bird for Duck {
    17. fn quack(&self) -> String{
    18. "duck duck".to_string()
    19. }
    20. }
    21. impl Bird for Swan {
    22. fn quack(&self) -> String{
    23. "swan swan".to_string()
    24. }
    25. }
    26. fn main() {
    27. // 填空
    28. let duck = Duck{};
    29. duck.swim();
    30. let bird = hatch_a_bird(2);
    31. // 变成鸟儿后,它忘记了如何游,因此以下代码会报错
    32. // bird.swim();
    33. // 但它依然可以叫唤
    34. assert_eq!(bird.quack(), "duck duck");
    35. let bird = hatch_a_bird(1);
    36. // 这只鸟儿忘了如何飞翔,因此以下代码会报错
    37. // bird.fly();
    38. // 但它也可以叫唤
    39. assert_eq!(bird.quack(), "swan swan");
    40. println!("Success!")
    41. }
    42. fn hatch_a_bird(n: u32) -> Box<dyn Bird> {
    43. match n {
    44. 1 => Box::new(Swan),
    45. 2 => Box::new(Duck),
    46. _ => Box::new(Duck),
    47. }
    48. }
    4.数组中使用特征 
    1.错误示例 
    let birds: [Box<dyn Bird>; 9] = [Box::new(Bird); 9];
    1. 语法错误Bird 是一个特性,不能直接实例化。必须使用实现了 Bird 特性的具体结构体。
    2. 数组初始化[Box::new(Bird); 9] 是尝试使用特征对象 Bird 来初始化数组,这在语法和逻辑上都是错误的。
    2.正确示例

    let birds: [Box; 2] = [Box::new(Duck), Box::new(Swan)];

    1. 正确的特征对象初始化:使用 Box::new(Duck)Box::new(Swan) 创建特征对象的数组元素是正确的做法。
    2. 数组长度:数组长度与初始化元素数量匹配。
    1. trait Bird {
    2. fn quack(&self);
    3. }
    4. struct Duck;
    5. impl Duck {
    6. fn fly(&self) {
    7. println!("Look, the duck is flying")
    8. }
    9. }
    10. struct Swan;
    11. impl Swan {
    12. fn fly(&self) {
    13. println!("Look, the duck.. oh sorry, the swan is flying")
    14. }
    15. }
    16. impl Bird for Duck {
    17. fn quack(&self) {
    18. println!("{}", "duck duck");
    19. }
    20. }
    21. impl Bird for Swan {
    22. fn quack(&self) {
    23. println!("{}", "swan swan");
    24. }
    25. }
    26. fn main() {
    27. // 填空
    28. // 错误 let birds: [Box;9]= [Box::new(Bird);9];
    29. let birds: [Box<dyn Bird>; 2] = [Box::new(Duck), Box::new(Swan)];
    30. for bird in birds {
    31. bird.quack();
    32. // 当 duck 和 swan 变成 bird 后,它们都忘了如何翱翔于天际,只记得该怎么叫唤了。。
    33. // 因此,以下代码会报错
    34. // bird.fly();
    35. }
    36. }
    5. 处理动态分发的特性对象
    对象类型:
    • &dyn Trait

      • 类型:引用
      • 内存管理:不负责
      • 可变性:不可变
      • 用法:短期借用,只读访问
    • Box

      • 类型:智能指针
      • 内存管理:负责
      • 可变性:可变
      • 用法:长期存储,可能修改数据
     函数类型定义:
    1. fn draw1(x: Box<dyn Draw>) {
    2. // 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法
    3. x.draw();
    4. }
    实例化
    draw1(Box::new(x));

    6.动态分发与特征对象
    核心概念
    1. 静态分发(Static Dispatch)

      • 在编译期确定,编译器为每个具体类型生成代码。
      • 性能高,因为没有运行时开销。
      • 这种方法在编译期就确定了具体的方法实现,因此不需要运行时的查找过程。静态分发通常通过泛型实现。
    2. 动态分发(Dynamic Dispatch)

      • 在运行时确定调用哪个方法。
      • 使用 dyn 关键字创建特征对象。
      • 编译器无法在编译期知晓具体类型,因此使用运行时的指针来确定调用的方法。
    特征对象的实现
    1. 大小不固定:特征对象(如 dyn Draw)的大小不固定,因为不同的类型可以实现相同的特征。
    2. 引用方式:通常使用特征对象的引用方式,如 &dyn DrawBox
    3. 固定大小的引用:特征对象的引用类型大小固定,由两个指针组成:
      • 数据指针(ptr):指向实现了特征的具体类型的实例。
      • 虚表指针(vptr):指向虚表(vtable),保存了实例对应的特征方法。
    虚表(vtable)
    • 虚表的作用:当调用特征对象的方法时,Rust 使用虚表来找到并调用具体的方法。
    • 方法调用过程
      1. ptr 指向实例数据。
      2. vptr 指向虚表,虚表中保存了可以调用的方法指针。
      3. 通过虚表找到并调用对应的方法。

    1. trait Foo {
    2. fn method(&self) -> String;
    3. }
    4. impl Foo for u8 {
    5. fn method(&self) -> String { format!("u8: {}", *self) }
    6. }
    7. impl Foo for String {
    8. fn method(&self) -> String { format!("string: {}", *self) }
    9. }
    10. // 通过泛型实现以下函数
    11. fn static_dispatch<T: Foo>(elem : T){
    12. elem.method();
    13. }
    14. // 通过特征对象实现以下函数
    15. fn dynamic_dispatch(elem : Box<dyn Foo>){
    16. elem.method();
    17. }
    7.解决特征对象的对象安全性问题
    1. // 使用至少两种方法让代码工作
    2. // 不要添加/删除任何代码行
    3. trait MyTrait {
    4. fn f(&self) -> Self;
    5. }
    6. impl MyTrait for u32 {
    7. fn f(&self) -> Self { 42 }
    8. }
    9. impl MyTrait for String {
    10. fn f(&self) -> Self { self.clone() }
    11. }
    12. fn my_function(x: Box<dyn MyTrait>) {
    13. x.f()
    14. }
    15. fn main() {
    16. my_function(Box::new(13_u32));
    17. my_function(Box::new(String::from("abc")));
    18. println!("Success!")
    19. }
    • 方法一通过修改特征方法的返回类型,使其符合对象安全性要求,从而可以使用特征对象。
      1. trait MyTrait {
      2. fn f(&self) -> Box<dyn MyTrait>;
      3. }
      4. impl MyTrait for u32 {
      5. fn f(&self) -> Box<dyn MyTrait> { Box::new(42) }
      6. }
      7. impl MyTrait for String {
      8. fn f(&self) -> Box<dyn MyTrait> { Box::new(self.clone()) }
      9. }
      10. fn my_function(x: Box<dyn MyTrait>) {
      11. x.f();
      12. }
      13. fn main() {
      14. my_function(Box::new(13_u32));
      15. my_function(Box::new(String::from("abc")));
      16. println!("Success!")
      17. }

    • 方法二通过使用泛型,使编译器在编译期确定类型,从而避免了对象安全性问题。
    1. trait MyTrait {
    2. fn f(&self) -> Self;
    3. }
    4. impl MyTrait for u32 {
    5. fn f(&self) -> Self { 42 }
    6. }
    7. impl MyTrait for String {
    8. fn f(&self) -> Self { self.clone() }
    9. }
    10. fn my_function<T: MyTrait>(x: T) {
    11. x.f();
    12. }
    13. fn main() {
    14. my_function(13_u32);
    15. my_function(String::from("abc"));
    16. println!("Success!")
    17. }

     深入理解特征

    1. 关联类型

    定义: 关联类型是在特征定义中声明的类型,用于在特征的方法签名中使用。

    例子:

    1. struct Container(i32, i32);
    2. // 使用关联类型实现重新实现以下特征
    3. // trait Contains {
    4. // type A;
    5. // type B;
    6. trait Contains {
    7. type A;
    8. type B;
    9. fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
    10. fn first(&self) -> i32;
    11. fn last(&self) -> i32;
    12. }
    13. impl Contains for Container {
    14. type A = i32;
    15. type B = i32;
    16. fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
    17. (&self.0 == number_1) && (&self.1 == number_2)
    18. }
    19. // Grab the first number.
    20. fn first(&self) -> i32 { self.0 }
    21. // Grab the last number.
    22. fn last(&self) -> i32 { self.1 }
    23. }
    24. fn difference (container: &dyn Contains<A = i32, B = i32>) -> i32 {
    25. container.last() - container.first()
    26. }
    27. fn main() {
    28. let number_1 = 3;
    29. let number_2 = 10;
    30. let container = Container(number_1, number_2);
    31. println!("Does container contain {} and {}: {}",
    32. &number_1, &number_2,
    33. container.contains(&number_1, &number_2));
    34. println!("First number: {}", container.first());
    35. println!("Last number: {}", container.last());
    36. println!("The difference is: {}", difference(&container));
    37. }

    关键点:

    • 用途:简化复杂类型的使用,使代码更加可读。
    • 使用Self::Item 用于指代实现特征时定义的具体类型。

    总结: 关联类型简化了特征实现中的类型定义,提高了代码的可读性和简洁性。

    2. 默认泛型类型参数

    定义: 为泛型类型参数指定一个默认的具体类型。

    例子:

    1. impl<T: Sub<Output = T>> Sub> for Point {
    2. type Output = Self;
    3. fn sub(self, other: Self) -> Self::Output {
    4. Point {
    5. x: self.x - other.x,
    6. y: self.y - other.y,
    7. }
    8. }
    9. }

    T: Sub 确保泛型类型 T 必须实现 Sub trait,并且减法操作的结果类型是 T

    关键点:

    • 减少样板代码:无需在所有地方都指定泛型类型。
    • 扩展性:可以在不修改现有代码的情况下扩展类型。
    3. 调用同名方法和完全限定语法

    定义: 在一个类型上实现多个特征时,可能会有同名的方法。可以通过完全限定语法显式调用特定特征的方法。

    例子:

    1. trait Pilot {
    2. fn fly(&self);
    3. }
    4. trait Wizard {
    5. fn fly(&self);
    6. }
    7. impl Pilot for Human {
    8. fn fly(&self) {
    9. println!("This is your captain speaking.");
    10. }
    11. }
    12. impl Wizard for Human {
    13. fn fly(&self) {
    14. println!("Up!");
    15. }
    16. }
    17. impl Human {
    18. fn fly(&self) {
    19. println!("*waving arms furiously*");
    20. }
    21. }

    调用方法:

    1. let person = Human;
    2. Pilot::fly(&person); // 调用 Pilot 特征的方法
    3. Wizard::fly(&person); // 调用 Wizard 特征的方法
    4. person.fly(); // 调用 Human 类型的方法
    完全限定语法:
    语法格式

    完全限定语法的格式如下:

    <Type as Trait>::function(receiver_if_method, next_arg, ...);
    
    • :指定类型和特征。
    • function:要调用的方法或函数。
    • receiver_if_method:方法的接收器(例如 self&self&mut self)。
    • next_arg:方法或函数的其余参数。
    1. fn main() {
    2. println!("A baby dog is called a {}", ::baby_name());
    3. }

    总结: 通过显式调用和完全限定语法,可以避免方法名称冲突,明确调用的目标方法。

    4. 特征定义中的特征约束

    定义: 特征约束是指在定义一个特征时,要求实现该特征的类型也必须实现另一个特征。

    例子:

    1. trait Person {
    2. fn name(&self) -> String;
    3. }
    4. // Person 是 Student 的 supertrait .
    5. // 实现 Student 需要同时实现 Person.
    6. trait Student: Person {
    7. fn university(&self) -> String;
    8. }
    9. trait Programmer {
    10. fn fav_language(&self) -> String;
    11. }
    12. // CompSciStudent (computer science student) 是 Programmer
    13. // 和 Student 的 subtrait. 实现 CompSciStudent 需要先实现这两个 supertraits.
    14. trait CompSciStudent: Programmer + Student {
    15. fn git_username(&self) -> String;
    16. }
    17. fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
    18. format!(
    19. "My name is {} and I attend {}. My favorite language is {}. My Git username is {}",
    20. student.name(),
    21. student.university(),
    22. student.fav_language(),
    23. student.git_username()
    24. )
    25. }
    26. struct CSStudent {
    27. name: String,
    28. university: String,
    29. fav_language: String,
    30. git_username: String
    31. }
    32. impl Person for CSStudent {
    33. fn name(&self) -> String {
    34. self.name.clone()
    35. }
    36. }
    37. impl Student for CSStudent {
    38. fn university(&self) -> String {
    39. self.university.clone()
    40. }
    41. }
    42. impl Programmer for CSStudent {
    43. fn fav_language(&self) -> String {
    44. self.fav_language.clone()
    45. }
    46. }
    47. impl CompSciStudent for CSStudent {
    48. fn git_username(&self) -> String {
    49. self.git_username.clone()
    50. }
    51. }
    52. fn main() {
    53. let student = CSStudent {
    54. name: "Sunfei".to_string(),
    55. university: "XXX".to_string(),
    56. fav_language: "Rust".to_string(),
    57. git_username: "sunface".to_string()
    58. };
    59. // 填空
    60. println!("{}", comp_sci_student_greeting(&student));
    61. }

    (1)trait CompSciStudent: Programmer + Student 表示 CompSciStudentProgrammerStudent 的 subtrait。实现 CompSciStudent 的类型必须同时实现 ProgrammerStudent

    (2)CSStudent 需要逐个实现了 PersonStudentProgrammerCompSciStudent 这些 trait。

    (3)fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String 函数使用了动态调度,通过 &dyn CompSciStudent 允许在运行时确定具体类型。 

    关键点:

    • 用途:确保特征的方法可以使用另一个特征的方法。
    • 实现约束:在实现特征时,同时实现必要的依赖特征。

    5. 在外部类型上实现外部特征(newtype 模式)

    定义: newtype 模式通过创建一个新类型(元组结构体)来绕过孤儿规则,从而实现外部类型上的外部特征。

    例子:

    1. struct Wrapper(Vec<String>);
    2. impl fmt::Display for Wrapper {
    3. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    4. write!(f, "[{}]", self.0.join(", "))
    5. }
    6. }
    7. use std::fmt;
    8. // 定义一个 newtype ``
    9. struct Pretty (String);
    10. impl fmt::Display for Pretty {
    11. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    12. write!(f, "\"{}\"", self.0.clone() + ", world")
    13. }
    14. }
    15. fn main() {
    16. let w = Pretty("hello".to_string());
    17. println!("w = {}", w);
    18. }

     在这个例子中,我们定义了一个新的元组结构体类型 Wrapper,其内部包含一个 Vec。然后,我们为 Wrapper 实现了 fmt::Display 特征,这样就可以打印 Wrapper 包含的字符串向量。

    关键点:

    • 解决孤儿规则:允许为外部类型实现外部特征。
    • 运行时无性能损耗:编译期自动忽略封装类型。
    • 使用 Deref 特征:简化封装类型的方法调用。

    集合

    vector

    概述
    • 定义:在 Rust 中,动态数组类型用 Vec 表示。
    • 特性:存储多个值,内存中紧挨排列,低访问成本,只能存储相同类型的元素。
    创建动态数组
    1. Vec::new:标准方法,需显式声明类型。
      1. let v: Vec<i32> = Vec::new();
      2. let mut v = Vec::new();
      3. v.push(1); // 自动推断类型
    2. Vec::with_capacity:预先指定容量,提高性能。
    3. let mut v = Vec::with_capacity(10);
      
    4. vec![] 宏:同时创建并初始化。
      let v = vec![1, 2, 3];
      
    更新 Vector
    • 添加元素:使用 push 方法。
      1. let mut v = Vec::new();
      2. v.push(1);
    生命周期
    • 自动删除:超出作用域自动删除,所有内容随之删除。
      1. {
      2. let v = vec![1, 2, 3];
      3. } // v 超出作用域被删除
    读取元素
    1. 下标索引访问
      1. let v = vec![1, 2, 3, 4, 5];
      2. let third: &i32 = &v[2];
      3. println!("第三个元素是 {}", third);
    2. 使用 get 方法
      1. match v.get(2) {
      2. Some(third) => println!("第三个元素是 {third}"),
      3. None => println!("去你的第三个元素,根本没有!"),
      4. }
    安全性
    • 下标索引:越界访问会报错。
    • .get 方法:越界返回 None
      1. let does_not_exist = &v[100]; // 报错
      2. let does_not_exist = v.get(100); // 安全
    借用规则
    • 同时借用多个元素:不能同时进行不可变和可变借用。
      1. let mut v = vec![1, 2, 3, 4, 5];
      2. let first = &v[0];
      3. v.push(6);
      4. println!("The first element is: {first}"); // 编译器报错
    迭代遍历
    • 遍历数组元素
      1. let v = vec![1, 2, 3];
      2. for i in &v {
      3. println!("{i}");
      4. }
    • 修改数组元素
      1. let mut v = vec![1, 2, 3];
      2. for i in &mut v {
      3. *i += 10;
      4. }
    存储不同类型的元素
    1. 枚举类型

      1. #[derive(Debug, PartialEq)]
      2. enum IpAddr {
      3. V4(String),
      4. V6(String),
      5. }
      6. fn main() {
      7. // 填空
      8. let v : Vec<IpAddr>= Vec::from([IpAddr::V4("127.0.0.1".to_string()),
      9. IpAddr::V6("::1".to_string())]);
      10. // 枚举的比较需要派生 PartialEq 特征
      11. assert_eq!(v[0], IpAddr::V4("127.0.0.1".to_string()));
      12. assert_eq!(v[1], IpAddr::V6("::1".to_string()));
      13. println!("Success!")
      14. }
    2. 特征对象

      1. trait IpAddr {
      2. fn display(&self);
      3. }
      4. struct V4(String);
      5. struct V6(String);
      6. impl IpAddr for V4 { ... }
      7. impl IpAddr for V6 { ... }
      8. let v: Vec<Box<dyn IpAddr>> = vec![
      9. Box::new(V4("127.0.0.1".to_string())),
      10. Box::new(V6("::1".to_string())),
      11. ];
    Vector 常用方法
    • 初始化方式

      1. let v = vec![0; 3];
      2. let v_from = Vec::from([0, 0, 0]);
    • 管理容量

      1. let mut v = Vec::with_capacity(10);
      2. v.reserve(100);
      3. v.shrink_to_fit();
    • 常见操作

      1. let mut v = vec![1, 2];
      2. v.insert(2, 3);
      3. assert_eq!(v.remove(1), 2);
      4. assert_eq!(v.pop(), Some(3));
      5. v.clear();
    Vector 的排序
    • 整数数组排序

      1. let mut vec = vec![1, 5, 10, 2, 15];
      2. vec.sort_unstable();
    • 浮点数数组排序

    NaN(Not a Number),它代表一个未定义或无法表示的数值。由于 NaN 值的存在,浮点数类型没有实现完全可比较的特性 Ord,而是实现了部分可比较的特性 PartialOrd。 

    1. let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];
    2. vec.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
    • 结构体数组排序

      1. #[derive(Debug)]
      2. struct Person { ... }
      3. let mut people = vec![
      4. Person::new("Zoe".to_string(), 25),
      5. Person::new("Al".to_string(), 60),
      6. ...
      7. ];
      8. people.sort_unstable_by(|a, b| b.age.cmp(&a.age));
      将 X 类型转换成 Vec
    • 数组转换为 Vec
      1. let v1: Vec<i32> = Vec::from(arr);
      2. let v2: Vec<i32> = arr.to_vec();

      这里使用 Vec::fromto_vec 方法将数组 [1, 2, 3] 转换为 Vec

    • String 转换为 Vec
      1. let s = "hello".to_string();
      2. let v1: Vec<u8> = s.into();
      3. let v2 = s.into_bytes();

            to_vecinto_bytes 方法将字符串 "hello" 转换为 Vec

    • str 转换为 Vec
      let v3: Vec<u8> = Vec::from(s);
      

      使用 Vec::from 将字符串切片 &str 转换为 Vec

    • 迭代器转换为 Vec
      let v4: Vec<i32> = [0; 10].into_iter().collect();
      

      通过迭代器和 collect 方法将数组 [0; 10] 转换为 Vec

     切片和 Vec 的区别
    1. // 切片是只读的
    2. // 注意:切片和 `&Vec` 是不同的类型,后者仅仅是 `Vec` 的引用,并可以通过解引用直接获取 `Vec`
    3. let vec_ref: &mut Vec<i32> = &mut v;
    4. (*vec_ref).push(4);
    5. let slice3 = &vec_ref[0..4]; // 修改为正确的切片范围

    Hashmap

    概念概述
    • HashMap 是 Rust 中用于存储键值对的集合类型,查询效率高,提供了平均复杂度为 O(1) 的查询方法。
    • 默认哈希算法:HashMap 默认使用 SipHash 1-3 哈希算法,对于抵抗 HashDos 攻击非常有效,但在性能上对中型大小的 key 表现较好。对于小型(如整数)或大型 key(如字符串),可以考虑使用社区提供的其它哈希算法来提高性能。
    • 实现基础:Rust 的 HashMap 实现基于 Google 的 SwissTable 算法。
    基本操作
    1. // 填空并修复错误
    2. use std::collections::HashMap;
    3. fn main() {
    4. let mut scores = HashMap::new();
    5. scores.insert("Sunface", 98);
    6. scores.insert("Daniel", 95);
    7. scores.insert("Ashley", 69);
    8. scores.insert("Katie", 58);
    9. // get 返回一个 Option<&V> 枚举值
    10. let score = scores.get("Sunface");
    11. assert_eq!(score, Some(98).as_ref());
    12. if scores.contains_key("Daniel") {
    13. // 索引返回一个值 V
    14. let score = scores["Daniel"];
    15. assert_eq!(score, 95);
    16. scores.remove("Daniel");
    17. }
    18. assert_eq!(scores.len(), 3);
    19. for (name, score) in scores {
    20. println!("The score of {} is {}", name, score)
    21. }
    22. }
    创建 HashMap
    1. 使用 new 方法创建
    1. use std::collections::HashMap;
    2. let mut my_gems = HashMap::new();
    3. my_gems.insert("红宝石", 1);
    1. 使用迭代器和 collect 方法创建
    1. use std::collections::HashMap;
    2. let teams_list = vec![
    3. ("中国队".to_string(), 100),
    4. ("美国队".to_string(), 10),
    5. ("日本队".to_string(), 50),
    6. ];
    7. let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
    8. println!("{:?}", teams_map);
    所有权与生命周期
    • 所有权转移:如果值类型实现了 Copy 特征,则会被复制进 HashMap,否则所有权将被转移。
    1. use std::collections::HashMap;
    2. let name = String::from("Sunface");
    3. let age = 18;
    4. let mut handsome_boys = HashMap::new();
    5. handsome_boys.insert(name, age);
    6. println!("{} 已经被从帅气男孩名单中除名", name); // 此处会报错,因为 `name` 的所有权已被转移
    • 使用引用类型放入 HashMap
    1. use std::collections::HashMap;
    2. let name = String::from("Sunface");
    3. let age = 18;
    4. let mut handsome_boys = HashMap::new();
    5. handsome_boys.insert(&name, age);
    6. std::mem::drop(name); // 此处会报错,因为 `name` 的生命周期结束
    7. println!("因为过于无耻,{:?} 已经被除名", handsome_boys);
    查询 HashMap
    • 使用 get 方法
    1. use std::collections::HashMap;
    2. let mut scores = HashMap::new();
    3. scores.insert(String::from("Blue"), 10);
    4. scores.insert(String::from("Yellow"), 50);
    5. let team_name = String::from("Blue");
    6. let score: Option<&i32> = scores.get(&team_name);
    • 使用 copiedunwrap_or 方法
     
    
    更新 HashMap
    • 插入新值或覆盖已有值
    1. use std::collections::HashMap;
    2. let mut scores = HashMap::new();
    3. scores.insert("Blue", 10);
    4. let old = scores.insert("Blue", 20);
    5. assert_eq!(old, Some(10));
    6. let v = scores.entry("Yellow").or_insert(5);
    7. assert_eq!(*v, 5);
    8. let v = scores.entry("Yellow").or_insert(50);
    9. assert_eq!(*v, 5);
    • 基于已有值进行更新
    1. use std::collections::HashMap;
    2. let text = "hello world wonderful world";
    3. let mut map = HashMap::new();
    4. for word in text.split_whitespace() {
    5. let count = map.entry(word).or_insert(0);
    6. *count += 1;
    7. }
    8. println!("{:?}", map);
    HashMap 的 Key 限制

    需要注意的是,f32 和 f64 并没有实现 Hash,原因是 浮点数精度 的问题会导致它们无法进行相等比较。

    如果一个集合类型的所有字段都实现了 Eq 和 Hash,那该集合类型会自动实现 Eq 和 Hash。例如 Vect 要实现 Hash,那么首先需要 T 实现 Hash

    • 任何实现了 Eq 和 Hash 特征的类型都可以用于 HashMap 的 key,包括:

    • bool (虽然很少用到,因为它只能表达两种 key)
    • intuint 以及它们的变体,例如 u8i32 等
    • String 和 &str (提示: HashMap 的 key 是 String 类型时,你其实可以使用 &str 配合 get 方法进行查询
    1. // 修复错误
    2. // 提示: `derive` 是实现一些常用特征的好办法
    3. use std::collections::HashMap;
    4. #[derive(Debug, Eq, Hash,PartialEq)]
    5. struct Viking {
    6. name: String,
    7. country: String,
    8. }
    9. impl Viking {
    10. fn new(name: &str, country: &str) -> Viking {
    11. Viking {
    12. name: name.to_string(),
    13. country: country.to_string(),
    14. }
    15. }
    16. }
    17. fn main() {
    18. // 使用 HashMap 来存储 viking 的生命值
    19. let vikings = HashMap::from([
    20. (Viking::new("Einar", "Norway"), 25),
    21. (Viking::new("Olaf", "Denmark"), 24),
    22. (Viking::new("Harald", "Iceland"), 12),
    23. ]);
    24. // 使用 derive 的方式来打印 viking 的当前状态
    25. for (viking, health) in &vikings {
    26. println!("{:?} has {} hp", viking, health);
    27. }
    28. }
    容量
    • 可以通过 HashMap::with_capacity(capacity) 来初始化指定容量的 HashMap,或者使用 HashMap::new() 进行默认初始化。
    1. use std::collections::HashMap;
    2. let mut map = HashMap::with_capacity(10); // 初始化指定容量
    3. map.insert("one", 1);
    4. map.insert("two", 2);
    使用第三方高性能哈希函数
    • 示例:使用 twox_hash::XxHash64
    1. use std::hash::BuildHasherDefault;
    2. use std::collections::HashMap;
    3. use twox_hash::XxHash64;
    4. let mut hash: HashMap<_, _, BuildHasherDefault<XxHash64>> = Default::default();
    5. hash.insert(42, "the answer");
    6. assert_eq!(hash.get(&42), Some(&"the answer"));

    生命周期概述

    生命周期(Lifetime)是 Rust 中一个重要且复杂的概念,主要用于确保引用的有效性,避免悬垂引用(Dangling References)。它描述了引用在程序中有效的范围。

    生命周期的作用
    1. 避免悬垂引用:确保引用在其指向的值被释放前有效。
    2. 保证内存安全:通过静态检查,确保引用在其有效期内不会被非法访问。

    生命周期语法和规则

    生命周期标注用 ' 开头,通常使用 'a'b 等单个字母表示。它们并不改变引用的实际生存周期,只是告诉编译器不同引用之间的关系。

    基本语法:

    1. &i32 // 一个引用
    2. &'a i32 // 具有显式生命周期的引用
    3. &'a mut i32 // 具有显式生命周期的可变引用

    标注生命周期的函数:

    1. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    2. if x.len() > y.len() {
    3. x
    4. } else {
    5. y
    6. }
    7. }

    上述函数中的生命周期 'a 表示 xy 和返回值的生命周期至少与 'a 一样长。

    借用检查(Borrow Checker)

    Rust 的借用检查器会在编译期确保引用是有效的。例如:

    1. {
    2. let r;
    3. {
    4. let x = 5;
    5. r = &x;
    6. }
    7. println!("r: {}", r); // 错误:`x` 不够长
    8. }

    编译器会报错,因为 x 在内部作用域结束时被释放,而 r 引用了一个无效的 x

    生命周期消除(Lifetime Elision)

    在某些情况下,编译器可以自动推导生命周期,不需要显式标注。例如:

    1. fn first_word(s: &str) -> &str {
    2. let bytes = s.as_bytes();
    3. for (i, &item) in bytes.iter().enumerate() {
    4. if item == b' ' {
    5. return &s[0..i];
    6. }
    7. }
    8. &s[..]
    9. }

    编译器自动推导出输入参数 s 和返回值具有相同的生命周期。

    生命周期消除规则
    1. 每个引用参数都有一个独立的生命周期
    2. 如果只有一个输入生命周期,返回值的生命周期等于这个输入生命周期
    3. 如果有多个输入生命周期,且其中一个是 &self&mut self,则 self 的生命周期赋给所有输出生命周期
    fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
    

    但是此时,第二条规则却无法被使用,因为输入生命周期有两个,第三条规则也不符合,因为它是函数,不是方法,因此没有 &self 参数。在套用所有规则后,编译器依然无法为返回值标注合适的生命周期,因此,编译器就会报错,提示我们需要手动标注生命周期: 

    1. fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    2. x
    3. }

    结构体中的生命周期

    结构体中的引用字段也需要标注生命周期,以确保引用在结构体的生命周期内有效。例如:

    1. #[derive(Debug)]
    2. struct NoCopyType {}
    3. #[derive(Debug)]
    4. #[allow(dead_code)]
    5. struct Example<'a, 'b> {
    6. a: &'a u32,
    7. b: &'b NoCopyType
    8. }
    9. /* 修复函数的签名 */
    10. fn fix_me<'a, 'b>(foo: &'a Example<'a, 'b>) -> &'b NoCopyType {
    11. foo.b
    12. }
    13. fn main()
    14. {
    15. let no_copy = NoCopyType {};
    16. let example = Example { a: &1, b: &no_copy };
    17. fix_me(&example);
    18. println!("Success!")
    19. }

    静态生命周期

    'static 生命周期是一个特殊的生命周期,表示引用在整个程序运行期间有效。它常用于字符串字面量:

    let s: &'static str = "I have a static lifetime.";
    

    复杂示例:泛型和特征约束结合生命周期

    1. use std::fmt::Display;
    2. fn longest_with_an_announcement<'a, T>(
    3. x: &'a str,
    4. y: &'a str,
    5. ann: T,
    6. ) -> &'a str
    7. where
    8. T: Display,
    9. {
    10. println!("Announcement! {}", ann);
    11. if x.len() > y.len() {
    12. x
    13. } else {
    14. y
    15. }
    16. }

    在这个例子中,函数 longest_with_an_announcement 接受两个字符串切片和一个实现了 Display 特征的参数,返回具有与输入字符串相同生命周期的较长字符串。

    Rust 中的错误处理 可恢复的错误

    在处理文件读写等操作时,不仅需要处理不可恢复的错误,还需要处理可恢复的错误。Rust 提供了 Result 枚举来实现这种错误处理机制。

    Result 概述

    Result 是一个包含两种可能结果的枚举:

    • Ok(T) 表示操作成功,并包含成功时的返回值 T
    • Err(E) 表示操作失败,并包含错误信息 E

    定义如下:

    1. enum Result {
    2. Ok(T),
    3. Err(E),
    4. }

    文件操作示例

    以下是打开文件的基本示例:

    1. use std::fs::File;
    2. fn main() {
    3. let f = File::open("hello.txt");
    4. }

    获取变量类型或函数返回类型

    可以通过以下方法获取变量或函数的返回类型:

    1. 查询文档:查看标准库或第三方库文档。
    2. 使用 IDE:使用 VSCode 和 rust-analyzer 插件,可以方便地查看代码中的类型。
    3. 故意制造编译错误:例如,故意标记错误的类型,然后查看编译器的错误提示。
    let f: u32 = File::open("hello.txt");
    

    编译器会提示实际返回类型为 Result

    处理 Result

    可以使用 match 语句对 Result 进行模式匹配:

    1. use std::fs::File;
    2. fn main() {
    3. let f = File::open("hello.txt");
    4. let f = match f {
    5. Ok(file) => file,
    6. Err(error) => panic!("Problem opening the file: {:?}", error),
    7. };
    8. }

    这种处理方法虽然有效,但在遇到不同类型的错误时,可能需要进行更详细的处理。

    更详细的错误处理

    可以对不同的错误类型进行分类处理:

    1. use std::fs::File;
    2. use std::io::ErrorKind;
    3. fn main() {
    4. let f = File::open("hello.txt");
    5. let f = match f {
    6. Ok(file) => file,
    7. Err(error) => match error.kind() {
    8. ErrorKind::NotFound => match File::create("hello.txt") {
    9. Ok(fc) => fc,
    10. Err(e) => panic!("Problem creating the file: {:?}", e),
    11. },
    12. other_error => panic!("Problem opening the file: {:?}", other_error),
    13. },
    14. };
    15. }

    使用 unwrapexpect

    在不需要处理错误的场景下,可以使用 unwrapexpect 来简化代码:

    1. use std::fs::File;
    2. fn main() {
    3. let f = File::open("hello.txt").unwrap();
    4. }
    5. fn main() {
    6. let f = File::open("hello.txt").expect("Failed to open hello.txt");
    7. }

    错误传播

    可以通过返回 Result 类型的函数来传播错误:

    1. use std::fs::File;
    2. use std::io::{self, Read};
    3. fn read_username_from_file() -> Result<String, io::Error> {
    4. let f = File::open("hello.txt");
    5. let mut f = match f {
    6. Ok(file) => file,
    7. Err(e) => return Err(e),
    8. };
    9. let mut s = String::new();
    10. match f.read_to_string(&mut s) {
    11. Ok(_) => Ok(s),
    12. Err(e) => Err(e),
    13. }
    14. }

    使用 ? 简化错误传播

    ? 还能实现链式调用,File::open 遇到错误就返回,没有错误就将 Ok 中的值取出来用于下一个方法调用)

    1. use std::fs::File;
    2. use std::io;
    3. use std::io::Read;
    4. fn read_username_from_file() -> Result<String, io::Error> {
    5. let mut s = String::new();
    6. File::open("hello.txt")?.read_to_string(&mut s)?;
    7. Ok(s)
    8. }

    Option 的使用

    ? 也可以用于 Option 类型:

    1. fn first(arr: &[i32]) -> Option<&i32> {
    2. let v = arr.get(0)?;
    3. Some(v)
    4. }
    5. fn last_char_of_first_line(text: &str) -> Option<char> {
    6. text.lines().next()?.chars().last()
    7. }

    带返回值的 main 函数

    可以使 main 函数返回 Result 类型,从而使用 ? 进行错误处理:

    1. use std::error::Error;
    2. use std::fs::File;
    3. fn main() -> Result<(), Box<dyn Error>> {
    4. let f = File::open("hello.txt")?;
    5. Ok(())
    6. }

    在 Rust 中,mapand_then 是两个重要的组合器(combinator),常用于处理 ResultOption 类型的值。这两个方法在函数式编程风格中非常常见,可以简化代码,减少重复的错误处理逻辑。下面我们详细解释这两个方法,并总结它们的核心概念。

    map 方法

    map 方法用于对 ResultOption 类型中的成功值(即 OkSome)进行转换,而不改变错误值(即 ErrNone)。

    用法

    1. result.map(|value| {
    2. // 对成功值进行操作
    3. })

    示例

    1. let result: Result<i32, ParseIntError> = "42".parse();
    2. let new_result = result.map(|num| num + 2);

    在上面的例子中,如果 resultOk(42),那么 new_result 将是 Ok(44);如果 resultErr(e),那么 new_result 仍然是 Err(e)

    and_then 方法

    and_then 方法用于链式调用多个可能失败的操作。它接收一个返回 ResultOption 的闭包,并将当前成功值传递给这个闭包。

    用法

    1. result.and_then(|value| {
    2. // 返回新的 Result 或 Option
    3. })

    示例

    1. let result: Result<i32, ParseIntError> = "42".parse();
    2. let new_result = result.and_then(|num| {
    3. // 假设这个函数也返回 Result
    4. another_function(num)
    5. });

    在上面的例子中,如果 resultOk(42),则 num 会被传递给 another_function,并返回新的 Result;如果 resultErr(e),则 new_result 仍然是 Err(e)。 

    重写示例代码

    让我们用 mapand_then 重写之前的 multiply 函数,以使其更简洁。

    1. use std::num::ParseIntError;
    2. // 使用 Result 重写后,我们使用模式匹配的方式来处理,而无需使用 `unwrap`
    3. // 但是这种写法实在过于啰嗦..
    4. fn multiply(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> {
    5. match n1_str.parse::<i32>() {
    6. Ok(n1) => {
    7. match n2_str.parse::<i32>() {
    8. Ok(n2) => {
    9. Ok(n1 * n2)
    10. },
    11. Err(e) => Err(e),
    12. }
    13. },
    14. Err(e) => Err(e),
    15. }
    16. }
    17. // 重写上面的 `multiply` ,让它尽量简洁
    18. // 提示:使用 `and_then` 和 `map`
    19. fn multiply1(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> {
    20. n1_str.parse::<i32>().and_then(|n1| {
    21. n2_str.parse::<i32>().map(|n2| n1 * n2)
    22. })
    23. }
    24. fn print(result: Result<i32, ParseIntError>) {
    25. match result {
    26. Ok(n) => println!("n is {}", n),
    27. Err(e) => println!("Error: {}", e),
    28. }
    29. }
    30. fn main() {
    31. let twenty = multiply1("10", "2");
    32. print(twenty);
    33. // 下面的调用会提供更有帮助的错误信息
    34. let tt = multiply("t", "2");
    35. print(tt);
    36. println!("Success!")
    37. }

    类型别名在 Rust 中是一种简化代码、提高可读性和可维护性的有效方式。它可以减少代码中的冗长和重复,使代码更加清晰。

    类型别名的定义与使用

    定义类型别名

    类型别名使用 type 关键字来定义。例如,如果你在代码中多次使用 Result,可以将其定义为一个类型别名:

    1. use std::num::ParseIntError;
    2. type Res<T> = Result<T, ParseIntError>;

    使用类型别名

    一旦定义了类型别名,就可以在代码中使用它来替代原来的长类型名:

    1. fn multiply(first_number_str: &str, second_number_str: &str) -> Res<i32> {
    2. first_number_str.parse::<i32>().and_then(|first_number| {
    3. second_number_str.parse::<i32>().map(|second_number| first_number * second_number)
    4. })
    5. }

    高级 

    函数式编程
    闭包捕获作用域中的值
    • 闭包特性:闭包可以捕获并使用定义时作用域中的变量,而函数则不能。
    • 使用场景:在需要访问定义时作用域内的变量时,闭包比函数更灵活。
    • 编译器提示:Rust 编译器会友好地提示将函数替换为闭包,以便捕获动态环境中的值。
    闭包对内存的影响

    闭包的内存分配

    • 捕获环境变量:闭包会捕获其定义时环境中的变量。
    • 内存分配:捕获的变量需要额外的内存来存储。
    • 性能影响:在某些场景中,这种内存分配可能成为一种负担。

    函数的内存管理

    • 不捕获环境变量:函数不会捕获其定义时环境中的变量。
    • 无额外内存分配:因此,函数不会为环境变量分配额外的内存。
    • 性能优势:避免了因内存分配带来的负担。
    Fnonce转移所有权易错
    • move 关键字move 关键字将变量的所有权从其原始作用域移动到闭包的作用域中。
    • 捕获行为:对于 Copy 类型(如 i32),闭包捕获的是变量的一个副本,而不是引用。
    • 副本的生命周期:闭包持有的副本在整个闭包生命周期内是唯一的,不会每次调用闭包时重新复制原始值。
    • 闭包内的修改:每次调用闭包时,闭包内部的 count 副本都会递增,而外部的 count 保持不变。
    示例代码:
    1. fn main() {
    2. let mut count = 0;
    3. // 闭包 `inc` 使用 `move` 捕获了 `count` 的所有权
    4. // 由于 `count` 是一个 `i32` 类型,实现了 `Copy` trait,
    5. // 因此捕获的是 `count` 的一个副本,但这个副本在闭包的整个生命周期内是唯一的
    6. let mut inc = move || {
    7. count += 1; // 修改的是闭包内的 `count` 副本,每次调用闭包时,这个副本都会递增
    8. println!("`count`: {}", count); // 打印副本的值
    9. };
    10. inc(); // 调用闭包,闭包内的 `count` 变为 1
    11. // 尝试不可变借用原始的 `count`
    12. // 因为闭包捕获的是副本,原始的 `count` 仍然存在且未被借用
    13. let _reborrow = &count;
    14. inc(); // 再次调用闭包,闭包内的 `count` 副本变为 2
    15. // 尝试可变借用原始的 `count`
    16. // 由于闭包捕获的是副本,原始的 `count` 仍然存在且未被借用
    17. let _count_reborrowed = &mut count;
    18. // 断言外部的 `count` 仍然为 0
    19. // 因为闭包内部修改的是副本,外部的 `count` 未被修改
    20. assert_eq!(count, 0);
    21. }

  • 相关阅读:
    【网络安全 --- 工具安装】VMware 16.0 详细安装过程(提供资源)
    机械搬运手结构设计
    猫头虎博主的AI魔法课:一起探索CSDN AI工具集的奥秘!
    数据库系统及应用复习——第七章数据库设计
    基于VTD自带的场景 进行场景搭建
    【坑货IDEA】Spring项目运行中的问题
    数据结构——栈和队列
    qt——窗口置灰不可操作
    Linux系统安装Anaconda
    网络安全(黑客)自学
  • 原文地址:https://blog.csdn.net/2301_79140115/article/details/140232697