8.5.0. Chapter Overview
Chapter 8 is mainly about common collections in Rust. Rust provides many collection-like data structures, and these collections can hold many values. However, the collections covered in Chapter 8 are different from arrays and tuples.
The collections in Chapter 8 are stored on the heap rather than on the stack. That also means their size does not need to be known at compile time; at runtime, they can grow or shrink dynamically.
This chapter focuses on three collections: Vector, String, and HashMap (this article).
If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.
8.5.1. What Is a HashMap?
HashMap is written as HashMap<K, V>, where K stands for key and V stands for value. A HashMap stores data as key-value pairs, with one key corresponding to one value. Many languages support this kind of collection data structure, but they may call it something else—for example, the same concept in C# is called a dictionary.
The internal implementation of HashMap uses a hash function, which determines how keys and values are stored in memory.
In a Vector, we use indices to access data. But sometimes you want to look up data by key, and the key can be any data type, instead of by index, or you may not know which index the data is at. In that case, you can use a HashMap.
Note that HashMap is homogeneous, which means that all keys in one HashMap must be the same type, and all values must be the same type.
8.5.2. Creating a HashMap
- Because
HashMapis not used as often, Rust does not include it in the prelude. Before using it, you need to importHashMapby writinguse std::collections::HashMap;at the top of the file. - To create an empty
HashMap, use theHashMap::new()function. - To add data, use the
insert()method.
Example:
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<String, i32> = HashMap::new();
}
Here a variable named scores is created to store a HashMap. Because Rust is a strongly typed language, it must know what data types you are storing in the HashMap. Since there is no surrounding context for the compiler to infer from, you must explicitly declare the key and value types when you declare the HashMap. In this code, the keys of scores are set to String, and the values are set to i32.
Of course, if you later add data to this HashMap, Rust will infer the key and value types from the inserted data. Data is added with the insert() method. Example:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
}
Because a key-value pair is inserted into scores on line 5, and the key String::from("dev1ce") is of type String while the value 0 is of type i32 (Rust’s default integer type is i32), the compiler will infer that scores is a HashMap<String, i32>, so there is no need to explicitly declare the type on the fourth line.
8.5.3. Combining Two Vectors into One HashMap
On a Vector whose element type is a tuple, you can use the collect method to build a HashMap. Put another way, if you have two Vectors and all of the values in them have a one-to-one correspondence, you can use collect to put the data from one Vector into the keys and the data from the other into the values of a HashMap. Example:
use std::collections::HashMap;
fn main() {
let player = vec![String::from("dev1ce"), String::from("Zywoo")];
let initial_scores = vec![0, 100];
let scores: HashMap<_, _> = player.iter().zip(initial_scores.iter()).collect();
}
- The
playerVectorstores player names, and its elements are of typeString. - The
initial_scoresVectorstores the score corresponding to each player. -
player.iter()andinitial_scores.iter()are iterators over the twoVectors. Using.zip()creates a sequence of tuples, andplayer.iter().zip(initial_scores.iter())creates tuples with elements fromplayerfirst and elements frominitial_scoressecond. If you want to swap the order of the elements, you can simply swap the two iterators in the code. Then.collect()is used to convert the tuples into aHashMap. - One last thing to note is that
.collect()supports conversion into many data structures. If you do not explicitly declare its type when writing the code, the program will fail. Here the type is specified asHashMap<_, _>. The two data types inside<>can be inferred by the compiler from the code, that is, from the twoVectortypes, so you can use_as a placeholder and let it infer the types automatically.
8.5.4. HashMap and Ownership
For data types that implement the Copy trait, such as i32 and most simple data types, the value is copied into the HashMap, and the original variable remains usable. For types that do not implement Copy, such as String, ownership is transferred to the HashMap.
If you insert references into a HashMap, the value itself is not moved. During the lifetime of the HashMap, the referenced values must remain valid.
8.5.5. Accessing Values in a HashMap
You can access values with the get method. The get method takes a HashMap key as its argument, and it returns an Option<&V> enum. Example:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
scores.insert(String::from("Zywoo"), 100);
let player_name = String::from("dev1ce");
let score = scores.get(&player_name);
match score {
Some(score) => println!("{}", score),
None => println!("Player not found"),
};
}
- First, an empty
HashMapcalledscoresis created, and then two key-value pairs,("dev1ce", 0)and("Zywoo", 100), are inserted usinginsert. The key type isString, and the value type isi32. - Then a
Stringvariable namedplayer_nameis declared with the value"dev1ce". - Next, the
getmethod on theHashMapis used to look up the value corresponding to theplayer_namekey inscores(&means reference). But becausegetreturns anOptionenum, theOptionvalue is first assigned toscoreand then unwrapped later. - Finally, a
matchexpression is used to handlescore. If the corresponding value is found, thescoreenum is theSomevariant, and the value associated withSomeis bound toscoreand then printed. If nothing is found, thescoreenum is theNonevariant, and"Player not found"is printed.
Output:
0
8.5.6. Iterating Over a HashMap
You usually iterate over a HashMap with a for loop. Example:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
scores.insert(String::from("Zywoo"), 100);
for (k, v) in &scores {
println!("{}: {}", k, v);
}
}
This for loop uses a reference to the HashMap, namely &scores, because after iterating you usually still want to keep using the HashMap. Using a reference means you do not lose ownership. The (k, v) on the left is pattern matching: the first value is the key, which is assigned to k, and the second is the value, which is assigned to v.
Output:
Zywoo: 100
dev1ce: 0