(六)Rust_常用集合


常用的集合

前言

Rust 笔记也做到集合部分了,强烈推荐 rustling 工具,配合官方文档或者本系列笔记作为练习题是相当不错的。

Vector

  1. 可以使用 Vector 存储多个值:

    • Vec<T>,称为 Vector。
    • 其由标准库提供。
    • 可以存储多个值。
    • 只能存储相同类型的数据。
    • 值在内存中连续存放。
  2. 使用 Vec::new 创建 Vector:

    • let v: Vec<i32> = Vec::new();,由于 new 创建的 vec 是没有元素的,需要显式的指定 vec 类型。
    • let v2 = vec![1, 2, 3];,使用初始值创建 Vector,通过 vec! 宏。
  3. 更新 Vector:

    • 向 Vector 添加元素,使用 push 方法。
     let mut v = Vec::new();
     v.push(1); // 添加完元素 编译器可以推断出当前 vector 的类型,可以不再显式声明。
    
  4. 删除 Vector:

    • 与任何其他 struct 一样,当 Vector 离开作用域后,它就被清理掉了,其中的所包括的元素也被清理掉。
  5. 读取 vector 的元素:

    • 索引方式。
    • get 方法。
     let v = vec![1, 2, 3, 4, 5];
     let third: &i32 = &v[2];
    
     println!("The third element is {}", third);
    
     match v.get(2) {
         Some(num) => println!("The third element is {}", num),
         None => println!("There is no third element"),
     }
    
  6. 索引与 get 在处理访问越界时是有区别的:

    • 索引会引起 panic。
    • get 则会返回 None。
  7. 所有权和借用规则

    • 不能在同一作用域内同时拥有可变和不可变引用。
    let mut v = vec![1, 2, 3, 4, 5];
    let third = &v[2];
    v.push(6); // 这时存在对 v 对可变与不可变借用(third)会报错
    
    println!("The third element is {}", third);
    v.push(6); // 这时不可变应用 third 后续没有再进行使用,因此可以正常的可变借用
    
  8. 遍历 Vector:

     let mut v = vec![100, 32, 57];
     for i in &mut v {
         println!("{}", i);
         *i += 50;
     }
     for i in v {
         println!("{}", i);
     }
    

Vector 例子

  1. 使用 enum 来存储多种数据类型。

    • enum 的变体可以附加不同类型的数据。
    • enum 的变体定义在同一个 enum 类型下。
    enum SpreadssheetCell {
     Int(i32),
     Float(f64),
     Text(String),
    }
    
     fn test_vector_enum() {
         let _row = vec![
             SpreadssheetCell::Int(3),
             SpreadssheetCell::Text(String::from("blue")),
             SpreadssheetCell::Float(10.12),
         ];
     }
    

String

  1. Rust 中的字符串困难点:

    • Rust 倾向于暴露可能的错误。
    • 字符串数据结构复杂。
    • 默认是使用 UTF-8 编码。
  2. 在 Rust 的核心语言层面,只提供来一个字符串类型:字符串切片 str(或 &str)。

    • 字符串切片:对于存储在其他地方、UTF-8 编码的字符串引用。
      • 字符串字面值:存储在二进制文件中,也就是字符串切片。
  3. String 类型来自标准库,不是核心语言层。其特点是可增长、可修改、可拥有的字符串数据管理结构,也采用 UTF-8 编码。

其他字符串类型

  1. Rust 的标准库中还包括其他的字符串类型,例如:OsString、OsStr、CString、CStr 等。
    • String 或 Str 后缀是指,拥有借用的变体。
    • 可存储不同编码的问题或在内存中以不同的形式展现。

String 使用

  1. 创建字符串:

    • 很多 Vec 的操作都可以作用于 String。
    • String::new() 函数:let mut s = String::new();
  2. 使用初始值来创建 String:

    • to_string() 方法,可用于实现来 Display trait 的类型,包括字符串字面值。
     let data = "initial contents";
     let _s = data.to_string();
    
     let _s1 = "initial contents".to_string();
    
    • String::from() 函数从字面值创建 String。
  3. 更新 String:

    • push_str() 方法:把一个字符串切片附加到 String。
     let mut s = String::from("foo");
     s.push_str("bar"); //foobar
    
     let s1 = "bar2".to_string();
     s.push_str(&s1); // 仅借用 s1 不会获取其所有权,后续可继续使用
    
    • push() 方法:将单个字符附加到 String。example: s.push('l');

    • + 运算符:拼接字符串。

     let s1 = "Hello, ".to_string();
     let s2 = "World!".to_string();
    
     let s3 = s1 + &s2; // 这里 s1 需要是 String,s2 是要求字符串切片或 String 类型引用。
    
     /*
     println!("s1 = {}", s1);  s1 已经被 moved,因此不能再使用
     error[E0382]: borrow of moved value: `s1`
       --> src/main.rs:25:25
        |
     20 |     let s1 = "Hello, ".to_string();
        |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
     ...
     23 |     let s3 = s1 + &s2; // 这里 s1 需要是 String,s2 是要求字符串切片或 String 类型引用。
        |              -- value moved here
     24 |
     25 |     println!("s1 = {}", s1);
        |                         ^^ value borrowed here after move
     */
     println!("s2 = {}", s2);
     println!("s3 = {}", s3);
    
    • format! 宏:连接多个字符串,它不会获取其他字符串的所有权,会返回一个全新的字符串:
     let s1 = String::from("tic");
     let s2 = String::from("tac");
     let s3 = String::from("toe");
    
     //let s3 = s1 + "-" + &s2 + "-" + &s3;
     //print!("{}", s3);
    
     let s = format!("{}-{}-{}", s1, s2, s3);
     print!("{}", s);
    

String 访问

  1. 按索引的形式对 String 进行访问是不允许的。example: s[0]

  2. String 内部表示,其实是对 Vec 的包装。又一个 len 方法可以返回 String 的长度(所占的字节数)。

    let len = String::from("hola").len();
    //Unicode 标量值往往不是一个字节表示,对于中文或者其他文字可能两个或三个字节表示一个文字。
    // 你好。len() 可能返回 4,这时通过索引进行访问就存在问题了。
    
    println!("{}", len);
    

字节(Bytes)、标量值(Scalar Values)、字形簇(Grapheme Clusters)

  1. Rust 有三种看待字符串的方式:

    • 字节:String.bytes(),最基本的字节数据。
    • 标量值:String.Chars()
    • 字形簇(最接近单个语言字符的):标准库是不支持直接字形簇。
  2. Rust 不允许对 String 进行索引的最后一个原因:

    • 索引操作应消耗一个常量时间(O(1))。
    • 而 String 无法保证:需要遍历所有内容,来确定有多少个合法字符。

切割 String

  1. 可以使用 []一个范围来创建字符串的切片。

    • 注意需要按照字符的边界进行,如果切割在 unicode 中间则会引起 panic。
     let hello = "Здравствуйте";
     let s = &hello[0..4];
     println!("{}", s); //Зд
    
     /*
     thread 'main' panicked at 'byte index 3 is not a char boundary; it is inside 'д' (bytes 2..4) of `Здравствуйте`'
     , src/main.rs:69:14
     note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
     */
     let s = &hello[0..3];
    

HashMap<K, V>

  1. 键值对的形式存储数据,一个键(key)对应一个值(Value)。
  2. 其内部主要使用 Hash 函数:决定如何在内存中存放 K 和 V。

创建 HashMap

  1. 创建空 HashMap: new() 函数。

  2. 添加数据:insert() 方法。

  3. HashMap 不在预导入模块中,因此需要使用 use 进行导入。标准库对其支持比较少,没有内置宏来进行创建。

  4. HashMap 数据是存储在 Heap 上的。

  5. HashMap 是同构的:

    • 所有的 K 必须是同一种类型。
    • 所有的 V 必须是同一种类型。
    use std::collections::HashMap;
    
    fn main() {
        let mut scores = HashMap::new();
        scores.insert(String::from("Blue"), 10);
    }
    
  6. 另一种创建 HashMap 的方式:collect 方法。

    • 在元素类型为 Tuple 的 Vector 上使用 collect 方法,可以创建一个 HashMap:
      • 要求 Tuple 有两个值:一个作为 K,一个作为 V。
      • collect 方法可以把数据整合程很多种类型,包括 HashMap,因此返回值需要显式的指明类型。
     let teams = vec![String::from("Blue"), String::from("Yellow")];
     let intial_scores = vec![10, 50];
    
     // 使用 zip (拉链)来创建一个 tuple
     let _scores: HashMap<_, _> = teams.iter().zip(intial_scores.iter()).collect();
    

HashMap 和所有权

  1. 对于实现了 Copy trait 的类型(例如 i32),值会被复制到 HashMap 中。

  2. 对于拥有所有权的值(例如 String),值会被移动,所有权会转移给 HashMap。

     let field_name = String::from("Favorite color");
     let field_value = String::from("Blue");
    
     let mut map = HashMap::new();
     map.insert(field_name, field_value);
    
     /*
         error[E0382]: borrow of moved value: `field_name`
     --> src/main.rs:24:24
     |
     18 |     let field_name = String::from("Favorite color");
     |         ---------- move occurs because `field_name` has type `String`, which does not implement the `Copy`
     trait
     ...
     22 |     map.insert(field_name, field_value);
     |                ---------- value moved here
     23 |
     24 |     println!("{}: {}", field_name, field_value);
     |                        ^^^^^^^^^^ value borrowed here after move
     |
     */
     //println!("{}: {}", field_name, field_value);
    
  3. 如果将值的引用插入到 HashMap,值本身不会移动。

    • 在 HashMap 有效期间内,被引用的值必须保持有效。
     let field_name = String::from("Favorite color");
     let field_value = String::from("Blue");
    
     let mut map = HashMap::new();
     map.insert(&field_name, &field_value);
    
     println!("{}: {}", field_name, field_value);
    

访问 HashMap 中的值

  1. get 方法:

    • 参数:K
    • 返回:Option<&V>
     let mut scores = HashMap::new();
     scores.insert(String::from("Blue"), 10);
     scores.insert(String::from("Yellow"), 50);
    
     let team_name = String::from("Blue");
    
     let score = scores.get(&team_name);
    
     match score {
         Some(s) => println!("{}", s),
         None => println!("team not exit"),
     }
    

遍历 HashMap

  1. for 循环:
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

for (k, v) in &scores {
    println!("{}: {}", k, v);
}

更新 HashMap<K, V>

  1. HashMap 大小可变,但是每个 K 同时只能对应一个 V。

  2. 更新 HashMap 中的数据:

    • K 已经存在,对应一个 V:
      • 替换现有 V。
      • 保留现有 V,忽略新的 V。
      • 合并现有 V 和新的 V。
    • K 不存在:
      • 添加一对 K,V。
  3. 覆盖现有的 V:

     let mut scores = HashMap::new();
     scores.insert(String::from("Blue"), 10);
     scores.insert(String::from("Blue"), 50);
    
     println!("{:?}", scores); //{"Blue": 50}
    
  4. 只在 K 不对应任何值的情况下,才插入 V:

    • entry 方法:检查指定的 K 是否有对应一个 V。

      • 参数为 K。
      • 返回 enum Entry:代表值是否存在。
    • entry 的 or_insert() 方法:

      • 返回:
      • 如果 K 存在,返回到对应 V 的一个可变引用。
      • 如果 K 不存在,将方法参数作为 K 的新值插入进去,返回到这个值的可变引用。
     let mut scores = HashMap::new();
     scores.insert(String::from("Blue"), 10);
     //scores.entry(String::from("Yellow")).or_insert(50);
     let e = scores.entry(String::from("Yellow"));
     println!("{:?}", e); //Entry(VacantEntry("Yellow"))
    
     e.or_insert(50);
    
     let e = scores.entry(String::from("Blue"));
     println!("{:?}", e); //Entry(OccupiedEntry { key: "Blue", value: 10, .. })
     e.or_insert(50);
    
     println!("{:?}", scores); //{"Blue": 10, "Yellow": 50}
    
  5. 基于现有 V 来更新 V。

     let text = "hello world wonderful world";
     let mut map = HashMap::new();
    
     for word in text.split_whitespace() {
         let count = map.entry(word).or_insert(0); // 如果 K 存在,返回到对应 V 的一个可变引用。
         *count += 1;
     }
    
     println!("{:?}", map); //{"wonderful": 1, "world": 2, "hello": 1}
    

Hash 函数

  1. 默认情况下,HashMap 使用加密功能强大的 Hash 函数,可以抵抗拒绝服务(DOS)攻击,所以不是可用的最快 Hash 算法。
    • 具有更好的安全性。
  2. 可以指定不同的 hasher 来切换到另一个函数。
    • hasher 是实现 BuildHasher trait 类型。

文章作者: Layton
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Layton !
  目录