Skip to content

Vector 容器

概念

存储在堆内存的数组,可以动态扩容,写法 Vec<T>

  • 标准库提供
  • 可存储多个值
  • 只能存储相同类型
  • 在内存中连续存放
rust
fn main() {
    // 使用泛型显式声明Vec里存储的数据类型
    let arr: Vec<i32> = Vec::new();

    // 使用初始值创建,使用vec!宏, 编译器可以自动推导出Vec里存储的数据类型
    let arr2 = vec![1, 2, 3];
}

插入数据使用 push 方法

rust
fn main() {
  let mut arr = Vec::new();
  // 往Vec添加数据
  arr.push(1);
}

读取值,使用索引或者 get 方法

rust
fn main() {
  let arr = vec![1, 2, 3, 4, 5];
  let second = &arr[1];
  println!("第二个元素是:{}", second); // 第二个元素是:2

  // get方法传索引值,使用get时,如果索引值超出范围会比较安全,会走下面的None逻辑
  match arr.get(1) {
    Some(second) => println!("能拿到第二个元素:{}", second), //能拿到第二个元素:2
    None => println!("拿不到第二个元素"),
  }
}

Vec 的所有权借用

rust
fn main() {
  let mut arr = vec![1, 2, 3, 4, 5];
  let first = &arr[0]; // 不可变的引用

  // 这里会报错cannot borrow `arr` as mutable because it is also borrowed as immutable
  arr.push(6);

  println!("第一个元素的值是: {}", first);
}

为什么这里会报错呢,因为我们 first 借用的是第一个元素,下面 arr.push 新添加一个元素,又因为 Vec 在内存中是连续存放的,可能新添加进来的一个 6 会遇到地址不够的情况,那就得进行重新分配,得去找另一块连续地址的内存去存放,但是我们的 first 还是指向的原来的第 1 个元素的地址,就会出现问题了。

遍历 Vec 的值并修改里面的每一个元素

rust
fn main() {
  let mut arr = vec![1, 2, 3, 4, 5];
  for i in &mut arr {
    //解引用才能去修改值
    *i += 1;
    println!("遍历修改后的值是:{}", i);
  }
}

// 遍历修改后的值是:2
// 遍历修改后的值是:3
// 遍历修改后的值是:4
// 遍历修改后的值是:5
// 遍历修改后的值是:6

如果想要在 Vec 中存储不同的数据类型,可以使用 enum

  • enum 的变体可以附加不同的数据类型
  • enum 的变体定义在同一个 enum 类型下
rust
enum ArrType {
  Int(i32),
  Float(f32),
  Text(String),
}

fn main() {
  let arr = vec![
    ArrType::Float(1.2),
    ArrType::Int(10),
    ArrType::Text(String::from("hello world"))
  ]
}

String

概念

Rust 中的字符串可能会比较让人困扰,在 Rust 中它并不简单

  • Rust 倾向于暴露可能的错误
  • 字符串数据结构复杂
  • 使用了 UTF-8 编码

字符串是基于 byte 的集合,里面提供了一些方法能将 byte 解析为文本。

Rust 在核心语言层面只有一种字符串类型:字符串切片 str( &str)。它是对存储在其它地方、UTF-8 编码的字符串的引用。字符串字面值存储在二进制文件中。

标准库中的 String 类型:内容可增长、可修改、可获得所有权

通常所说的字符串是 String 或者&str

创建字符串

其实在这里之前我们就已经有过很多字符串的实践了,比如创建一个字符串String::new()

使用初始值来创建字符串的话可以使用to_string(),可用于实现了 Display trait 的类型,包括字符串字面值。

rust
fn main() {
  //如果直接创建字面量字符串,它的类型是&str。
  let str = "hello world";

  //下面两种创建字符串的方法效果是一样的,可根据个人喜好选择写法。
  //而使用to_string方法,可以将它转变为String类型
  let str2 = "hello world".to_string();
  //使用String::from来创建String类型的字符串。
  let str3 = String::from("hello world");
}

更新 String

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

  • push 方法:把单个字符附加到 String 中

  • +运算符:这个方法实际上使用了类似于 fn add(self, &str) -> String 的方法。标准库中的 add 方法是使用了泛型的,所以这里说是类型于,这个方法只能把&str 类型添加到 String 中,这里我们传入的&s2 是 String 的引用,这个方法使用解引用强制转换将 String 转换成了字符串切片&str,所以可以调用成功。s1 的所有权转移到 add 函数里,所以 add 函数执行完 s1 就失效了。

    rust
    fn main() {
        let s1 = String::from("hello");
        let s2 = String::from(" world");
    
        let s3 = s1 + &s2;
    
        println!("{}", s3); //hello world
        println!("{}", s1); //s1不能再使用了
        println!("{}", s2); //s2可以继续使用
    }
  • format!宏:连接多个字符串

    rust
    fn main() {
      let s1 = String::from("打");
      let s2 = String::from("飞");
      let s3 = String::from("鸡");
    
      let s = format!("{}-{}-{}", s1, s2, s3);
      println!("{}", s); //打-飞-鸡
    }

String 类型的索引访问

String 类型的内部实现其实就是对 Vec<u8>的包装。

String 类型不支持索引方式访问,比如 str[0]或者&str[0] 都不能对 str 字符串进行索引访问。因为索引操作的时间复杂度是 O(1),而 String 不能保证这个 O(1)的时间复杂度,因为它需要去遍历所有内容来确认有多少合法的字符。

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

  • 字节
  • 标量值(Unicode 标量值)
  • 字形簇(最接近所谓的"字母"):Rust 标准库中没有实现,可以使用第三方库
rust
fn main() {
  let s = "中文";

  //这里使用的是字节的概念
  for i in s.bytes() {
    println!("{}", i);
  }
  // 一个中文用三个字节表示
  // 228
  // 184
  // 173
  // 230
  // 150
  // 135

  // 这里使用的是标量值的概念
  for i in s.chars() {
    println!("{}", i);
  }

  // 中
  // 文
}

切割 String

这里之前其实也讲了,就是字符串切片。在多语言环境中会容易出现问题

rust
fn main() {
  let str = "asdfghjkl";
  println!("{}", &str[..2]); //as
}
rust
fn main() {
  let str = "中文";
  println!("{}", &str[..3]);
  // 因为一个中文表示三个字节类似于(a,b,c)(a,b,c),切割不按照边界切的话就会编译出错
  // 'byte index 4 is not a char boundary; it is inside '文' (bytes 3..6) of `中文`'
  println!("{}", &str[0..4]); //这里编译报错panicked
}

HashMap

概念

HashMap<K, V>,K 是 key,V 是 value,类似于 JS 的 Map。以键值对的形式存储数据,数据存储在堆中,HashMap 是同构的,就是所有 K 必须是同种类型,所有 V 也必须是同种类型。

适用于使用 key 来寻找值

创建

  • 因为 HashMap 用的比较少,所以不在 prelude 预导入模块中,所以需要我们手动引入
  • 标准库对其支持较少,没有内置的宏来创建 HashMap
  • 初始化没有数据时需要显式声明类型,不显式声明时可以使用 insert 方法来添加一条数据,会自动推导出 hashMap 的类型
rust
use std::collections::HashMap;

fn main() {
  let mut map: HashMap<String, i32> = HashMap::new();

  // 或者
  let mut map2 = HashMap::new();
  map2.insert(String::from("0"), 100);
}

Collect 方法创建 HashMap

在元素类型为 Tuple 的 Vector 上使用 collect 方法,可以组建一个 HhashMap

  • 要求 Tuple 有两个值,一个作为 Key,一个作为 Value
  • collect 方法可以把数据整合成很多种数据集合类型,包括 HashMap,返回值需要显式声明
rust
use std::collections::HashMap;

fn main() {
  let colors = vec![String::from("red"), String::from("pink")];
  let items = vec![10, 20];

  // 遍历items和colors,调用zip方法创建一个元组,就是拉链的意思
  // 这两个Vector像拉链一样拉形成了一个元组的数组,最后使用collect生成hashMap
  // collect可以生成很多种数据结构,所以必须要在前面显式声明数据类型
  // 两个下划线占位符,Rust能推导出Key和Value的数据类型
  let map: HashMap<_, _> = items.iter().zip(colors.iter()).collect();
}

HashMap 和所有权

  • 对于实现了 Copy trait 的类型(i32 等存放在栈中的基础数据类型),值会被复制到 HashMap 中
  • 对于拥有所有权的值(如 String 等存在堆中的数据类型),值会被移动,所有权会转移给 HashMap
  • 如果将值的引用插入到 HashMap 中,值本身不移动
rust
use std::collections::HashMap;

fn main() {
  let s1 = String::from("hello");
  let s2 = String::from("world");

  let mut map = HashMap::new();
  map.insert(s1, s2);

  // 因为s1、s2的所有权已经转移到HashMap里了,所以这里的s1和s2就已经失效了
  // borrow of moved value: `s1`,`s2`
  // 如果上面改成将引用传入,那么s1和s2就不会失效
  println!("{}, {}", s1, s2);
}

访问 HashMap 中的值

get(key: K) -> Option<&V>

rust
use std::collections::HashMap;

fn main() {
  let mut map = HashMap::new();
  map.insert(String::from("red"), 10);
  map.insert(String::from("pink"), 20);

  let red = String::from("red");
  let val = map.get(&red);

  match val {
    Some(s) => println!("{}", s),
    None => println!("HashMap中没有该值"),
  }
}

遍历

rust
use std::collections::HashMap;

fn main() {
  let mut map = HashMap::new();
  map.insert(String::from("red"), 10);
  map.insert(String::from("pink"), 20);

  for (k, v) in &map {
    println!("k: {} , v: {}", k, v);
  }
}
// k: pink , v: 20
// k: red , v: 10

更新

  • HashMap 大小可变
  • 每个 K 只能对应一个 V

更新策略:

  • 替换原有的 V:就是两次插入的键相同时,第二次插入的值会覆盖掉第一次的值

  • 添加:HashMap 中没有 K,那么我们才去插入 V,我们需要使用 entry 方法(检查指定的 K 是否对应一个 V,参数为 K,返回值 enum Entry 代表值是否存在)

    rust
    use std::collections::HashMap;
    
    fn main() {
      let mut map = HashMap::new();
      map.insert(String::from("red"), 10);
      map.insert(String::from("pink"), 20);
    
      // blue的键不存在时就插入100
      map.entry(String::from("blue")).or_insert(100);
    
      // {"red": 10, "pink": 20, "blue": 100}
      println!("{:?}", map);
    }
  • 基于现有的 K 更新 V:

    rust
    use std::collections::HashMap;
    
    fn main() {
      let text = "hello world today is a good day";
      let mut map = HashMap::new();
    
      // 按空格分割的遍历器
      for word in text.split_whitespace() {
        // 判断遍历到的单词在Map里是否存在,不存在就插入0
        // 这里的count得到的是插入值的引用
        let count = map.entry(word).or_insert(0);
        //解引用然后将插入的值+1
        *count += 1;
      }
    
      // {"day": 1, "is": 1, "world": 1, "hello": 1, "today": 1, "good": 1, "a": 1}
      println!("{:#?}", map);
    }

Hash 函数

默认情况下,HashMap 使用加密功能强大的 Hash 函数,可以抵挡拒绝服务 (Dos) 攻击。

  • 它不是可用的最快的 Hash 算法
  • 但具有更好的安全性
  • 可以指定不同的 Hasher 来切换到另一个函数,Hasher 是实现 BuildHasher trait 的类型

每天进步一丢丢