Rust Collections

tzutoo
10 min readMar 17, 2023

--

Introduction

Rust is a systems programming language known for its performance, reliability, and strong memory safety guarantees. One of the key features of Rust is its collection types, which provide efficient and safe ways to manage and manipulate data. In this article, we will explore some of the most commonly used Rust collections and their use cases, as well as common operations, chain manipulations, and type conversions.

Collection Types

In this section, we will discuss five commonly used Rust collection types:

Vec

HashMap

HashSet

BTreeMap

LinkedList

1. Vec

When to use it

Vec is a growable array type, which means that it can change its size at runtime. You should use a Vec when you need a dynamic array with fast random access and when the order of elements is important.

When not to use it

Avoid using Vec if you need a collection with constant-time insertions or deletions at arbitrary positions or if you require a fixed-size array.

Example

// Create a new empty Vec
let mut numbers: Vec<i32> = Vec::new();
// Add elements to the Vec
numbers.push(1);
numbers.push(2);
numbers.push(3);
// Access elements by index
let second_element = numbers[1];
println!("The second element is: {}", second_element);
// Iterate over the elements
for number in &numbers {
println!("Number: {}", number);
}

Output:

The second element is: 2
Number: 1
Number: 2
Number: 3

In this example, we create a mutable Vec called numbers, add elements to it, access an element by index, and iterate through the elements. This demonstrates how Vec provides an efficient and easy-to-use interface for managing dynamic arrays.

2. HashMap

When to use it

HashMap is a collection that stores key-value pairs in an unordered manner. Use HashMap when you need to efficiently access, insert, or delete elements by a unique key.

When not to use it

Avoid using HashMap if you need to maintain the order of elements or if you require a sorted collection.

Example

use std::collections::HashMap;

// Create a new empty HashMap
let mut capitals = HashMap::new();
// Add key-value pairs to the HashMap
capitals.insert("USA", "Washington, D.C.");
capitals.insert("Germany", "Berlin");
capitals.insert("Japan", "Tokyo");
// Access value by key
if let Some(capital) = capitals.get("Germany") {
println!("The capital of Germany is: {}", capital);
}
// Iterate over key-value pairs
for (country, capital) in &capitals {
println!("{}: {}", country, capital);
}

Output:

The capital of Germany is: Berlin
USA: Washington, D.C.
Germany: Berlin
Japan: Tokyo

In this example, we create a HashMap called capitals, add key-value pairs, access a value by its key, and iterate through the key-value pairs. This demonstrates how HashMap is a powerful collection for efficient key-value storage and retrieval.

3. HashSet

When to use it

HashSet is an unordered collection that stores unique elements. Use HashSet when you need to efficiently check for the presence of an element, ensure uniqueness, or perform set operations like union and intersection.

When not to use it

Avoid using HashSet if you need to maintain the order of elements or if you require a collection with key-value pairs.

Example

use std::collections::HashSet;

// Create a new empty HashSet
let mut colors = HashSet::new();
// Add elements to the HashSet
colors.insert("red");
colors.insert("blue");
colors.insert("green");
// Check for the presence of an element
let contains_red = colors.contains("red");
println!("Contains red? {}", contains_red);
// Iterate over the elements
for color in &colors {
println!("Color: {}", color);
}

Output:

Contains red? true
Color: green
Color: blue
Color: red

In this example, we create a HashSet called colors, add elements to it, check for the presence of an element, and iterate through the elements. This demonstrates how HashSet can efficiently manage unique elements and perform set operations.

4. BTreeMap

When to use it

BTreeMap is a sorted collection that stores key-value pairs in a binary tree structure. Use BTreeMap when you need a collection with sorted keys or when you require range-based queries.

When not to use it

Avoid using BTreeMap if you don't need the keys to be sorted or if you prioritize insertion and retrieval performance, as HashMap is generally faster for these operations.

Example

use std::collections::BTreeMap;

// Create a new empty BTreeMap
let mut scores = BTreeMap::new();
// Add key-value pairs to the BTreeMap
scores.insert("Alice", 90);
scores.insert("Bob", 80);
scores.insert("Carol", 70);
// Access value by key
if let Some(score) = scores.get("Alice") {
println!("Alice's score is: {}", score);
}
// Iterate over key-value pairs in sorted order
for (name, score) in &scores {
println!("{}: {}", name, score);
}

Output:

Alice's score is: 90
Alice: 90
Bob: 80
Carol: 70

In this example, we create a BTreeMap called scores, add key-value pairs, access a value by its key, and iterate through the key-value pairs in sorted order. This demonstrates how BTreeMap is useful for managing sorted collections and performing range-based queries.

Please let me know when you would like to proceed to the next section by using the /next command.

5. LinkedList

When to use it

LinkedList is a doubly-linked list that provides constant-time insertions and deletions at the beginning and end of the list. Use LinkedList when you need a collection with fast insertions and deletions at both ends, or when you have large elements and need to minimize reallocation costs.

When not to use it

Avoid using LinkedList if you need a collection with fast random access, as accessing elements by index is inefficient due to the need to traverse the list.

Example

use std::collections::LinkedList;

// Create a new empty LinkedList
let mut names = LinkedList::new();
// Add elements to the LinkedList
names.push_back("Alice");
names.push_back("Bob");
names.push_front("Carol");
// Remove elements from the LinkedList
let first = names.pop_front().unwrap();
println!("First removed: {}", first);
// Iterate over the elements
for name in &names {
println!("Name: {}", name);
}

Output:

First removed: Carol
Name: Alice
Name: Bob

In this example, we create a LinkedList called names, add elements to it, remove elements, and iterate through the elements. This demonstrates how LinkedList can efficiently manage insertions and deletions at both ends of the list.

Common Operations

Now that we have covered the five most commonly used Rust collections, let’s proceed to discuss common operations, chain manipulations, and type conversions.

Chain Manipulations

Chain manipulations refer to applying a sequence of operations or transformations to a collection. In Rust, the Iterator trait provides a variety of methods for performing chain manipulations. These methods are applicable to most collection types, including Vec, HashMap, HashSet, BTreeMap, and LinkedList. Here are some examples of chain manipulations using the five collection types we discussed:

1. Vec

let numbers = vec![1, 2, 3, 4, 5];

// Filter out odd numbers and square the remaining even numbers
let even_squares: Vec<_> = numbers
.iter()
.filter(|&n| n % 2 == 0)
.map(|n| n * n)
.collect();
println!("Even squares: {:?}", even_squares);

Output:

Even squares: [4, 16]

In this example, we filter out odd numbers from a Vec, square the remaining even numbers, and store the result in a new Vec.

2. HashMap

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert("Alice", 90);
scores.insert("Bob", 80);
scores.insert("Carol", 70);
// Filter out entries with a score below 80 and create a new HashMap
let high_scores: HashMap<_, _> = scores
.iter()
.filter(|&(_, score)| *score >= 80)
.map(|(name, score)| (name.to_string(), *score))
.collect();
println!("High scores: {:?}", high_scores);

Output:

High scores: {"Alice": 90, "Bob": 80}

In this example, we filter out entries with a score below 80 from a HashMap and create a new HashMap containing only the high scores.

3. HashSet

use std::collections::HashSet;

let set1: HashSet<_> = [1, 2, 3, 4, 5].iter().cloned().collect();
let set2: HashSet<_> = [4, 5, 6, 7, 8].iter().cloned().collect();
// Compute the intersection of two sets
let intersection: HashSet<_> = set1.intersection(&set2).cloned().collect();
println!("Intersection: {:?}", intersection);

Output:

Intersection: {4, 5}

In this example, we compute the intersection of two HashSets and create a new HashSet containing the result.

4. BTreeMap

use std::collections::BTreeMap;

let mut scores = BTreeMap::new();
scores.insert("Alice", 90);
scores.insert("Bob", 80);
scores.insert("Carol", 70);

// Map scores to grades and create a new BTreeMap
let grades: BTreeMap<_, _> = scores
.iter()
.map(|(name, score)| {
let grade = if *score >= 90 {
"A"
} else if *score >= 80 {
"B"
} else {
"C"
};
(name.to_string(), grade.to_string())
})
.collect();

println!("Grades: {:?}", grades);

Output:

Grades: {"Alice": "A", "Bob": "B", "Carol": "C"}

In this example, we map the scores to grades and create a new BTreeMap containing the names as keys and the corresponding grades as values.

5. LinkedList

use std::collections::LinkedList;

let mut names = LinkedList::new();
names.push_back("Alice");
names.push_back("Bob");
names.push_back("Carol");
names.push_back("Dave");
// Create a new LinkedList containing only names starting with 'C'
let names_starting_with_c: LinkedList<_> = names
.iter()
.filter(|name| name.starts_with('C'))
.cloned()
.collect();
println!("Names starting with 'C':");
for name in names_starting_with_c {
println!("{}", name);
}

Output:

Names starting with 'C':
Carol

In this example, we filter out names that do not start with ‘C’ from a LinkedList and create a new LinkedList containing only the names that start with 'C'.

Type Conversions

Now that we have discussed chain manipulations using the five collection types, let’s proceed to the topic of type conversions.

Converting between different collection types is a common operation in Rust. The collect() function is a powerful and versatile method provided by the Iterator trait, which allows you to convert one collection type into another. Here are some examples of type conversions between the five collection types we discussed:

1. Vec to HashMap

let words = vec!["apple", "banana", "orange"];

// Create a HashMap where keys are words and values are their lengths
let word_lengths: HashMap<_, _> = words
.iter()
.map(|word| (word, word.len()))
.collect();
println!("Word lengths: {:?}", word_lengths);

Output:

Word lengths: {"apple": 5, "banana": 6, "orange": 6}

In this example, we convert a Vec of words into a HashMap where keys are words and values are their lengths.

2. HashMap to Vec

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert("Alice", 90);
scores.insert("Bob", 80);
scores.insert("Carol", 70);
// Create a Vec of names sorted by score
let sorted_names: Vec<_> = scores
.iter()
.sorted_by_key(|&(_, score)| score)
.map(|(name, _)| name)
.collect();
println!("Sorted names by score: {:?}", sorted_names);

Output:

Sorted names by score: ["Carol", "Bob", "Alice"]

In this example, we convert a HashMap of names and scores into a Vec of names sorted by score.

3. HashSet to Vec

use std::collections::HashSet;

let set: HashSet<_> = [1, 2, 3, 4, 5].iter().cloned().collect();
// Create a Vec containing elements from the HashSet
let vec: Vec<_> = set.into_iter().collect();
println!("Vec from HashSet: {:?}", vec);

Output:

Vec from HashSet: [1, 2, 3, 4, 5]

In this example, we convert a HashSet of integers into a Vec containing the same elements.

4. BTreeMap to Vec

use std::collections::BTreeMap;

let mut scores = BTreeMap::new();
scores.insert("Alice", 90);
scores.insert("Bob", 80);
scores.insert("Carol", 70);
// Create a Vec of names sorted by score
let sorted_names: Vec<_> = scores
.iter()
.map(|(name, _)| name)
.collect();
println!("Sorted names by score: {:?}", sorted_names);

Output:

Sorted names by score: ["Alice", "Bob", "Carol"]

In this example, we convert a BTreeMap of names and scores into a Vec of names sorted by score.

5. LinkedList to Vec

use std::collections::LinkedList;

let mut names = LinkedList::new();
names.push_back("Alice");
names.push_back("Bob");
names.push_back("Carol");
// Create a Vec containing elements from the LinkedList
let vec: Vec<_> = names.into_iter().collect();
println!("Vec from LinkedList: {:?}", vec);

Output:

Vec from LinkedList: ["Alice", "Bob", "Carol"]

In this example, we convert a LinkedList of names into a Vec containing the same elements.

Some Tips

  1. Start with an iterator: To begin chaining operations, you’ll first need an iterator. Most collections in Rust have methods like iter(), iter_mut(), and into_iter() that can provide an iterator.
  2. Use closures effectively: Many iterator methods, such as map(), filter(), and fold(), accept closures as arguments. Make use of closures to apply transformations, filter elements, or accumulate values.
  3. Laziness is your friend: Iterator methods like map() and filter() are lazy, meaning they don't execute until you consume the iterator (e.g., by calling collect() or using a for loop). This allows you to build up complex chains of operations without sacrificing performance.
  4. Collect the result: Use the collect() method to convert the final iterator back into a collection type. You can explicitly specify the type of the resulting collection, or let Rust infer the type based on the context.
  5. Chain methods from other traits: Iterator methods can be combined with methods from other traits, such as Iterator::sorted_by_key() from the itertools crate. This allows for even more powerful and expressive chains of operations.
  6. Be aware of the performance implications: While chaining operations can lead to clean and concise code, keep in mind that some iterator methods can have performance implications. For example, using collect() to create an intermediate collection can lead to extra allocations, and chaining many flat_map() calls may result in nested loops.
  7. Consider using method chaining for more complex transformations: If you find yourself applying multiple transformations in sequence, method chaining can make your code more readable and maintainable. By chaining methods, you can express a series of transformations as a single, coherent pipeline, making it easier to understand the overall process.
  8. Test and benchmark your chained operations: To ensure that your chained operations perform as expected, write tests and benchmarks to measure their correctness and performance. This will help you identify potential bottlenecks or inefficiencies in your code.
  9. Leverage the power of the Option and Result types: Many iterator methods work seamlessly with Option and Result types, allowing you to handle errors and optional values gracefully in your chained operations. For example, you can use filter_map() to filter and transform elements while also handling None values, or try_fold() to accumulate values while propagating errors.
  10. Keep your chains readable: While chaining operations can lead to concise code, excessively long or complex chains can become difficult to read and understand. Break up long chains into smaller, well-named intermediate variables or functions to keep your code clear and maintainable.

By following these tips, you can harness the power of chain operations in Rust to write efficient, expressive, and readable code. Remember to always consider the specific requirements of your use case and strike a balance between performance and readability when applying these techniques.

Conclusion

In this article, we explored Rust collections, including their types, common operations, chain manipulations, and type conversions. We discussed the following collection types:

  1. Vec
  2. HashMap
  3. HashSet
  4. BTreeMap
  5. LinkedList

We demonstrated chain manipulations using iterators and various iterator methods for filtering, mapping, and reducing. We also showcased how to convert between different collection types using the versatile collect() method.

By utilizing these concepts, you can write efficient, expressive, and readable Rust code when working with collections. Remember to strike a balance between performance and readability, and always consider the specific requirements of your use case.

As you continue to work with Rust collections, don’t hesitate to consult the Rust documentation and explore additional iterator methods, traits, and third-party crates to further enhance your code.

References

  1. The Rust Programming Language (Book): Chapter 8 — Common Collections
  2. Rust Standard Library Documentation: Collections
  3. Rust Standard Library Documentation: Iterator
  4. The Rust Programming Language (Book): Chapter 13 — Functional Language Features
  5. itertools crate: Iterator tools for Rust

--

--