Rust Deep Dive Part 2 - Stack, Heap, and Collections That Actually Make Sense

Table of contents
- Why am I learning about memory layout and collections ??
- The Great Memory Mystery - Stack vs Heap Explained
- Vectors - The Dynamic Arrays That Don't Suck
- HashMaps - Key-Value Pairs Done Right
- Iterators - The Functional Programming Magic
- Different Types of Iterators - When to Use What ??
- Putting It All Together - A Real Example
- What I Learned Today - The Memory and Collections Picture
- Something to Note

28th August, 2025
Hey there! Welcome back to my Rust learning journey :)) If you missed Part 1 where I freaked out about ownership and borrowing, go check that out first - https://deadmanabir.hashnode.dev/started-deep-diving-into-rust-today-heres-what-blew-my-mind-part-1.
Today I'm diving into something that confused the hell out of me initially - where does Rust actually store my data? And how do I work with collections like vectors and hashmaps without pulling my hair out?
After spending another full day wrestling with Rust's memory model and collections, I finally had those "OHHHHH" moments that made everything click. Let me walk you through what I discovered, because trust me, once you understand this stuff, you'll start appreciating why Rust is so obsessed with safety and performance.
But first, the usual question...
Why am I learning about memory layout and collections ??
Simple answer: because I kept getting confused about when my variables lived on the stack versus the heap, and I needed to actually DO something useful with Rust beyond just printing "Hello World." Plus, every real program needs to work with collections of data, right? Whether it's a list of users, a bunch of configuration settings, or just keeping track of things - you need vectors, hashmaps, and ways to iterate through them efficiently.
My goal today was clear: "I want to understand where my data lives in memory and how to work with collections like I would in Python or JavaScript, but with Rust's safety guarantees."
The Great Memory Mystery - Stack vs Heap Explained
Okkay, this is where things get interesting. Coming from high-level languages, I never really cared about stack vs heap. JavaScript? Python? They handle all that for you. But Rust makes you think about it, and honestly, it's not as scary as it sounds.
Here's the simple rule that changed everything for me:
Stack = Known size, fast access, automatically cleaned up
Heap = Unknown/dynamic size, slightly slower, needs explicit management
Let me show you with actual code:
fn main() {
// STACK VARIABLES - size known at compile time
let number: i32 = 42; // 4 bytes, goes to stack
let flag: bool = true; // 1 byte, goes to stack
let array: [i32; 5] = [1, 2, 3, 4, 5]; // 20 bytes (5 * 4), goes to stack
println!("Stack variables: {}, {}, {:?}", number, flag, array);
// HEAP VARIABLES - size can change during runtime
let mut dynamic_string = String::from("Hello"); // Goes to heap!
dynamic_string.push_str(" World"); // Can grow, needs heap
let mut numbers_list = Vec::new(); // Vector goes to heap!
numbers_list.push(1);
numbers_list.push(2); // Can grow dynamically
println!("Heap variables: {}, {:?}", dynamic_string, numbers_list);
// When this function ends:
// Stack variables automatically disappear (fast!)
// Heap variables get cleaned up by Rust's ownership system
}
The key insight here is that if Rust knows exactly how much memory something needs at compile time, it goes on the stack. If the size can change (like a String that can grow, or a Vec that can have more elements added), it goes on the heap.
Think of the stack like your desk drawer - you know exactly what fits, it's super fast to access, and when you're done, everything just disappears. The heap is like your storage room - more flexible, can hold bigger things, but takes a bit more time to find what you need.
Vectors - The Dynamic Arrays That Don't Suck
Now let's talk about something you'll use ALL THE TIME in Rust - vectors! They're like arrays but can grow and shrink. Coming from JavaScript arrays or Python lists, vectors felt familiar but with Rust's ownership rules sprinkled on top.
fn main() {
// Creating vectors - multiple ways!
let mut numbers = Vec::new(); // Empty vector
let mut fruits = vec!["apple", "banana"]; // vec! macro with initial values
let zeros = vec![0; 5]; // [0, 0, 0, 0, 0] - 5 zeros
// Adding elements - push them on!
numbers.push(10);
numbers.push(20);
numbers.push(30);
println!("Numbers: {:?}", numbers);
// Accessing elements - be careful here!
println!("First number: {}", numbers[0]); // This can panic if index doesn't exist!
// Safer way to access
match numbers.get(1) {
Some(value) => println!("Second number: {}", value),
None => println!("No second number exists"),
}
// Removing elements
let last = numbers.pop(); // Removes and returns last element
println!("Popped: {:?}, Remaining: {:?}", last, numbers);
// Remove by index (shifts everything after it)
numbers.remove(0); // Removes first element
println!("After removing first: {:?}", numbers);
// Length and capacity - this blew my mind!
println!("Length: {}, Capacity: {}", numbers.len(), numbers.capacity());
}
Here's something cool I learned - vectors have both length (how many elements) and capacity (how much space is allocated). Rust is smart about memory allocation. When a vector needs to grow, it doesn't just add one more slot - it doubles the capacity to avoid frequent reallocations. Efficiency FTW!
HashMaps - Key-Value Pairs Done Right
HashMaps are like JavaScript objects or Python dictionaries - you store key-value pairs and can look them up super fast. But of course, Rust adds its own safety guarantees:
use std::collections::HashMap;
fn main() {
// Creating a HashMap
let mut scores = HashMap::new();
// Inserting key-value pairs
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 50);
scores.insert(String::from("Green"), 25);
println!("Scores: {:?}", scores);
// Getting values - safe way
let team_name = String::from("Blue");
match scores.get(&team_name) {
Some(score) => println!("Blue team score: {}", score),
None => println!("Blue team not found"),
}
// Or use get with unwrap_or for default values
let yellow_score = scores.get("Yellow").unwrap_or(&0);
println!("Yellow team score: {}", yellow_score);
// Updating values
scores.insert(String::from("Blue"), 25); // Overwrites old value
// Only insert if key doesn't exist
scores.entry(String::from("Purple")).or_insert(15);
scores.entry(String::from("Blue")).or_insert(100); // Won't overwrite!
println!("Final scores: {:?}", scores);
// Iterating through HashMap
for (team, score) in &scores {
println!("{}: {}", team, score);
}
}
The entry().or_insert()
pattern is super handy - it's like saying "if this key exists, leave it alone, otherwise insert this value." I use this all the time for counting things or initializing values.
Iterators - The Functional Programming Magic
Now here's where Rust really shines. Iterators let you process collections in a functional programming style, and they're FAST because Rust optimizes them at compile time. No runtime overhead like you'd expect!
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Basic iteration - for loop (actually uses iterator behind the scenes!)
println!("Basic iteration:");
for num in &numbers {
println!("{}", num);
}
// Iterator methods - this is where it gets fun!
// 1. FILTER - keep only elements that match condition
let even_numbers: Vec<i32> = numbers
.iter() // Create iterator
.filter(|&x| x % 2 == 0) // Keep only even numbers
.cloned() // Convert &i32 to i32
.collect(); // Turn back into Vec
println!("Even numbers: {:?}", even_numbers);
// 2. MAP - transform each element
let squared: Vec<i32> = numbers
.iter()
.map(|x| x * x) // Square each number
.collect();
println!("Squared: {:?}", squared);
// 3. CHAINING - combine multiple operations
let processed: Vec<String> = numbers
.iter()
.filter(|&x| x > 5) // Only numbers greater than 5
.map(|x| format!("{}!", x)) // Turn into strings with exclamation
.collect();
println!("Processed: {:?}", processed);
// 4. REDUCE operations
let sum: i32 = numbers.iter().sum();
let max = numbers.iter().max(); // Returns Option<&i32>
println!("Sum: {}, Max: {:?}", sum, max);
// 5. FINDING elements
let found = numbers.iter().find(|&&x| x > 7);
println!("First number > 7: {:?}", found);
}
Different Types of Iterators - When to Use What ??
This part confused me initially, but once I got it, everything made sense. There are three main ways to create iterators:
fn main() {
let mut words = vec!["hello".to_string(), "world".to_string(), "rust".to_string()];
// 1. iter() - borrows each element (read-only)
println!("Using iter() - borrowing:");
for word in words.iter() {
println!("{}", word); // word is &String here
// Can't modify original vector through this iterator
}
// 2. iter_mut() - mutably borrows each element (can modify)
println!("Using iter_mut() - mutable borrowing:");
for word in words.iter_mut() {
word.push('!'); // word is &mut String here - we can modify it!
}
println!("After modification: {:?}", words);
// 3. into_iter() - takes ownership of each element
println!("Using into_iter() - taking ownership:");
for word in words.into_iter() {
println!("{}", word); // word is String here (owned)
// Original vector is consumed and can't be used after this
}
// println!("{:?}", words); // ERROR! words was moved by into_iter()
// Quick reference for when to use what:
// - iter(): When you want to READ elements without changing anything
// - iter_mut(): When you want to MODIFY elements in place
// - into_iter(): When you want to CONSUME/TRANSFORM the collection
}
Here's my mental model for choosing iterators:
Use
iter()
when you want to look at your data without changing it - like reading a book without writing in it.Use
iter_mut()
when you want to modify your data in place - like editing a document.Use
into_iter()
when you're done with the original collection and want to transform it into something else - like melting down old jewelry to make something new.
Putting It All Together - A Real Example
Let me show you how all these concepts work together in a practical example:
use std::collections::HashMap;
fn main() {
// Let's build a simple word frequency counter!
let text = "hello world hello rust world rust is amazing rust";
let words: Vec<&str> = text.split_whitespace().collect();
// Count word frequencies using HashMap
let mut word_count = HashMap::new();
for word in &words {
let count = word_count.entry(word).or_insert(0);
*count += 1;
}
println!("Word frequencies: {:?}", word_count);
// Find most common words using iterators
let mut sorted_words: Vec<_> = word_count
.iter()
.filter(|(_, &count)| count > 1) // Only words appearing more than once
.collect();
// Sort by count (descending)
sorted_words.sort_by(|a, b| b.1.cmp(a.1));
println!("Most common words:");
for (word, count) in sorted_words {
println!("{}: {} times", word, count);
}
}
This example shows stack variables (the counters), heap variables (the HashMap and vectors), iterator usage for filtering and processing, and practical HashMap operations all working together!
What I Learned Today - The Memory and Collections Picture
After another intense day with Rust, I can see how all these pieces fit together. Understanding stack vs heap isn't just academic knowledge - it helps you write more efficient code and understand why Rust makes certain design decisions.
Collections like vectors and hashmaps give you the dynamic data structures you need for real programs, but with Rust's safety guarantees. No null pointer dereferences, no buffer overflows, no use-after-free bugs.
And iterators? They're like having a Swiss Army knife for data processing. They're expressive, efficient, and safe. Coming from languages where you write manual loops for everything, Rust's iterator methods feel like superpowers.
The coolest realization? All of this happens with zero runtime overhead for safety checks. Rust's compiler is doing all the heavy lifting at compile time, so your final binary is as fast as hand-optimized C code but without the safety concerns.
Something to Note
Don't worry if the iterator syntax feels overwhelming at first - it took me several attempts to get comfortable with the filter().map().collect()
chains. Start with simple operations and gradually build up to more complex transformations.
Also, the stack vs heap distinction might seem academic, but it becomes really important when you start working with performance-critical code or trying to understand ownership errors. Think of it as building intuition about where your data lives and how long it stays alive.
One more thing - when you see compiler errors about borrowing or lifetimes while working with collections, don't panic! Read the error message carefully (Rust has amazing error messages), and think about whether you're trying to use data after it's been moved or modify something through an immutable reference.
Hope this Part 2 helped you understand Rust's approach to memory and collections! Next time, I'm planning to dive into error handling patterns and maybe some concurrency basics. The journey continues :))
Happy Coding! By Abir
Subscribe to my newsletter
Read articles from Abir Dutta directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Abir Dutta
Abir Dutta
I am a Blockchain and MERN stack developer. While building real-life application based full-stack projects, I also like to connect and network with different types of people to learn more from them.