Rust的集合(collections)

Rust的标准库里有一系列数据结构,称作集合(collections)。这里讲解三种被广泛使用的集合:vector、string和hashmap。

Vector🔗

vector是一个动态数组,可以存储多个值,并且值类型相同。vector是可变的,可以添加、删除和修改元素。

创建vector🔗

创建一个空的vector:

let v: Vec<i32> = Vec::new();

这里创建了个叫v的vector,类型是i32,初始为空。

创建一个包含初始值的vector:

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

这里使用了vec!宏,这个宏会根据提供的类型判断出vector的类型,并创建一个包含初始值的vector。

更新vector🔗

向vector中添加元素:

let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
...

这里使用了push方法向vector中添加元素。

读取vector中的元素🔗

读取vector中的元素有两种方式:索引和get方法。

let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {}", third);

let third: Option<&i32> = v.get(2);
match third {
    Some(third) => println!("The third element is {}", third),
    None => println!("There is no third element."),
}

这里分别使用索引和get两种方法来读取vector中的元素,可以看到get方法的返回值是一个Option类型,这是因为索引可能超出vector的范围,所以需要使用Option来处理这种情况。

❗ 重要

注意:使用索引访问vector中的元素时,如果索引超出了vector的范围,会导致panic,而使用get方法访问时,如果索引超出了vector的范围,会返回None。

遍历vector🔗

可以使用for遍历vector中的元素:

let v = vec![1, 2, 3, 4, 5];
for i in &mut v {
    println!("{i}");
}

for i in &mut v {
    *i += 50;
}

这里分别使用了for遍历vector中的元素,以及for遍历vector中的可变引用,并修改了元素。

使用枚举来存储多个类型的值🔗

可以使用枚举来存储多个类型的值,例如:

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

这里定义了一个枚举SpreadsheetCell,它有三个变体:Int、Float和Text,分别对应i32、f64和String类型。然后使用vec!宏创建了一个包含SpreadsheetCell类型的vector。

String🔗

String是一个可变的、动态分配的UTF-8编码的字符串,是一个vector的封装,可以存储多个字符。

创建String🔗

创建一个空的String:

let mut s = String::new();

这里创建了一个叫s的String,初始为空。

创建带初始值的字符串:

let s = String::from("initial contents");
let s = "initial contents".to_string();

这里分别使用了String::from和to_string方法创建了一个带初始值的String。

ℹ️ 注意

注意:字符串是UTF-8编码的,所以可以存储任何Unicode字符。

更新String🔗

使用push_str方法添加slice到String中,使用push方法添加单个字符到String中:

let mut s = String::from("foo");
s.push_str("bar");
s.push('l');

这里分别使用了push_str和push方法向String中添加了slice和字符。

使用’+’或format!宏连接字符串🔗

可以使用+连接字符串:

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used

这里使用了+运算符连接了两个String,注意s1被移动了,所以不能再使用s1。s2是一个引用,在这里会转移成&str,所以可以连接,之后也可以继续使用。

在连接多个字符串时,可以使用format!宏:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

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

这里使用了format!宏连接了三个String,并返回了一个新的String,format!宏和println!宏类似,只不过不是打印而是返回结果。

String索引🔗

Rust中的String不可索引,String是Vec的封装,由于使用UTF-8编码,一个字符可能占用多个字节,所以不能像数组那样使用索引访问字符串中的字符。

ℹ️ 注意

另外:索引操作预期需要常数时间O(1),但Rust计算一个字符需要遍历整个字符串来确认有效字符。

使用索引创建字符串slice是可行的:

let hello = "Здравствуйте";
let s = &hello[0..4];

这里使用了索引创建了一个字符串slice,s的值是“Зд“,注意索引是从0开始的,所以s的长度是4。(这些字符每个占用2个字节)

🔥 谨慎

如果获取&hello[0..1],会报错,因为第一个字符是两个字节。

遍历String🔗

遍历String需要明确表示的是字符还是字节,可以使用chars方法遍历字符,使用bytes方法遍历字节:

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

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

这里分别使用了chars和bytes方法遍历了字符串中的字符和字节,输出如下内容:

З
д
208
151
208
180

这里使用了chars方法遍历了字符串中的字符,使用bytes方法遍历了字符串中的字节。

HashMap🔗

HashMap<K,V>储存了一个键值对的映射。哈希map用于需要任何类型作为key来寻找数据的情况,这里简单介绍HashMap。

创建HashMap🔗

创建一个空的HashMap:

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

这里使用了HashMap::new方法创建了一个空的HashMap,然后使用insert方法向HashMap中添加了两个键值对。
像vector一样,hashmap的键类型和值类型必须相同,这里使用了String和i32类型。

ℹ️ 注意

注意:要先使用use std::collections::HashMap引入HashMap,由于HashMap相对Vector和String使用频率较低,所以没被prelude自动引用。

访问HashMap中的值🔗

使用get方法访问HashMap中的值:

use std::collections::HashMap;

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).copied().unwrap_or(0);

get方法返回一个Option类型,如果键存在,则返回Some值,否则返回None,这里使用了copied方法获取一个Option而不是Option<&i32>,接着用unwrap_or(0)方法获取值,如果Option是None,则返回0。

遍历HashMap中的值也可以用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);
}

这里使用了for遍历了HashMap中的键值对,并打印了键和值,输出如下:

Blue: 10
Yellow: 50

hashmap和所有权🔗

对于实现Copy trait的类型,例如i32,HashMap会将值复制到哈希map中,对于String这样有所有权的值,HashMap会将值的移动并获取所有权:

use std::collections::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);

这里创建了一个String类型的field_name和field_value,然后使用insert方法将它们插入到HashMap中,之后field_name和field_value的所有权被移动到了HashMap中,所以不能再使用它们。
如果将值的引用插入到HashMap中,那么值不会被移动,但是这些引用所指的值必须至少在hashmap有效时有效。

更新HashMap🔗

每一个键只能关联一个值,所以在处理一个键时,需要先检查键是否存在,如果存在则更新值,如果不存在则插入键值对。

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);

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

这里先插入了一个键值对,然后又插入了一个键值对,但是键是相同的,所以第二个键值对会覆盖第一个键值对,输出如下:

{"Blue": 25}

使用entry可以更简洁的更新HashMap:

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);

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

entry返回值是一个枚举Entry,代表可能存在也可能不存在的值。使用Entry的or_insert方法在键值存在时返回这个值的可变引用,如果不存在则将参数作为值插入,并返回这个新值的可变引用。这里使用了or_insert方法更新了HashMap中的值,输出如下:

{"Yellow": 50, "Blue": 10}

如果要根据旧值更新一个值,考虑如下代码:

//这里是计数一个字符串中每一单词出现的次数
use std::collections::HashMap;

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);
    *count += 1;
}

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

这里使用了entry方法获取了键的可变引用,然后使用*count += 1更新了值,输出如下:

{"hello": 1, "world": 2, "wonderful": 1}

其中,split_whitespace方法返回一个由空格分割text值子slice的迭代器,or_insert方法返回一个可变引用,所以需要使用*count来更新值。

总结🔗

本文介绍了Rust中的Vector、String和HashMap,Vector是动态数组,String是字符串,HashMap是哈希表,它们都是Rust中常用的集合类型,其中HashMap使用时要用use std::collections::HashMap引入,而Vector和String可以直接使用。