Skip to main content
8 集合

8 集合

•2 min•388 words
集合就是动态大小,存储多个值的结构

集合可以包含多个值。与内建的数组和元组类型不同,这些集合指向的数据存储在堆上,这意味着数据量不需要在编译时已知,并且可以随着程序运行而增长或缩小。

有三种常用的集合结构

vector

Summary

下面按使用vector的几种重要操作来介绍。

新建

可以调用 Vec::new 函数,或者Vec::from

    let v: Vec<i32> = Vec::new();
    let v2 = Vec::from([0, 0, 0]);

还可以使用 vec! 宏

    let v = vec![1, 2, 3];

vec! 宏,它会创建一个新的 vector,并把你提供的值放进去。

Rust 会通过push或者vec!中的数据推断出你想存储的值的类型,所以很少需要写这种类型注解。

Capacity容量

capacity是数组实际分配的内存容量,capacity一定是大于等于实际容量的。当我们知道要使用的vec大致需要多少容量的时候最好一次性申请足够的容量,这样可以避免进行扩容操作导致进行数据的拷贝扩容。

更新

首先使用 mut 关键字让它变成可变的。

Summary
  • 通过下标索引访问。
  • 使用 get 方法获取引用,使用get_mut方法获取可变引用。

通过索引的方式,效率更高,但是容易导致索引越界的问题。
使用get方法会获得Option<T>需要进行额外的匹配,性能也略有消耗,但是更加安全。

如何移动集合中元素的所有权?

vec中的所有元素的所有权属于vec变量,如果直接将元素进行所有权转移(除非属于Copy类型),而vec本身却包含这个元素,那么vec变量将不可用。
正确的做法应该是将元素从vec中移除,再获得所有权。vec提供了pop,remove等移除元素的方法,这些方法会返回元素的所有权。
或者我们可以使用into_iter()将所有元素的所有权组成一个流并返回迭代器对象。

获取到元素的可变引用就可以进行修改。

Cite

删除

遍历访问

迭代器:内部状态对应流的位置

众多语言都提供了迭代遍历的语法,语义都是对当前集合状态进行变量。在迭代中万不可改变数组状态,迭代器是无法感知数组的动态变化的只能使用之前的快照版本。当然某些语言也提供了能同时让迭代器感知变化的特殊更新语法。但是迭代器的语义应当牢记。

迭代器的本质

for循环其实是 while let Some(i) = iter().next(){ 代码块 }的语法糖。

into_iter()是所有权变量的迭代。
iter()是不可变引用的迭代。
iter_mut()是可变引用的迭代。
而for i in v就是for i in v.into_iter()的语法糖。
还有for i in &v 等于 for i in v.iter()
for i in &mut 等于 for i in v.iter_mut()

常用方法

容量操作

初始化时设置容量

Vec::with_capacity(10);

使用时更改容量

 v.reserve(100);        // 调整 v 的容量,至少要有 100 的容量

使用时释放剩余容量

 v.shrink_to_fit();     // 释放剩余的容量,一般情况下,不会主动去释放容量

获取容量大小

v.capacity()

元素操作

Summary

数组追加到末尾

 v.extend([1, 2, 3]);    // 附加数据到 v

移动其他vec的元素到自己

v.append(&mut v1);              // 将 v1 中的所有元素附加到 v 中, v1会被清空: []
Summary

删除指定范围的元素,同时获取被删除元素的迭代器

let mut m: Vec<_> = v.drain(1..=3).collect();    

截断到指定长度,多余的元素被删除

v.truncate(1);                  // 截断到指定长度,多余的元素被删除, v: len = 1

清空vec

v.clear();                      // 清空 v, v: []
Summary

定点插入

v.insert(2, 3);       // 在指定索引插入数据,索引值不能大于 v 的长度

定点删除

assert_eq!(v.remove(1), 2);     // 移除指定位置的元素并返回, v: [1, 3]
Summary

末尾弹出元素,为空则None,不空则Some<T>

assert_eq!(v.pop(), Some(3));   // 删除并返回 v 尾部的元素,v: [1]

末尾新增元素

v.push(1);

拆分操作

按索引切分成两个 vec(类似1..5这种,不取末尾5)

let v2 = m.split_off(1);        // 指定索引处切分成两个 vec, m: [22], v2: [33, 44]

排序操作

Summary

默认的sort使用的是归并排序 + 插入排序自适应优化,而sort_unstable是超强优化版快速排序,速度:比 sort() 快 20%~40%

但是就是相同排序值的顺序并不稳定。

Summary

rust提供的排序方法,只能用于实现了 Ord trait 的类型(i32, String 等),并且只能升序。

sort_by以及sort_unstable_by就是by其他判断元素大小的方法。

需要传入一个函数:

impl<T> [T]  
pub fn sort_by<F>(&mut self, mut compare: F)  
where  
F: FnMut(&T, &T) -> Ordering

// --- odering 是一个枚举
pub enum Ordering {
    Less = -1,
    Equal = 0,
    Greater = 1,
}

只要这个函数符合函数签名即可,可以是fn函数也可以是闭包函数。

我们可以重新组合Ord trait中的方法,来实现降序。Ord中必须要实现的方法是cmp会返回Ording结果,我们只需要将调用cmp的实体换成右实体则能实现降序排列。

let mut v = [4, -5, 1, -3, 2];
v.sort_by(|a, b| b.cmp(a));

另外还有sort_by_key的方法,区别与sort_by,sort_by_key无须确定大小,只需要将元素转换成对应排序的key值即可。

只有当所有元素都实现 时,元组自动实现

[!todo] 只有当所有元素都实现 Ord 时,元组自动实现 Ord
元组按字典序比较:先比第 1 个元素,相等再比第 2 个,依此类推。

Warning
let v = Vec![5,2,3,1,4];
v.sort_by_key(|a,b| (num>3, Reverse(num)));

提示:Reverse是一个结构体元组,实现了Ord,其中cmp方法是相反比较。

浮点数排序小坑

在浮点数当中,存在一个NAN 的值,这个值无法与其他的浮点数进行对比,因此,浮点数类型并没有实现全数值可比较 Ord 的特性,而是实现了部分可比较的特性 PartialOrd。
所以我们需要使用sort_by,在自定义方法中调用 partial_cmp 方法,处理NAN值,或许我们保证排序值中不出现NAN,直接unwrap是个不错的选择。

Todo

#[derive(Debug, Ord, Eq, PartialEq, PartialOrd)]

HashMap

Faq

hashmap的作用和定义其实已经不必再提,我们只需要了解rust中hashmap的操作方法。

Summary

新建

HashMap结构中有关联函数new,可以新建一个HashMap的变量。

let mut scores = HashMap::new();
Rust的prelude 预导入

[!todo] Rust的prelude 预导入
默认情况下,Rust 会把标准库中的一组内容自动带入每个程序的作用域,这组内容被称为 预导入(prelude)。你可以在标准库文档中查看 prelude 里都有哪些内容。
而hashmap显然不属于prelude内容。

更新

insert 覆盖插入

并返回原来键值对,如果没有则Option::None,如果有则返回旧值

use std::collections::HashMap;

let mut map = HashMap::new();
assert_eq!(map.insert(37, "a"), None);
assert_eq!(map.is_empty(), false);

map.insert(37, "b");
assert_eq!(map.insert(37, "c"), Some("b"));
assert_eq!(map[&37], "c"); // 赋值不能用 [],只能用于访问确定存在的键
→ 获取可变引用,修改值

[!summary] get_mut(&k) → 获取可变引用,修改值
键不存在返回 None,只修改,不新增

if let Some(val) = map.get_mut("key") {
    *val += 1; // 修改值
}
→ 最强大的更新 API

[!summary] entry(k) → 最强大的更新 API
HashMap是通过实现了Entry API来实现的操作内部键值对的。
Entry是一个由HashMap构建生成的枚举。

pub enum Entry<'a, K: 'a, V: 'a, A: Allocator = Global> {
    Occupied(OccupiedEntry<'a, K, V, A>),
    Vacant(VacantEntry<'a, K, V, A>),
}

默认包含两种变体,占用和空置,类似Result一样。

Entry提供了以下函数:

  • or_insert → 没有就插入,有就返回可变引用
*map.entry("key").or_insert(0) += 1;
  • or_insert_with → 不存在时才执行函数创建值
map.entry("key").or_insert_with(|| expensive_calc());
  • and_modify → 存在就修改
map.entry("key").and_modify(|v| *v += 1);

而且很多函数返回的也是entry可以组合使用

map.entry("key")
   .and_modify(|v| *v += 1)  // 存在就+1
   .or_insert(1);            // 不存在就设1

删除

  • remove(&k) → 删除元素
  • retain(...) → 按条件保留 / 批量删除
  • clear 清空所有元素
  • drain 清空映射表,以迭代器形式返回所有键值对,保留已分配内存以便重复使用。

若迭代器未遍历完毕就被丢弃,剩余键值对也会随之释放。该迭代器会持有映射表的可变借用,以此优化运行逻辑。

遍历访问

最常用的遍历方法for in

hashmap的iter()返回的是(key,value)元组

for (key, value) in &map // 常用方式 不拿走所有权
for (key, value) in &mut map // 获得可变的 key, value 引用
for key in map.keys() // 遍历key的不可变引用
for key in map.values() // 遍历value的不可变引用
// 还有keys_mut(),valus_mut()与vec特别类似。
Summary

map.get(&key) → 安全,返回 Option<&V>(最推荐)

map.get_mut(&key) → 获取可变引用,可修改值

map[&key] → 直接取值,不存在就 panic(确定 key 存在时用)

map.entry(key) → 高级 API,不存在就插入(最强)

常用方法

容量操作

HashMap同样有容量的概念,我们可以提前指定这个HashMap的容量,来避免后面频繁插入元素导致的内存申请与拷贝。

with_capacity(capacity: usize)

reserve(&mut self, additional: usize)

String

[!todo ] 字符串字面值:它不是栈上变量,也不是堆上变量,而是被硬编码在编译后的可执行文件的只读数据段
运行时内存结构

字符串的理解

字符是一个层次的编码,字符有english,中文,日文,罗马符号,表情符号等等。
而字符编码是另一个层次的编码,是为了将字符转换成二进制的数据,保存在计算机系统中。

Unicode是字符集,将中文,英文,表情符号等等几乎世界上最多的符号集合在一起编了号。

那么如何将这些字符保存在计算机中?如果直接将最大的Unicode字符作为最大表示范围,来让一个字符编码可以容纳所有可能的Unicode字符,那么这个一个编码可能就需要8字节,甚至更多字节。要知道ASCII编码只占一个字节。

所以为了让每个字符编码所占用的字节数最小,有了不同的Unicode编码方案。

UTF-8是使用最广泛的Unicode编码方案,一种变长编码:通过字符的前缀位判断这个字符占用多少字节。

  • 0xxxxxxx → 1 字节(ASCII 英文、数字、符号)
  • 110xxxxx → 2 字节
  • 1110xxxx → 3 字节(汉字、日韩文)
  • 11110xxx → 4 字节(超大字符、大部分 emoji)

编程语言内部也要存储字符,而Unicode的字符序号占用太大,所以还是得选用一个字符编码来表示字符。不同的编程语言有不同的选择,Java选择使用UTF-16来存储字符,而Rust、Python3、Go都选择使用Utf-8编码来储存字符。

但是当我们准确表示一个字符概念的时候,char类型中存储是字符集中该字符的码点,也就是序号。每个char都是4字节,没有编码的概念。

而字符串在有编码的概念,当我们获得这个字符串的字节时,可以看到内部的utf-8编码。

两种字符串

str字符串slices与String字符串

Rust 的核心语言中只有一种字符串类型,字符串 slice str,它通常以被借用的形式出现。借用程序不可变内存中的一段。
字符串(String)类型由 Rust 标准库提供,而不是编入核心语言,它是一种可增长、可变、可拥有、UTF-8 编码的字符串类型。
转换其实非常简单,常用的是&String[..],以及&str.to_string()

Cite

事实上 String 被实现为一个带有一些额外保证、限制和功能的字节 vector 的封装。

新建

  • String::new()
  • 某变量.to_String() 等任何实现了Display trait 的类型,比如字符串字面值。
  • String::from(“initial contents”);

更新

使用 push_str 和 push 追加字符串

push_str 方法采用字符串 slice,push 方法只能追加字符。

使用 + 运算符或 format! 宏拼接字符串

+运算符本质上是add方法的调用,左边必须使用所有权变量,而右边必须是借用。这个函数签名看起来像这样:
fn add(self, s: &str) -> String {

let s = String::from("hello") + " world";
// 本质上是调用了add方法
let s = String::add(String::from("hello"), " world");
之所有不允许两个所有权字符串是因为移动语义

[!warning] 之所有不允许两个所有权字符串+是因为移动语义
如果两个所有权通过+变成一个所有权,就不像移动语义中不消除所有权只是移动所有权的印象了。

强制转换机制

编译器可以把&String 参数强制转换成 &str。当调用 add 方法时,Rust 会使用一种叫做 deref 强制转换(deref coercion)的机制

使用format!宏

format! 与 println! 的工作原理相同6.9 调试与宏,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String。这个版本就好理解的多,宏 format! 生成的代码使用引用因此不会获取任何参数的所有权

Rust中String的本质

本质就是存储utf-8的编码

[!summary] 本质就是Vec<u8>存储utf-8的编码
字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。

所以rust并不运行我们直接使用[]通过索引访问String中的元素,因为往往这些元素只是一个字符的一部分。

最后一个 Rust 不允许使用索引获取 String 字符的原因是,索引操作预期总是需要常数时间(O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。

Warning

相比使用 [] 和单个值的索引,可以使用 [] 和一个 range 来创建含特定字节的字符串 slice,如果range中包含不完全的字符字节,那么就跟访问 vector 中的无效索引时一样程序崩溃。

遍历

最好的方法是明确表示需要字符还是字节

提供chars()方法,返回Chars是字符串切片字符的迭代器。
提供一个bytes()方法,返回bytes的字符串切片bytes的迭代器。

for c in "Зд".chars() {
    println!("{c}");
}

for b in "Зд".bytes() {
    println!("{b}");
}