Rust 常用集合

686 阅读7分钟

常见集合

Rust 标准库中包含了一系列被称为集合(collections)的非常有用的数据结构,集合可以包含多个值,并且不同于内建的数据或元组类型,集合内的数据是存储在堆上的,这意味着集合内数据的数量不必在编译时知道。在 Rust 中,vector 、字符串、hash map 这三个是比较常用的集合类型。

Vector

vector 允许我们一个挨着一个地存储一系列数量可变的值,其类型是 Vec<T>vector 只能存储相同类型的值

创建 vector

通过 Vec::new 可以创建一个空的 vector

let v: Vec<i32> = Vec::new();
println!("v is {}", v.len());

为了方便,Rust 也提供了一个更简短地创建 vectorvec! 宏:

let v = vec![666, 666, 666];
println!("v = {:?}", v);

这里没有显式地标注类型,是因为在此时 Rust 可以自动推断类型。

还可以通过 Vec::from 创建一个 vector

let v = Vec::from([666, 666, 666]);
println!("v = {:?}", v);

修改 vector

通过 push 方法向 vector 增加元素:

let mut v = Vec::from([666, 666, 666]);
v.push(777);
println!("v = {:?}", v);

注意⚠️:这时候需要使用 mut 关键字使其可变,因为需要改变 vector 的值。

通过 remove 方法删除 vector 中某个元素,通过索引来删除具体元素:

let mut v = Vec::from([666, 666, 666]);
v.push(777);
println!("v = {:?}", v);

v.remove(3);
println!("v = {:?}", v);

通过 clear 方法直接清空 vector 里的元素

let mut v = Vec::from([666, 666, 666]);
v.push(777);
println!("v = {:?}", v);

v.remove(3);
println!("v = {:?}", v);

v.clear();
println!("v = {:?}", v);

// v = [666, 666, 666, 777]
// v = [666, 666, 666]
// v = []

可以通过 索引或 get 方法,来获取 vector 中的元素:

#![allow(unused)]
fn main() {
  let v = vec![666, 666, 666];

  // 索引方式
  let first_element = &v[0];

  println!("index pattern: {first_element}");

  // Option get方式
  let first_element: Option<&i32> =  v.get(0);

  match first_element {
      Some(ele) => println!("ele: {}", ele),
      None => println!("None"),
  }
}

通过索引访问一个不存在的元素,即引用一个不存在的元素时,Rust 会造成 panic

通过 get 方法访问一个不存在的元素时,Rust 不会造成 panic 而是返回 None ,它可以容忍偶尔出现超过 vector 范围的访问,也就是说这种方式下出现越界访问属于正常情况,不会造成 panic

在使用索引方式来获取 vector 中的元素时,需要特别注意借用规则:

#![allow(unused)]
fn main() {
  let v = vec![666, 666, 666];

  // 索引方式
  let first_element = &v[0];

  v.push(888);

  println!("index pattern: {first_element}");
}

上方代码通过索引方式来访问 vector 中元素时,就与借用规则产生了冲突,不可变引用和可变引用不能同时存在于相同作用域中。

那为什么 vector 结尾的变化会关联到第一个元素的引用?

是由于 vector 的工作方式,在没有足够空间并在 vector 的结尾增加新元素时,所有元素依次相邻存放,可能会要求分配新内存并老的元素拷贝到新空间内。这时,第一个元素的引用就指向了被释放的内存。于是,借用规则防止了这个情况的发生。

使用 for 循环来获取 vector 中的每一个元素的不可变引用

#![allow(unused)]
fn main() {
  let v = vec![666, 666, 666];
  
  for i in &v {
    println!("{}", i);
  }
}

遍历可变 vector 中的每一个元素的可变引用以便能改变它们

#![allow(unused)]
fn main() {
  let mut v = vec![666, 666, 666];
  
  for i in &mut v {
    // 为了修改可变引用所指向的值,必须使用解引用运算符 (*) 来获取 i 的值
    *i += 1;
  }

  println!("{:?}", v);
}

使用枚举来存储多种类型

vector 只能存储相同类型的值,这固然满足不了某些场景。结合枚举,就能在 vector 中存放多种类型了。由于枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中存储不同的类型值时,可以定义一个枚举。

fn main() {
  enum Apple {
    SmallApple(i32),
    MiddleApple(f64),
    BigApple(String),
  }

  let v = vec![
    Apple::SmallApple(6),
    Apple::MiddleApple(6.6),
    Apple::BigApple(String::from("BigApple"))
  ];

  for i in &v {
    match i {
      Apple::SmallApple(i) => println!("SmallApple: {}", i),
      Apple::MiddleApple(i) => println!("MiddleApple: {}", i),
      Apple::BigApple(i) => println!("BigApple: {}", i),
    }
  }
}

// 输出为
SmallApple: 6
MiddleApple: 6.6
BigApple: BigApple

Rust 在编译时就必须准确地知道 vector 中类型的原因是因为编译器需要知道存储每个元素到底需要多少内存

vector 在离开作用域时会被释放

#![allow(unused)]
fn main() {
  {
    let v = vec![1, 2, 3];
  } // v 被释放掉了
}

vector 被丢弃时,所有其内容也会被丢弃!

字符串

字符串是字符的集合。字符串可能指的是 String 或 string slice &str 类型,而不仅仅是其中一种类型

通过 String::new 方法新建一个空的字符串

let mut s = String::new();

也可以通过 String::from 方法从字符串字面值创建 String

let s = String::from("Hello Rust!");

String 的大小可以增加,其内容也可以改变,因为它是一种可增长、可变、可拥有、UTF-8 编码的字符串类型。

可以使用 pushpush_str 来更新字符串

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

push 方法获取一个单独的字符作为参数,并附加到 String 中。

fn main() {
  let mut s1 = String::from("Hello");
  s1.push('!');
  println!("{}", s1);
}

对于复杂的字符串链接,可以使用 format! 宏:

fn main() {
  let s1 = String::from("Hello");
  let s2 = String::from("World");
  let s3 = String::from("!");

  let s = format!("{}-{}{}", s1, s2, s3);

  println!("{}", s); 
}

与其他语言不一样,Rust 的字符串并不支持索引

Hash Map

Hash Map 允许我们将值与一个特定的 key 相关联,其类型为 HashMap<K, V>HashMap<K, V> 类型存储了一个键类型 K 对应一个值类型 V 的映射。它通过一个哈希函数来实现映射,决定如何将键和值放入内存中。Hash Map 存储的数据都储存在堆上

新建一个 Hash Map ,可以通过 new 创建一个空的 HashMap

use std::collections::HashMap;

fn main() {
  let mut company = HashMap::new();
  company.insert(String::from("Boss"), 56);
  company.insert(String::from("Worker"), 32);

  println!("company: {:?}", company);
}

可以通过 get 方法来从 Hash Map 中获取值,该方法返回 Option<&V>,如果某个键在 Hash Map 中没有对应的值,会返回 None

use std::collections::HashMap;

fn main() {
  let mut company = HashMap::new();
  company.insert(String::from("Boss"), 56);
  company.insert(String::from("Worker"), 32);

  let name = String::from("Boss");
  let age = company.get(&name).copied().unwrap_or(0);
  println!("age: {:?}", age); // 输出为:56
}

遍历 Hash Map会以任意顺序打印出每一个键值对

use std::collections::HashMap;

fn main() {
  let mut company = HashMap::new();
  company.insert(String::from("Boss"), 56);
  company.insert(String::from("Worker"), 32);

  for (key, value) in &company {
    println!("{}: {}", key, value);
  }
}

更新 Hash Map

如果插入了一个键值对,并且用相同的键插入了一个不同的值,与这个值相关联的旧值将被覆盖

use std::collections::HashMap;

fn main() {
  let mut company = HashMap::new();
  company.insert(String::from("Boss"), 56);
  company.insert(String::from("Worker"), 32);

  company.insert(String::from("Boss"), 88);

  for (key, value) in &company {
    println!("{}: {}", key, value);
  }
}

如果哈希 Map 中键已经存在,则不做任何操作;如果不存在则连同值一块插入。这种判断非常常见,在 Rust 有一个专门的 API 用于描述,叫做 entry 。它获取我们想要检查的键作为参数,返回一个枚举 Entry

use std::collections::HashMap;

fn main() {
  let mut company = HashMap::new();
  company.insert(String::from("Boss"), 56);
  company.insert(String::from("Worker"), 32);

  company.insert(String::from("Boss"), 88);

  company.entry(String::from("Manager")).or_insert(45);

  for (key, value) in &company {
    println!("{}: {}", key, value);
  }
}

Entryor_insert 方法在键对应的值存在时,就返回这个值对应的可变引用;如果不存在则将参数作为新值插入并返回新值的可变引用

输出为:

Manager: 45
Boss: 88
Worker: 32

注意,遍历 Hash Map 会以任意顺序进行输出。