文章目录
前言
这一篇介绍Rust常用的容器类型~
Rust常用的容器类型如下:
类型 | 容器 | 描述 |
---|---|---|
线性序列 | Vec<T> | 连续存储的可变长数组 |
-> | VecDeque<T> | 连续存储的可变长双端队列 |
-> | LinkedList<T> | 非连续存储的双向链表 |
键-值对 | HashMap<K,V> | 基于哈希表的无序键值对 |
-> | BTreeMap<K,V> | 基于B树的有序键值对,按Key排序 |
集合 | HashSet<T> | 基于哈希表的无序集合 |
-> | BTreeSet<T> | 基于B树的有序集合 |
优先队列 | BinaryHeap<T> | 基于二叉堆的优先队列 |
本文主要介绍Vec<T>
、VecDeque<T>
、HashMap<K,V>
以及字符串的使用方法,其它的容器类型使用方法大同小异,旁类触通,配合官方文档学习更佳。
7.1 Vec
Vec<T>
是动态数组,由标准库提供,可在运行时可增长或缩短数组的长度。动态数组在内存中用一段连续的内存存储元素,且只能存储相同类型的元素。
7.1.1 基础用法
创建
- 使用
Vec::new
函数创建空动态数组:let mut v1: Vec<i32> = Vec::new();
- 这里显式声明了
v
的类型,是因为在new
的时候并没有说明数组元素是什么类型的,无法自动推导出来,因此只能显式说明v
的类型。
- 这里显式声明了
- 使用
Vec::with_capacity
创建指定容量的动态数组:let mut v2: Vec<i32> = Vec::with_capacity(10);
- 使用
vec!
宏创建动态数组的同时完成初始化:let mut v3: Vec<i32> = vec![]; // 创建空Vec let mut v4 = vec![1,2,3]; // 创建一个指定初始值的Vec let mut v5 = vec![0; 10]; // 创建一个长度为10,每个元素初始值为0的Vec
- 这里由于有初始元素,因此能够推导出数组类型。
修改
修改包括:追加、修改指定索引的元素、删除最后一个元素、删除指定索引元素。
因为需要修改动态数组,所以声明时需要将动态数组声明为mut
才能进行修改。
push
追加元素:let mut v = vec![1,2,3]; v.push(4);
实例名[索引]
通过索引修改元素:let mut v = vec![1,2,3]; v[0] = 5; v[1] = 6; v[2] = 7
pop
删除最后一个元素,并返回该元素,如果数组为空,则返回None:let mut v = vec![1,2]; v.pop(); // Some(2) v.pop(); // Some(1) v.pop(); // None
remove
删除指定索引元素,然后后面的元素向前补齐,如果索引越界会导致程序错误:let mut v = vec![1,2]; v.remove(0); // 返回1,v中剩下[2] v.remove(1); // 越界,报错
访问
实例名[索引]
通过实例名访问指定元素,如果索引:let v = vec![1,2,3]; v[0]; // 1 v[3]; // 越界,报错
get
通过索引访问元素,返回值是Option<&T>
,如果元素存在则返回Some(T)
,否则返回None
:let v = vec![1,2,3]; v.get(1); // Some(2) v.get(3); // None
- 使用索引:
- 下标从0开始。
- 如果元素实现了Copy trait,则使用
v[idx]
会返回这个元素,但如果元素没有实现Copy trait,则无法直接用[]
返回元素(因为涉及move)。 - 使用
&
和[]
会返回一个引用,这种方式读取动态数组元素,如果越界了会报panic。
let v = vec![1,2,3,4,5]; let third: &i32 = &v[2]; println!("The third element is {}", third);
- 使用
get(索引值)
方法:- 返回一个
Option<&T>
,这种方式读取动态数组元素,如果越界了则返回None
match v.get(2) { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."), }
- 返回一个
- 索引和
get
方法在处理越界访问的区别:- 索引:越界会发生panic
- get:会返回None
容器内结构体值的访问
假如动态数组中保存的是自定义的结构体,需要更新容器第0项的某一个字段,需要保证该动态数组是mut
的,并且要求获取第0项元素的mut
引用。
例子如下:
struct Entity {
pub name: String,
pub year: i32,
}
fn main() {
let mut v = vec![
Entity{
name: String::from("xiaoming"),
year: 10,
},
Entity{
name: String::from("xiaohong"),
year: 15,
}
];
let x = &mut v[0];
x.year = 11;
}
上述例子,如果v
并不是mut
,那么就无法获取&mut v[0]
,因为无法从只读引用中获取可变引用。这说明修改容器持有所有权的数据也需要拥有这个容器的可变变量或可变引用。
遍历
let v = vec![100, 32, 57];
for i in &v { // &v 表示获取v中每一个元素的不可变借用
println!("{}", i);
}
let mut v = vec![1,2,3];
for i in &mut v { // &mut v 表示获取v中每一个元素的可变借用
*i += 1;
}
注意一个问题:在上面的例子中每一次循环获取的i
都是引用,如果获取的不是引用,即for i in v
,那么每一次循环都会发生所有权转移,即vector中的元素在每次循环开始都会转移给i,因此在循环结束以后vector就会被清理。 这里需要注意,所有上面的例子vector保存的是基础类型,但是这些数据都是保存在heap的,所以每个元素还是会有所有权。
fn main() {
let v = vec![1,2,3,4,5];
for e in v {
println!("{:#?}", e);
}
println!("{:#?}",v); // 这一句会报错:v移动以后再次读v
}
7.1.2 所有权和借用规则
- “不能在同一作用域内同时拥有可变和不可变引用”的规则对
Vec
是适用的。 - 下面的例子会报错:
fn main() { let mut v = vec![1,2,3,4,5]; let first = &v[0]; // first为不可变借用 v.push(6); // push方法会传入一个可变借用,违背借用规则 println!("The first element is {}", first); }
- 借用规则在上面的例子中究竟防止什么发生?
Vec
的数据时连续不断地放在heap中的一块空间上的,当获取了其中一个元素的引用以后,该引用实际上是指向了这个元素的内存。- 但是
push
方法可能会引起vector扩容,从而导致元素被迁移到heap的另一块空间上,此时上一步获取的引用就变成悬垂指针了。 - 借用规则就是为了防止这种情况发生。
- 为什么Java的ArrayList不用担心这种情况发生?
- 要知道Java的容器中的元素都不是元素真正的对象,而是对象的引用,因此无论ArrayList扩容多少次,里面的数据被换到哪个地方,获取的元素还是个真正数据的引用,用户开始可以安全地找到真正的数据。
7.2 VecDeque
VecDeque
是双端队列,同时具有栈和队列特征的数据结构。使用前需要显式引入:use std::collections::VecDeque
。
7.2.1 基本用法
创建
VecDeque::new
创建空的VecDeque
:let mut v: VecDeque<u32> = VecDeque::new();
VecDeque::with_capacity
创建指定容量的VecDeque
:let mut v: VecDeque<u32> = VecDeque::with_capacity(10);
修改
修改包括:在队列头部或者尾部追加元素、修改指定索引的元素、删除头部或者尾部元素、删除指定索引元素。
因为需要修改双端队列,所以声明时需要将双端队列声明为mut
才能进行修改。
push_front
:在队列的头部添加新元素。push_back
:在队列的尾部添加新元素。
let mut v: VecDeque<u32> = VecDeque::new();
v.push_front(1);
v.push_back(2);
实例名[索引]
为指定索引的元素重新赋值。
let mut v: VecDeque<u32> = VecDeque::new();
v.push_front(1);
v.push_back(2);
v[1] = 0; // [1,0]
pop_front
删除并返回队列的头部元素,并返回该元素,如果列表为空,则返回None
。pop_back
删除并返回队列的尾部元素,并返回该元素,如果列表为空,则返回None
。
let mut v: VecDeque<u32> = VecDeque::new();
v.push_back(1);
v.push_back(2);
v.pop_front(); // Some(1)
v.pop_back(); // Some(2)
v.pop_back(); // None
remove
删除指定索引元素,然后后面的元素向前补齐,如果索引越界会导致程序错误:
let mut v: VecDeque<u32> = VecDeque::new();
v.push_back(0);
v.push_back(1);
v.remove(0); // Some(1)
v.remove(1); // None
访问
实例名[索引]
访问指定索引的元素:
let mut v: VecDeque<u32> = VecDeque::new();
v.push_back(0);
v.push_back(1);
v[0]; // 0
v[2]; // 越界
get
通过索引访问元素,返回值是Option<&T>
,如果元素存在则返回Some(&T)
,否则返回None
:
let mut v: VecDeque<u32> = VecDeque::new();
v.push_back(0);
v.push_back(1);
v.get(1); // Some(2)
v.get(3); // None
7.3 HashMap
HashMap<K, V>
类型储存了一个键类型K
对应一个值类型V
的映射。哈希表没有预引入,需要手动引入哈希表:use std::collection::HashMap
。
7.3.1 基本用法
创建
HashMap::new()
:创建空的HashMap
。// 注意这里由于没有其他操作,所以编译器无法推断实例的类型,因此需要手动声明泛型的类型 let mut scores = HashMap::<i32, i32>::new();
HashMap::with_capacity
创建指定容量的HashMap
:let mut map: HashMap<&str,u32> = HashMap::with_capacity(10);
修改
修改包括:插入/更新键值对、只在键没有对应值的时候插入键值对、以新旧两值的计算结果来更新键值对、删除键值对。
insert
插入/更新键值对,如果键不存在,则执行插入操作,并返回None
;如果键存在,则执行更新操作,并返回旧值:let mut map: HashMap<&str, i32> = HashMap::new(); map.insert("lisi", 86); // None map.insert("lisi", 97); // Some(86)
- 使用
entry
和or_insert
方法检查键是否有对应值,没有对应值就插入键值对,有对应值则不执行操作。entry
方法以键为参数,返回值是一个枚举类型Entry
。Entry
类型的or_insert
方法以值为参数,在键有对应值时不执行任何操作;在键没有对应值时将将只对插入HashMap。
let mut map: HashMap<&str, i32> = HashMap::new(); map.entry("zhangsan").or_insert(97); // 插入成功 map.entry("zhangsan").or_insert(98); // 没有插入和更新
- 以新旧两值的计算结果来更新键值对,可以通过迭代器的形式对HashMap中每个键值对进行迭代并更新。
let mut map: HashMap<&str, i32> = HashMap::new(); map.insert("zhangsan", 97); map.insert("lisi", 98); for (_, val) in map.iter_mut() { *val += 2; }
remove
删除并返回指定的键的值,如果键不存在则返回None
:let mut map: HashMap<&str, i32> = HashMap::new(); map.insert("zhangsan", 97); map.insert("lisi", 98); map.remove("zhangsan"); // Some(97) map.remove("wangwu"); // None
访问
实例名[键]
访问指定键的值:let mut map: HashMap<&str, i32> = HashMap::new(); map.insert("zhangsan", 97); map["zhangsan"]; // 97 map["lisi"]; // 报错
get
通过索引访问元素,返回值是Option<&T>
,如果元素存在则返回Some(&T)
,否则返回None
:let mut v: HashMap<&str, i32> = HashMap::new(); map.insert("zhangsan", 97); v.get("zhangsan"); // Some(97) v.get("lisi"); // None
遍历
通过for循环遍历:
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{}: {}", key, value);
}
7.3.2 HashMap和所有权
- 对于实现了
Copy
trait 的类型(如i32),值会被复制到HashMap中。 - 对于拥有所有权的值(如String),值将被移动,所有权会转移给HashMap。
- 如果将值的引用插入到HashMap,值本身不会移动。但是开发者需要保证在HashMap有效的期间内,被引用的值必须保持有效。
use std::collections::HashMap;
let field_name = String::from("I am key");
let field_value = String::from("I am value");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// 这里 field_name 和 field_value 不再有效
// ********** 例子2 **********
let field_name = String::from("I am key");
let field_value = String::from("I am value");
let mut map = HashMap::new();
map.insert(&field_name, &field_value);
// field_name和field_value仍然可以使用
7.3.3 Hash函数
默认情况下,HashMap使用加密功能强大的Hash函数,可以抵抗拒绝服务(DoS)攻击。默认使用的Hash函数不是最快的Hash算法,但具有更好的安全性。
可以指定不同的hasher来切换其它Hash函数:hasher是实现BuildHasher
trait的类型。
7.4 字符串
- 字符串本质是一种特殊的容器类型,由零个或多个字符组成的有限序列。
- Rust常用的字符串有两种:
str
:固定长度的字符串字面量。String
:可变长度的字符串对象。
str
是Rust内置的字符串类型,它通常以引用的形式&str
出现。字符串字面量&str
是字符的集合,代表的是不可变的UTF-8编码的字符串的引用,创建后无法再为其追加内容或更改内容。String
是Rust标准库提供的、拥有所有权的UTF-8编码的字符串类型,创建后可以为其追加内容或更改内容。String类型本质上是一个字段为Vec<u8>
类型的结构体,它把字符串内容存放在堆上,由指向堆上字节序列的指针(as_ptr
方法)、记录堆上字节序列的长度(len
方法)和堆分配的容量(capacity
方法)组成。- 由于String是一个byte的集合,所以很多vector的操作都是用于String。
- 其他类型的字符串:Rust的标准库中还包含了很多其它的字符串类型。
- 例如:
OsString
,OsStr
,CString
,CStr
。- String后缀:可获得所有权
- Str后缀:可借用的。
- 这些字符串类型可存储不同编码的文本或在内存中以不同的形式布局。
- 某些第三方库会针对存储字符串可提供更多的选项。
- 例如:
7.4.1 基础用法
创建
&str(字面量) 的创建
- 使用双引号创建字符串字面量:
let s1 = "Hello Rust!";
- 使用
as_str
方法将字符串对象转化为字符串字面量:let str = String::from("Hello Rust!"); let s2 = str.as_str();
String 的创建
String::new
创建空的字符串对象:let mut s = String::new();
String::from
根据指定字符串字面量创建字符串对象:let s = String::from("Hello Rust!");
to_string
方法将字符串字面量转换为字符串对象:let str = "Hello Rust!"; let s = str.to_string();
修改
- String字符串的常见修改包括:追加、插入、连接、替换和删除等。
- 追加(在原来的字符串上追加,不会返回新的字符串):
push
在字符串后追加字符。push_str
在字符串后面追加字符串字面量。
let mut s = String.from("Hello, "); s.push('R'); // 实际上是Vec<u8>的方法 s.push_str("ust!");
- 插入(在原来的字符串上插入,不会返回新的字符串):
insert
在指定位置插入字符。insert_str
在指定位置插入字符串。
let mut s = String::from("Hello World!"); s.insert(5, ', '); s.insert_str(7, "Rust "); println!("{}", s); // Hello, Rust World!
- 拼接(不会改变原来的字符串,会返回新的字符串):
+
或+=
运算符将两个字符串连接成一个新的字符串。(注意+=
也是返回新的字符串)- 要求运算符的右边必须是字符串字面量,但不能对两个String类型字符串使用
+
或+=
运算符。
let s1 = "Hello "; let s2 = "World!"; let s3 = s1 + s2; let s1 += s2;
- 对于复杂的或者带格式的字符串连接,可以使用
format!
宏,它对String
类型和&str
类型的字符串都适用。
let s = format("{}-{}", "Hello ", String::from("World"));
- 替换(返回新的字符串):
replace
和replacen
,前者替换所有满足条件的字符串,后者替换指定数目的字符串。
let s = String::from("aaabbbbccaadd"); let s1 = s.replace("aa", "77"); let s2 = s.replacen("aa", "77", 1);
- 删除:
pop
删除并返回字符串的最后一个字符,返回值类型是Option<char>
。如果字符串为空,则返回None。remove
删除并返回字符串中指定位置的字符,其参数是该字符的起始索引位置。remove
方法是按字节处理字符串的,如果给定的索引位置不是合法的字符边界,将会导致程序错误。truncate
删除字符串中从指定位置开始到结尾的全部字符,其参数是起始索引位置。truncate
方法也是按字节处理字符串的,如果给定的索引位置不是合法的字符边界,将会导致程序错误。clear
等于与truncate
方法的参数指定为0,删除字符串中的所有字符。
let mut s = String::from("hello rust world!"); s.pop(); // Some('!') s.remove(9); // t s.truncate(9); // hello rus s.clear(); //
访问
按字节迭代(bytes) 和 按字符迭代(chars)
- 字符串是UTF-8编码的字节序列,不能直接使用索引来访问字符。
- 字符串操作可以分为按字节处理和按字节处理两种方式:
- 按字节处理使用
bytes
方法返回按字节迭代的迭代器。 - 按字符(Unicode)处理使用
chars
方法返回按字符迭代的迭代器。
- 按字节处理使用
let s = String::from("你好 世界");
println("{}", s.len()); // 获取以字节为单位的字符串长度,实际上是Vec的len?
- 通过迭代器访问字符串的字符:
let s = String::from("你好 世界"); // 按字节迭代 let bytes = s.bytes(); for b in bytes { print!("{} | ", b); } println!(); // 按字符迭代 let chars = s.chars(); for c in chars { print!("{} | ", c); }
对String按索引的形式进行访问
- **Rust的字符串不支持索引。**按索引语法访问String的某部分会报错。因为String没有实现
Index<integer>
trait。let s = String::from("Hello"); println!("{}", s[0]); // 会报错
- Rust不允许对String进行索引的原因:
- 在
Vec<u8>
中,单个字节并不一定有任何意义(非英文情况下)。 - 索引操作理应是一个消耗
O(1)
常量时间的操作,但是String无法保证,因为String需要遍历所有内容,来确定有多少个合法字符。
- 在
7.4.2 String的内部表示
- String是对
Vec<u8>
的包装。(这是要注意的,这说明每个元素不是unicode,而是u8
)// 例子 let len = String::from("Hola").len(); println!("{}", len); // 4 let len = String::from("Здравствуйте").len(); println!("{}", len); // 24
- 字节、UTF8标量值和字形簇的关系:
- 字节:最最底层存储的数据形式,有的时候会由不止一个字节组成一个字形簇,至于这几个字节描述的是什么字形簇,则需要有编码格式来决定,这几个字节也就是一个X编码的标量值(比如某相邻两个字节组成了一个UTF8标量值,这个标量值对应的字形簇为中文中的“爱”字)。
- UTF8标量值:UTF8是一种常用的不定长存储编码格式。UTF8标量值为一个字形簇编码后的值,在rust中占一个char类型(2个字节)。
- 字形簇:即最终输出显式出来的含义,比如英文短语、中文词语等,用户能理解能直观看到。字形簇可以通过不同的编码形式存储,不同编码的数据通过对应的解码方式处理后才能显示出正确的字形簇。
- 标准库中没有获取一个字符串中每一个字形簇的方法。如果需要的话可用第三方库实现。
7.4.3 String切片
可以使用[]
和一个范围(range
)来创建String的切片。
let hello = "Здравствуйте";
let s = &hello[0..4]; // 由于String是一个Vec<u8>,所以可以认为这个操作获取的是前4个字节
println!("{}", s);
let s = &hello[0..3];
println!("{}", s); // panic,因为第3个字节并不是一个UTF-8标量值的边界。
注意:range
指定的范围表示的是字节的范围。
当range
的范围中有不完整的UTF8标量值,则会抛出panic。