Rust's standard library includes several useful data structures called collections. Unlike other data types that represent a single value, collections can store multiple values, and their data is stored on the heap. This allows the size of the collections to grow or shrink at runtime.
Vectors allow you to store multiple values in a contiguous block of memory, where all the values are stored next to each other. The vector variable itself is stored on the stack, while the actual data it references is stored on the heap.
fn main() {
let mut vec = Vec::new(); // Create a new empty vector
vec.push(1); // Add values to the vector
vec.push(2);
vec.push(3);
println!("{:?}", vec); // Print the contents of the vector
}Explanation:
- We create a mutable empty vector
vecusingVec::new(). - We use
vec.push()to add values (1,2,3) to the vector. - Finally, we print the vector using the
println!macro with"{:?}"to format the output for debugging.
Q: Write a function that takes a vector as input and returns a vector containing only even values:
fn main() {
let mut vec = Vec::new(); // Initialize a new vector
vec.push(1);
vec.push(2);
vec.push(3);
println!("{:?}", filter_even(vec)); // Call the filter_even function
}
fn filter_even(vec: Vec<i32>) -> Vec<i32> { // Define the filter_even function
let mut new_vec = Vec::new();
for val in vec {
if val % 2 == 0 { // Check if the value is even
new_vec.push(val); // Add even values to new_vec
}
}
return new_vec; // Return the new vector containing only even values
}Explanation:
- The
filter_evenfunction takes a vectorvecof integers as input. - We iterate over the vector, and for each element, we check if it's even using the modulus operator (
val % 2 == 0). - If it is, we push it to a new vector
new_vec, which is returned as the result.
HashMaps in Rust store key-value pairs, similar to objects in JavaScript, dictionaries in Python, or hashmaps in Java. They allow for efficient retrieval of values based on a key.
insert: Adds a key-value pair to the HashMap.get: Retrieves a value associated with a key.remove: Removes a key-value pair.clear: Clears all the entries in the HashMap.
use std::collections::HashMap;
fn main() {
let mut users: HashMap<String, u32> = HashMap::new(); // Create a new HashMap
users.insert(String::from("tushar"), 20); // Insert key-value pairs
users.insert(String::from("harkirat"), 21);
let user1 = users.get("tushar"); // Get the value associated with the key "tushar"
println!("{}", user1.unwrap()); // Print the value (unwrap to handle Option)
}Explanation:
- We create a
HashMapnameduserswhere the key is aStringand the value is au32. - We insert two key-value pairs:
"tushar" -> 20and"harkirat" -> 21. - We retrieve the value associated with the key
"tushar"using thegetmethod and print it.
Q: Write a function that takes a vector of tuples (each containing a key and a value) and returns a HashMap where the keys are unique, and the values are vectors of all the corresponding values for each key:
use std::collections::HashMap;
fn group_values_by_key(pairs: Vec<(String, i32)>) -> HashMap<String, Vec<i32>> { // Update the return type
let mut map: HashMap<String, Vec<i32>> = HashMap::new(); // Create a HashMap to store vectors
for (key, value) in pairs {
map.entry(key).or_insert(Vec::new()).push(value); // Group values by key
}
return map;
}
fn main() {
let pairs: Vec<(String, i32)> = vec![
(String::from("tushar"), 20),
(String::from("harkirat"), 21),
(String::from("tushar"), 30), // Example with duplicate key "tushar"
];
let grouped_pairs = group_values_by_key(pairs); // Call the function
for (key, values) in grouped_pairs {
println!("{}: {:?}", key, values); // Print each key with its vector of values
}
}Explanation:
- We define a function
group_values_by_keythat takes a vector of tuples. - It returns a
HashMapwhere the keys are unique and the values are vectors containing all associated values for each key. - We use
entry(key).or_insert()to insert a new vector if the key doesn’t exist and thenpushthe value into the vector.
The Iterator pattern allows you to perform some task on a sequence of items in turn. An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished. When you use iterators, you don't have to reimplement that logic yourself.
In Rust, iterators are lazy, meaning they have no effect until you call methods that consume the iterator to use it up. For example, the code below creates an iterator over the items in the vector v by calling the iter method defined on Vec<T>. This code by itself doesn't do anything useful:
let v = vec![1, 2, 3];
let v_iter = v.iter();The iterator is stored in the v_iter variable.
-
Iterating using
forloops:fn main() { let nums = vec![1, 2, 3]; for value in nums { println!("{}", value); } }
Explanation:
- This directly uses
into_iterunder the hood, which takes ownership of the vector. - Once the loop is done,
numscan no longer be used.
- This directly uses
-
Iterating after creating an
iterator:fn main() { let nums = vec![1, 2, 3]; let iter = nums.iter(); // returns a struct of type Iter<_, i32> for value in iter { println!("{}", value); } }
Explanation:
iter()creates an iterator that borrows each element immutably.- You can still use the original vector after this since it wasn’t consumed.
-
IterMut
fn main() { let mut v1 = vec![1, 2, 3]; let v1_iter = v1.iter_mut(); // this will help you change the internal values of the vector for val in v1_iter { *val += 2; } println!("{:?}", v1); }
Explanation:
iter_mut()allows mutable access to each element in the vector.- The values are modified in-place.
- Since you're borrowing mutably, the original vector remains usable after the loop.
-
Iterating using
.next()fn main() { let nums = vec![1, 2, 3]; let mut iter = nums.iter(); while let Some(val) = iter.next() { print!("{}", val); } }
Explanation:
next()manually retrieves each item from the iterator, one at a time.- It returns
Some(value)until all items are consumed, then returnsNone. - You must make the iterator
mutbecausenext()changes its internal state.
-
IntoIter
fn main() { let nums = vec![1, 2, 3]; let iter = nums.into_iter(); for value in iter { println!("{}", value); } }
Explanation:
into_iter()takes ownership of the collection.- The original vector
numsis no longer usable after this. - Best used when you don't need the original collection again.
- You no longer need the original collection
- You want performance benefits by transferring ownership (avoiding references)
- iter: Use this if you want immutable references to the inner values and don’t want to transfer ownership
- iter_mut: Use this if you want mutable references to the inner values and still want to keep the original collection
- into_iter: Use this if you want to move the values and don’t need the original collection afterward
The two programs below are functionally the same (the for loop uses into_iter under the hood):
-
fn main() { let v1 = vec![1, 2, 3]; for val in v1 { println!("{}", val); } }
-
fn main() { let v1 = vec![1, 2, 3]; let iter = v1.into_iter(); for val in iter { println!("{}", val); } }
Methods that call next() are called consuming adapters, because calling them uses up the iterator.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum(); // .sum() consumes v1_iter
assert_eq!(total, 6);
let sum2: i32 = v1_iter.sum(); // Error: v1_iter was already consumed
}Explanation:
- The
sum()method consumes the iterator and returns the total. - Once consumed, the iterator can't be used again.
- This ensures safe handling of ownership.
These are methods defined on the Iterator trait that don’t consume the iterator. Instead, they produce new iterators by modifying the original one.
-
map
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; let iter = v1.iter(); let iter2 = iter.map(|x| x + 1); for x in iter2 { println!("{}", x); } }
Explanation:
map()transforms each item using the closure.- It's lazy: nothing is executed until you use the iterator.
- In this example, each element is incremented by 1.
-
filter
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; let iter = v1.iter(); let iter2 = iter.filter(|x| *x % 2 == 0); for x in iter2 { println!("{}", x); } }
Explanation:
filter()skips values that don’t satisfy the condition.- It's also lazy — values are only evaluated during iteration.
- Here, only even numbers are printed.
Q: Write the logic to first filter out all odd values then double each value and create a new vector:
fn filter_and_map(v: Vec<i32>) -> Vec<i32> {
// Create a lazy iterator:
// 1. Filter: retain only odd values
// 2. Map: double each retained value
let new_iter = v
.iter()
.filter(|x| *x % 2 == 1) // Keep only odd numbers
.map(|x| x * 2); // Double each retained value
// Collect the results from the iterator into a new vector
let new_vec: Vec<i32> = new_iter.collect();
return new_vec; // Return the final vector
}
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
let ans = filter_and_map(v1);
println!("{:?}", ans); // Output: [2, 6]
}Explanation:
- The
filter_and_mapfunction takes a vector of integers as input. - We use an iterator to filter out all odd numbers (
x % 2 == 1) and double each retained number usingmap. - Since
iter()returns immutable references (&i32), operations infilterandmapare done on references. - Finally, we collect the transformed values into a new vector using
.collect()and return it.