Rust

0x01 准备

(1)安装 Rust

  • 安装 Rust:
    • Windows 系统在官网下载 Rust
    • 版本检测:rustc --version
  • 查看文档:rustup doc
  • 更新 Rust:rustup update
  • 卸载 Rust:rustup self uninstall

在 Clion 中使用 Rust 开发,环境配置参考《在Windows上搭建Rust开发环境——Clion篇 | CSDN-swanmy

(2)Hello, World!

  1. 编写

    // filename: main.rs
    fn main() {
        println!("Hello, world!");
    }
    
  2. 编译:rustc main.rs

  3. 运行:./main.exe

  • 分析上述程序:
    • fn main(){}是主函数,是每个 Rust 可执行程序最先运行的代码
    • Rust 的缩进是 4 个空格
    • println!是一个 Rust macro[^宏],如果是函数时就没有!
    • 代码行以;结尾

(3)Cargo

  • Cargo 是 Rust 的构建系统和包管理工具

    • 创建项目:cargo new [projectname]

    • 项目结构:

      graph TB A(Project)-->src -->main.rs A-->Cargo.toml A-->.gitignore
      • Cargo.toml
        • TOML(Tom's Obvious Minimal Language)格式是 Cargo 的配置格式
        • [package]是一个区域标题,表示下方内容是用来配置包的
          • name:项目名
          • version:项目版本
          • authors:项目作者
          • edition:使用 Rust 的版本
        • [dependencies]表示另一个区域的开始,会列出项目的依赖项
        • 代码的包称作 crate
      • src
        • 保存全部源代码
        • 将所有代码移到 src 目录下,创建 Cargo.toml 文件并填写配置信息,从而将项目转换为 Cargo 项目
    • 构建项目:cargo build

      • 生成可执行文件:target\debug[projectname].exe
      • 生成 cargo.lock
        • 用于追踪项目依赖的精确版本
    • 运行项目:cargo run

    • 编译检查:cargo check

    • 发布版本:cargo build --release

0x02 猜数游戏

  • Cargo.toml

    [package]
    name = "Project"
    version = "0.1.0"
    edition = "2021"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [dependencies]
    rand = "0.8.5"
    
  • src/main.rs

    use std::io;
    use rand::Rng;
    use std::cmp::Ordering;
    
    fn main() {
        println!("猜数游戏");
        // 生成随机数
        let random_number = rand::thread_rng().gen_range(1..101);
        loop {
            println!("猜测一个数:");
            let mut guess = String::new();
            // 输入字符串
            io::stdin().read_line(&mut guess).expect("无法读取");
            // 字符串转数字及异常处理
            let guess: u32 = match guess.trim().parse() {
                Ok(num) => num,
                Err(_) => { println!("输入错误"); continue; },
            };
            println!("猜测了 {}", guess);
    
            // 比较
            match guess.cmp(&random_number) {
                Ordering::Less => println!("small"),
                Ordering::Greater => println!("BIG"),
                Ordering::Equal => { println!("Win"); break; },
            }
        }
    }
    

0x03 基本概念

(1)变量与可变性

  • 声明变量使用let关键字

    • 默认情况下,变量不可变
    • 声明变量时,在变量前面加上mut使得变量可变
  • 常量(constant)在绑定值后不可变

    • 不可以使用mut修饰

    • 声明常量使用const,其类型编写被标注

    • 常量可以在任何作用域内进行声明,包括全局作用域

    • 常量只可以绑定到常量表达式

    • 在程序运行期间,常量在其声明的作用域内永久有效

    • 常量命名规范:全大写,单词间下划线分隔

      const MAX_SIZE:u32 = 100_000;
      
  • 隐藏(shadowing)

    • 可以使用相同的名字声明新的变量,新的变量就会隐藏之前声明的同名变量

      • 报错写法

        let x = 1;
        x = x + 1;
        
      • 使用隐藏

        let x = 1;
        let x = x + 1;
        
    • mut的区别

      • 如果不使用let关键字那么重新给非mut的变量赋值会导致编译时错误

      • 使用let声明的同名新变量依然不可变

      • 使用let声明的同名新变量可能类型不同

        fn main() {
            let str = "abcde";
            println!("{}", str);	// output: abcde
            let str = str.len();
            println!("{}", str);	// output: 5
        }
        

(2)数据类型

  • 分为标量、复合类型

  • Rust 是静态编译语言,在编译时必须知道所有变量的类型

    • 基于使用的值,编译器通常能推断出它的具体类型。但当可能的类型比较多时,就必须添加类型标注

      • 报错写法

        fn main() {
            let guess = "12345".parse().expect("Error");
            println!("{}", guess);
        }
        
      • 正确写法

        fn main() {
            let guess:u32 = "12345".parse().expect("Error");
            println!("{}", guess);
        }
        

a. 标量类型

  • 一个标量类型代表一个单个的值

  • Rust 有四个主要的标量类型

    • 整数类型

      • 没有小数部分

      • u32代表占据 32 位空间的符号整数

      • i32代表占据 32 位空间的符号整数

      • isizeusize

        • 以上类型的位数由程序运行的计算机架构所决定
        • 一般在对某种集合进行索引操作时使用
      • 整数字面值

        • 除了byte类型外,所有数值字面值都允许使用类型后缀,如1234u8,默认后缀为i32
        Number Literals Example
        Decimal 123_456
        Hex 0xdef
        Octal 0o777
        Binary 0b1100_1000
        Byte (u8 only) b'A'
      • 整数溢出

        • Debug 时,会 panic
        • Release 时,不会 panic,会将数值递归减去范围最大值,直至结果在范围内
    • 浮点类型

      • f32,32 位,单精度
      • f64,64 位,双精度
      • 使用了 IEEE-754 标准描述
      • f64为默认类型
    • 布尔类型

      • 符号为bool
      • 取值分别为truefalse
      • 大小为 1 byte
    • 字符类型

      • 符号为char
      • 大小为 4 byte、
      • 字面值使用单引号
      • 编码方式为 Unicode

b. 复合类型

  • 复合类型可以将多个值放在一个类型里

  • Rust 有两种基础的复合类型

    • 元组(Tuple)

      • 可以将多个类型的多个值放在一个类型里

      • 一旦声明就无法改变元组的长度

      • 创建 Tuple

        fn main() {
            let tup:(i32, f64, u8) = (32, 6.4, 1);
        }
        
      • 获取 Tuple 的元素值

        fn main() {
            let tup:(i32, f64, u8) = (32, 6.4, 1);
            let (x, y, z) = tup;
            println!("{}, {}, {}", x, y, z);
        }
        
      • 访问 Tuple 的元素

        fn main() {
            let tup:(i32, f64, u8) = (32, 6.4, 1);
            println!("{}, {}, {}", tup.0, tup.1, tup.2);
        }
        
    • 数组

      • 其中每个元素类型相同

      • 长度固定

        • 数组类型:[类型; 长度],如[i32; 5]
      • 声明数组

        • 方法一

          fn main () {
              let a = [1, 2, 3, 4, 5];
          }
          
        • 方法二:[初始值; 数组长度]

          fn main () {
              let a = [3; 5];
          }
          
      • 对比

        • 优点
          • 将数据存放在 stack 而非 heap上
          • 保证固定数量的元素
        • 缺点
          • 不如 Vector 灵活
      • 访问元素

        • 数组是 Stack 上分配的单个块的内存

        • 可以使用索引来访问数组元素

          fn main () {
              let a = [3; 5];
              println!("{}", a[3]);	// output: 3
          }
          
        • 索引超出范围时,编译通过,而运行时 panic

(3)函数与注释

a. 函数

  • 使用fn关键字声明函数

  • 命名采用 snake case 方式(全部小写、下划线分隔)

  • 参数

    • parameters/arguments:形参/实参

    • 在函数签名里必须声明每个参数的类型

      fn main () {
          function(3, 4);
      }
      
      fn function(x: i32, y: i32) {
          println!("x={}", x);
          println!("y={}", y);
      }
      
  • 函数体中的语句与表达式

    • 函数体由一系列语句组成,可选的由一个表达式结束
    • Rust 是基于表达式的语言
    • 语句是执行一些动作的指令
    • 表达式会计算产生一个值
    • 函数的定义也是语句
    • 语句不返回值,因此不可使用let将一个语句赋给一个变量
    fn main () {
        let y = {
            let x = 3;
            x + 1
        };
        println!("{}", y);
    }
    
  • 函数的返回值

    • ->符号后声明函数的返回值类型,但不可以为返回值命名
    • Rust 中返回值就是函数体中最后一个表达式的值
    • 若想提取返回,则需使用return关键字,并指定一个返回值
    fn main () {
        let x = 3;
        let y = function(x);
        println!("{}", y);
    }
    
    fn function (x: i32) -> i32 {
        return 10;
    }
    

b. 注释

  • 单行注释:// [content]
  • 多行注释:/* content */

(4)控制流

a. 逻辑判断

  • if表达式

    • 根据bool类型的条件来执行不同的代码分支
    • 与条件相关联的代码块称作分支(arm)
    • 可选添加else
    fn main () {
        let x = 3;
        if x > 10 {
            println!("yes");
        } else { 
            println!("no");
        }
        println!("{}", x);
    }
    
  • 使用else if处理多重条件

    • 当使用了多个else if时,最好使用match重构代码
  • let语句中使用if

    fn main () {
        let mut x = 3;
        let y = if x > 3 {x = 4} else {x = 5};
        println!("{}", x);		// output: 5
    }
    

b. 循环

  1. loop循环

    • 默认无条件循环执行其中代码块
    • 可以使用break终止循环
    fn main () {
        let mut counter = 0;
        let result = loop {
            counter += 1;
            if counter == 10 {
                break counter;
            }
        };
        println!("Result: {}", result);	// output: Result: 10
    }
    
  2. while循环

    • 对某个条件进行判断后决定是否执行循环
    fn main () {
        let mut number = 10;
        while number != 0 {
            println!("{}", number);
            number -= 1;
        }
        println!("{}", number);	// output: 0
    }
    
  3. for循环

    • 主要用于安全、简洁遍历集合
    fn main () {
        let arr = [1, 2, 3, 4, 5];
        for element in arr.iter() {
            println!("{}", element);
        }
    }
    
    • Range

      • 该关键字由标准库提供
      • 在 Range 中指定开始数字b和结束数字e,可生成[b, e)之间的数字
      • 使用.rev()方法可以翻转 Range
      fn main () {
          for element in (1..4).rev() {
              println!("{}", element);
          }
      }
      

0x04 所有权

(1)概述

  • Rust 的核心特性就是所有权
  • 所有程序运行期间都必须管理其使用计算机内存的方式
  • 所有权解决的问题
    • 跟踪代码使用堆中数据情况
    • 最小化堆中数据重复量
    • 清理堆中数据以避免空间不足

a. 栈内存与堆内存

  • 存储数据
    • 栈内存按值接收的顺序来存储,按相反的顺序来读出(后进先出,LastInFirstOut)
    • 栈内存中的数据存取操作名称:压入栈(存储数据)、弹出栈(读出数据)
    • 所有在栈中存储的数据必须拥有已知的固定大小,否则存入堆中
      • 操作系统会在堆中找到一块足够大的空间并返回一个指针,这个过程称为分配
  • 访问数据
    • 在堆中访问数据需要通过指针,比栈慢
  • 函数调用
    • 当调用函数时,函数本地的变量被压到栈上,当函数结束后,这些值就会从栈上弹出

b. 所有权规则

  • 每个值都有一个变量,这个变量是该值的所有者
  • 每个值有且仅有一个所有者
  • 当所有者超过作用域(Scope)时将会被删除
    • 变量作用域就是程序中一个项目的有效范围

为演示所有权相关规则,需要提取使用 String 类型数据

  • 举例演示

    • 创建 String 类型的值:let s = String::from("Hello");

      • ::表示from是 String 类型下的函数
    • 字符串插入

      fn main () {
          let mut s = String::from("Hello");
          s.push_str(", world!");
          println!("{}", s);
      }
      

c. 所有权与函数

  • 在语义上,将值传递给函数和把值赋给变量是类似的

    • 将值传递给函数将发生移动或复制
  • 返回值与作用域

    • 函数在返回值的过程中也发生所有权的转移
    • 一个变量的所有权当把一个值赋给其他变量是就会发生移动,当一个包含堆数据的变量离开作用域时,除非数据的所有权被移动到另一个变量上,否则其值就会被 drop 函数清理
  • 如何让函数使用值但不获取其所有权:

    • 将参数传入后再传出以间接保障所有权

      fn main() {
          let s1 = String::from("Hello");
          let (s2, len) = function(s1);
          println!("The length of {} is {}.", s2, len,)	// The length of Hello is 5.
      }
      
      fn function(s: String) -> (String, usize) {
          let length = s.len();
          (s, length)
      }
      
    • 由于上述方法过于麻烦,故而可使用引用来实现

(2)引用与借用

a. 引用

  • &符号标识引用:允许引用其值而不获取其所有权

    fn main() {
        let s1 = String::from("Hello");
        let len = function(&s1);
        println!("The length of {} is {}.", s1, len,)
    }
    
    fn function(s: &String) -> usize {
        s.len()
    }
    
  • 默认引用不可变

b. 借用

  • 把引用作为函数参数的行为叫做借用
  • 借用的东西不可更改

c. 可变引用

  • 可变引用:通过添加mut实现

    fn main() {
        let mut s1 = String::from("Hello");
        let len = function(&mut s1);
        println!("The length of {} is {}.", s1, len,)
    }
    
    fn function(s: &mut String) -> usize {
        s.push_str(", world");
        s.len()
    }
    
  • 重要限制一:在特定作用域内,对某一块数据只能有一个可变引用

    • 防止编译时产生数据竞争

    数据竞争发生原因:

    • 两个或多个指针同时访问同一个数据
    • 至少有一个指针用于写入数据
    • 没有使用任何机制来同步对数据的访问
    • 可以创建新的作用域来允许非同时的创建多个可变引用

      fn main() {
          let mut s = String::from("Hello");
          {
              let s1 = &mut s;
          }
          let s2 = &mut s;
      }
      
  • 重要限制二:禁止同时拥有一个可变引用和一个不变引用

    • 可以拥有多个不变引用

d. 引用的作用域

  • 从声明到最后一次使用

e. 悬空引用

  • 悬空指针:一个指针引用了内存中的某个地址,而这块内存可能已经释放并被重新分配掉
  • 在 Rust 中,编译器可保证引用永远都不是悬空引用。如果引用了某些数据,则编译器将保证在引用离开作用域之前数据不会离开作用域

(3)切片

  • 切片:一种不持有所有权的数据类型

  • 举例:编写一个函数

    • 该函数接受字符串作为参数
    • 返回在这个字符串里找到的第一个单词
    • 当未找到任何空格则返回整个字符串
    fn main() {
        let mut s = String::from("hello world");
        let idx = function(&s);
        println!("{}", idx);
    }
    
    fn function(s: &String) -> usize {
        let bytes = s.as_bytes();
        for (i, &item) in bytes.iter().enumerate() {
            if item == b' ' {
                return i;
            }
        }
        s.len()
    }
    
    • 上述代码存在问题:当在输出前执行s.clear()时,idx的意义就不存在了。为此提出字符串切片
  • 字符串切片

    • 是指向字符串中一部分的引用

    • 形式:[起始索引 .. 终止索引]

      fn main() {
          let s = String::from("hello world");
          let s1 = &s[0 .. 5];
          let s2 = &s[6 .. 11];
          println!("{}, {}", s1, s2);
      }
      
    • 注意:

      • 字符串切片的范围索引必须发生在有效的 UTF-8 字符边界内

      • 当尝试从一个多字节的字符中创建字符串切片时,程序会报错退出

      • 重写上述例子:

        fn main() {
            let mut s = String::from("hello world");
            let s1 = function(&s);
            println!("{}", s1);
        }
        
        fn function(s: &String) -> &str {
            let bytes = s.as_bytes();
            for (i, &item) in bytes.iter().enumerate() {
                if item == b' ' {
                    return &s[..i];
                }
            }
            &s[..]
        }
        
  • 字符串字面值是切片,其被直接存储在二进制程序中

  • 将字符串切片作为参数传递:fn function(s: &str) -> &str {}

    • 此时可以同时接受切片转换成切片的字符串两种类型的数据
    • 定义函数时使用字符串切片来代替字符串引用会使 API 更加通用,且不会损失任何功能
    fn main() {
        let s1 = String::from("hello world");
        let s1_1 = function(&s[..]);
        let s2 = "hello world";
        let s2_2 = function(s2);
    }
    
    fn function(s: &str) -> &str {
        &s[..]
    }
    

0x05 Struct

struct,结构体

  • 自定义的数据类型
  • 为相关联的值命名与打包

(1)定义并实例化 struct

a. 定义 struct

  • 使用 struct 关键字,并为整个 struct 命名

  • 在花括号内,为所有字段定义名称和类型

    struct Student {
        name: String,
        age: u8,
        active: bool,
    }
    

b. 实例化 struct

  • 需要创建 struct 实例

    • 为每个字段指定具体值
    • 无需按声明顺序进行指定
    let mut s = Student {
        name: String::from("SRIGT"),
        age: 22,
        active: true,
    };
    
  • 获取 struct 中的某个值

    • 使用点标记法
    s.name = String::from("SRIGT");
    

一旦 struct 的实例是可变的,那么实例中所有的字段都是可变的

  • struct 作为函数返回值

    fn create_student(name: String) -> Student {
        Student {
            name: name,
            age: 0,
            active: true,
        }
    }
    
    fn main () {
        let mut s = create_student("SRIGT".to_string());
        println!("{}", s.name);
    }
    
    • 字段初始化简写

      • 当字段名与字段值对应变量名相同时,就可以使用以下简写方法
      fn create_student(name: String, age: u8) -> Student {
          Student {
              name,
              age,
              active: true,
          }
      }
      
      fn main () {
          let mut s = create_student("SRIGT".to_string(), 18);
          println!("{}", s.name);
      }
      
  • struct 更新语法

    • 当需要基于已有的某个 struct 来创建另一个 struct 时使用以下更新方法
    let mut s1 = Student {
        name: String::from("SRIGT"),
        age: 18,
        active: true,
    };
    let mut s2 = Student {
        name: String::from("白月"),
        age: s1.age,
        active: false,
    };
    
    • 简写

      let mut s2 = Student {
          name: String::from("白月"),
          ..s1
      };
      
  • Tuple struct

    • 可以定义类似 tuple 的 struct,其整体有名称,里面的元素没有
    struct Color(i32, i32, i32);
    let black = Color(0, 0, 0);
    
  • Unit-Like Struct

    • 可以定义没有任何字段的 struct
    • 适用于需要某个类型上实现某个 trait,但在里面又没有需要存储的数据时
  • struct 数据所有权

    • 对于struct Student { name: String };,这里的字段使用了 String 而不是&str
      • 该 struct 实例了其拥有的所有数据
      • 只要 struct 实例是有效的,那么里面的字段也是有效的
    • struct 里也可以存放引用,但需要使用生命周期
      • 生命周期中保证只要 struct 实例时有效的,那么里面的引用也是有效的
        • 如果 struct 里面存储引用,而不使用生命周期,就会报错
    #[derive(Debug)]
    struct Node {
        val: i32,
    }
    
    fn main () {
        let node = Node { val: 20};
        println!("{:?}", node);
    }
    

(2)struct 方法

  • 方法与函数类似:fn关键字、名称、参数、返回值

  • 方法与函数区别

    • 方法在 struct(或 enum、trait 对象)的上下文中定义
    • 第一个参数是self,表示方法被调用的 struct 实例

a. 定义方法

  • impl块里定义方法
  • 方法的第一个参数可以是&self,也可以是获得其所有权或可变借用(&mut self
  • 更良好的代码组织
struct node {
    number1: u32,
    number2: u32,
}

impl node {
    fn sum(&self) -> u32 {
        self.number1 + self.number2
    }
}

fn main() {
    let node = node {
        number1: 1,
        number2: 2,
    };
    println!("sum(1, 2) = {}", node.sum());
}
  • 方法调用的运算符

    • Rust 会自动引用或解引用(在调用方法时会发生这种行为)
    • 在调用方法时,Rust 根据情况自动添加&&mut*,以便object可以平铺方法的签名
  • 每个 struct 允许拥有多个impl

b. 关联函数

  • 可以在impl块里定义不把self作为第一个参数的函数,如:String::from()

  • 关联函数通常用于构造器

    • ::符号一般用于关联函数与模块创建的命名空间
    struct node {
        number1: u32,
        number2: u32,
    }
    
    impl node {
        fn equal(number: u32) -> node {
            node {
                number1: number,
                number2: number,
            }
        }
    }
    
    fn main() {
        let node = node::equal(1);
    }
    

0x06 枚举与模式匹配

(1)定义枚举

  • 枚举允许列举所有可能的值来定义一个类型

    enum Kind {
        A,
        B,
    }
    
    fn main() {
        let a = Kind::A;
        let b = Kind::B;
        function(a);
        function(b);
        println!("");
    }
    
    fn function(kind: Kind) {
        match kind {
            Kind::A => println!("A"),
            Kind::B => println!("B"),
        }
    }
    
  • 枚举的变体都位于标识符的命名空间下,使用::进行分隔

    • enum 的变体若附带有值,那么就会在函数的命名空间下生成一个函数

    • 将数据附加到枚举的变体中

      • 不需要额外使用 struct
      • 每个变体可以拥有不同的类型以及关联的数据量
      enum Kind {
          A(String),
          B(u32)
      }
      
      fn main() {
          let a = Kind::A("abc".to_string());
          let b = Kind::B(100);
      }
      
  • 为枚举定义方法

    • 使用empl关键字

(2)Option 枚举

  • 定义于标准库中

  • 在 Prelude(预导入模块)中

  • 描述了某个值可能存在或不存在的情况

  • Rust 中类似 Null 概念的枚举——Option<T>

    • 在标准库中定义为:

      enum Option<T> {
          Some(T),
          None,
      }
      
    • 避免将Option<T>直接当成 T,若想使用Option<T>中的 T,则必须将它转换为 T

(3)控制流运算符 match

  • 允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码

    • 模式可以为字面值、变量名、通配符……
    enum Kind {
        A,
        B
    }
    
    fn change(kind: Kind) -> String {
        match kind {
            Kind::A => String::from("a"),
            Kind::B => String::from("b")
        }
    }
    
    fn main() {}
    
  • 绑定值的模式

    • 匹配的分支可以绑定到任意被匹配对象的部分值
    #[derive(Debug)]
    
    enum Kind2 {
        A,
        B
    }
    
    enum Kind {
        A,
        B(Kind2)
    }
    
    fn change(kind: Kind) -> u8 {
        match kind {
            Kind::A => 1,
            Kind::B(kind2) => {
                println!("{:?}", kind2);
                2
            }
        }
    }
    
    fn main() {
        let b = Kind::B(Kind2::A);
        println!("{}", change(b));
    }
    
  • 匹配Option<T>

    fn main() {
        let one = Some(1);
        let two = Some(one);
        let three = Some(None);
    }
    
    fn function(x: Option<i32>) -> Option<i32> {
        match x {
            Some(x) => Some(x + 1),
            None => None,
        }
    }
    
  • match匹配必须穷举所有的可能

    • 或使用_通配符替代未列出的值

(4)if let

  • 处理只关心一种匹配而忽略其他匹配情况
  • 更少的代码与缩进
  • 放弃了穷举的可能
  • 可看作是match的语法糖
fn main() {
    let x = Some(0u8);
    if let Some(0) = x {
        println!("0")
    }
}

0x07 Package, Crate, Module

(1)Rust 的代码组织

  • 可以暴露或私有的细节
  • 作用域内有效的名称

(2)模块系统

  • Package(包):Cargo 的特性,用于构建、测试、共享 crate

    • 包含一个 Cargo.toml,描述了构建 Crates 的方法
    • 只能包含 0 或 1 个 library crate
    • 可以包含人员数量的 binary crate
    • 但必须至少包含一个 crate
  • Crate(单元包):一个模块树,可产生一个 library 或可执行文件

    • 类型:binary 或 library

    • Crate Root:

      • 是源代码文件,是 Rust 编译器的入口

      Cargo 项目默认设置:

      • src/main.rs:
        • binary crate 的 crate root
        • crate 名与 package 名相同
      • src/lib.rs:
        • package 包含一个 library crate
        • library crate 的 crate root
        • crate 名与 package 名相同
      • Cargo 将 crate root 文件交给 rustc 来构建 library 或 binary
    • Crate 的作用

      • 将相关功能组合到一个作用域内,便于在项目间进行共享
        • 如 rand crate,访问其功能时需要通过其名字 rand
  • Module(模块):利于控制代码的组织、作用域、私有路径

    • 在一个 crate 内将代码进行分组

    • 增加可读性,易于复用

    • 控制项目的私有性(私有边界 privacy boundary)

      • 模块不仅可以组织代码,还可以定义私有边界
      • 如果想把函数或 struct 设为私有,则可将其放入某个模块中
      • Rust 中的所有条目默认私有
      • 父级模块无法访问子模块的私有条目
      • 子模块可以使用所有祖先模块的条目
      • 使用pub关键字可以将某些条目标记为公共的
    • 建立 module

      • 关键字mod
      • 可嵌套
      • 可包含其他项的定义
      mod module_a {
          mod module_a_1 {
              fn function_1() {}
              fn function_2() {}
          }
      
          mod module_a_2 {
              fn function_2() {}
              fn function_3() {}
          }
      }
      
  • Path(路径):为 struct、function 或 module 等项命名的方式

    • 为了在 Rust 的模块中找到某个条目,需要使用路径

    • 路径的形式

      • 绝对路径:从 crate root 开始,使用 crate 名或字面值 crate
      • 相对路径:从当前模块开始,使用selfsuper或当前模块的标识符
    • 路径至少有一个标识符组成,标识符之间使用::

    mod module_outside {
        pub(crate) mod module_inside {
            pub(crate) fn function_1() {}
            pub(crate) fn function_2() {}
        }
    }
    
    pub fn function() {
        crate::module_outside::module_inside::function_1();
        module_outside::module_inside::function_2();
    }
    
    • super关键字可以用来访问父级模块路径中的内容

(3)use关键字

  • 可以使用use关键字将路径导入作用域内

    mod module_outside {
        pub mod module_inside {
            pub fn function_1() {}
        }
    }
    
    use crate::module_outside::module_inside;
    
    pub fn function() {
        module_inside::function_1();
    }
    
  • use的习惯用法

    • 将函数的父级模块引入作用域
    • 指定 struct、enum 等的完整路径
    • 指定同名条目到父级
  • 使用pub use重新导出名称

    • 使用use将路径导入到作用域内后,该名称在此作用域内是私有的
    • 重导出
      • 将条目引入作用域
      • 该条目可以被外部代码引入到其作用域中
  • as关键字

    • as关键字可以为引入的路径指定本地的别名

0x08 常用的集合

(1)Vector

a. Vec<T>称作 vector

  • 由标准库提供
  • 可存储多个值
  • 智能存储相同类型的数据
  • 值在内存中连续存放

b. 创建 Vector

  • Vec::new函数

    fn main() {
        // let vec: Vec<i32> = Vec::new();
        let mut v = Vec::new();
        v.push(1);
        v.push(2);
    }
    
  • 使用初始值创建Vec<T>,使用vec!

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

c. 删除 Vector

  • 与任何其他 struct 一样,当 Vector 离开作用域之后就会被清理

  • 读取 Vector 元素

    • 索引(越界:panic)

      fn main() {
          let v = vec![1, 2, 3, 4];
          let first: &i32 = &v[0];
          println!("first: {:?}", first);
          }
      }
      
    • get方法(越界:返回 None)

      fn main() {
          let v = vec![1, 2, 3, 4];
          match v.get(1) {
              Some(x) => println!("v[1]: {}", x),
              None => println!("v[1]: None"),
          }
      }
      

d. 所有权和借用规则

  • 不能再同一作用域内同时拥有可变和不可变引用
fn main() {
    let mut v = vec![1, 2, 3, 4];
    let first= &v[0];
    v.push(5);	// mutable borrow occurs here
    println!("first: {}", first);
}

e. 遍历 Vector 的值

fn main() {
    let mut v = vec![1, 2, 3, 4];
    for i in &v {
        println!("{}", i);
    }
}

基于 vector 使用 enum 来存储多种数据类型

enum list {
 Int(i32),
 Float(f32),
 Str(String),
}

fn main() {
 let vec = vec![
     list::Int(1),
     list::Float(2.0),
     list::Str("3".to_string()),
 ];
}

(2)String

a. 概述

  • Rust 的核心语言层面,只有一个字符串类型:字符串切片 str(或&str
  • 字符串切片:对存储再其他地方、UTF-8 编码的字符串的引用
    • 字符串字面值:存储再二进制文件中,也是字符串切片
  • String 类型
    • 由标准库提供,而非核心语言
    • 可增长、可修改、可拥有
    • 采用了 UTF-8 编码
  • 其他字符串类型:OsString、OsStr、CString、CStr

b. 创建字符串

  • String::new()函数

    fn main() {
        let mut s = String::new();
    }
    
  • 使用初始值来创建 String

    • to_string()方法,可用于实现了 Display trait 的类型,包括字符串字面值

      fn main() {
          let data = "hello";
          let mut s = data.to_string();
      }
      
    • String::from()方法,从字面值创建 String

      fn main() {
          let data = "hello";
          let mut s = String::from(data);
      }
      

c. 更新 String

  • push_str()方法,把一个字符串切片附加到 String

    fn main() {
        let mut s = String::from("abc");
        s.push_str("def");
        println!("{}", s);
    }
    
  • push()方法,把单个字符附加到 String

    fn main() {
        let mut s = String::from("abc");
        s.push('d');
        println!("{}", s);
    }
    
  • 使用+直接连接字符串

    fn main() {
        let mut s1 = String::from("abc");
        let mut s2 = String::from("def");
        println!("{}", s1+&s2);
    }
    
  • 使用format!宏连接多个字符串

    fn main() {
        let mut s1 = String::from("abc");
        let mut s2 = String::from("def");
        let s = format!("{}-{}", s1, s2);
        println!("{}", s);
    }
    
    • println!()类似,但返回字符串
    • 不会获得参数的所有权

d. 访问 String

  • Rust 的字符串不支持索引语法访问

    • 索引操作应消耗一个常量时间(\(O(1)\)),而 String 无法保证需要遍历的内容是否存在非法字符
  • String 是对Vec<u8>的包装

    • len()方法

      fn main() {
          let len = String::from("abc").len();
          println!("{}", len);
      }
      
    • Rust 中有三种看待字符串的方式

      • 字节 Bytes
      • 标量值 Scalar Values
      • 字形簇 Grapheme Clusters
  • 遍历 String

    • 对于标量值使用chars()方法

      fn main() {
          let s = String::from("abc");
          for i in s.chars() {
              println!("{}", i);
          }
      }
      
    • 对于字节使用bytes()方法

      fn main() {
          let s = String::from("abc");
          for i in s.bytes() {
              println!("{}", i);
          }
      }
      

e. 切割 String

  • 可以使用[]和一个范围来创建字符串的切片(遵循 “ 左闭右开 ” 的范围内取值的规则)

    fn main() {
        let s = String::from("123456");
        let s1 = &s[0..2];
        println!("{}", s1);
    }
    
    • 如果切割时跨越了字符边界,程序就会 panic

(3)HashMap

a. 概述

  • HashMap<K, V>
    • 键值对的形式存储数据,一个键(Key)对应一个值(Value)
    • Hash 函数决定如何在内存中存放 K 和 V
    • 适用于通过任何类型的 K 来寻找对应的 V 数据,而非索引
  • HashMap 使用频率较低,因此不在 Prelude 中
  • 标准库对其支持较少,没有内置的宏来创建 HashMap
  • HashMap 的数据存储在堆中
  • HashMap 是同构的
    • 即在一个 HashMap 中,所有的 K 类型相同、所有的 V 类型相同

b. 创建 HashMap

  • 创建空 HashMap:new()函数
  • 添加数据:insert()方法
use std::collections::HashMap;

fn main() {
    let mut stus = HashMap::new();
    stus.insert(String::from("Amy"), 18);
    stus.insert(String::from("Bob"), 19);
}
  • 可以使用collect方法创建 HashMap

    • 在元素类型为 Tuple 的 Vector 上使用collect方法
      • 要求 Tuple 有两个值,分别作为 K 和 V
      • collect方法可以把数据整合成很多种集合类型
    use std::collections::HashMap;
    
    fn main() {
        let name = vec![String::from("Amy")m String::from("Bob")];
        let age = vec![18, 19];
        let stus: HashMap<_, _> = name.iter().zip(age.iter()).collect();
    }
    

c. HashMap 和所有权

  • 对于实现了 Copy trait 类型(如i32),值会被复制到 HashMap
  • 对于拥有所有权的值(如 String),值会被移动,所有权会被转移给 HashMap

d. 访问 HashMap 中的值

  • get()方法

    • 参数 K
    • 返回值:Option<&V>
    use std::collections::HashMap;
    
    fn main() {
        let mut stus = HashMap::new();
        stus.insert(String::from("Amy"), 18);
        stus.insert(String::from("Bob"), 19);
    
        let age = stus.get("Amy");
        match age {
            Some(age) => println!("age is {}", age),
            None => println!("age is not found"),
        }
    }
    

e. 遍历 HashMap

use std::collections::HashMap;

fn main() {
    let mut stus = HashMap::new();
    stus.insert(String::from("Amy"), 18);
    stus.insert(String::from("Bob"), 19);
    for (k, v) in &stus {
        println!("{}: {}", k, v);
    }
}

f. 更新 HashMap

  • HashMap 的大小可变

  • 每个 K 同时只能对应一个 V

  • 更新 HashMap 中的数据

    • K 已经存在,对应了一个 V

      • 替换现有的 V

        • 强制替换

          use std::collections::HashMap;
          
          fn main() {
              let mut stus = HashMap::new();
              stus.insert(String::from("Amy"), 18);
              stus.insert(String::from("Amy"), 19);
              println!("{:?}", stus)
          }
          
        • 判空后替换

          • 使用entry()方法,检查指定的 K 是否对应一个 V
            • 参数为 K
            • 返回值为enum Entry代表值是否存在
          • Entry 的or_insert()方法:如果 K 不存在则插入新值,否则返回 V 的可变引用
          use std::collections::HashMap;
          
          fn main() {
              let mut stus = HashMap::new();
              stus.insert(String::from("Amy"), 18);
              stus.entry(String::from("Amy")).or_insert(19);
              stus.entry(String::from("Bob")).or_insert(20);
              println!("{:?}", stus)
          }
          
      • 保留现有的 V

      • 合并新旧的 V

        use std::collections::HashMap;
        
        fn main() {
            let mut map = HashMap::new();
            let text = "The man is iron man";
            for word in text.split_whitespace() {
                let cnt = map.entry(word).or_insert(0);
                *cnt += 1;
            }
            println!("{:?}", map)
        }
        
    • K 不存在

      • 添加一对<K, V>

g. Hash 函数

  • 默认情况下,HashMap 使用加密功能强大的 Hash 函数,可以抵抗 DoS 攻击
    • 不是最快,但是安全性高
  • 可以指定不同的 hasher 来切换至另一个 Hash 函数
    • hasher 是实现 BuildHasher trait 的类型

-End-

posted @ 2023-07-02 21:12  SRIGT  阅读(38)  评论(0编辑  收藏  举报