PHASE 2 ← Back to Course
6 / 17
📦

Collections

Master Vec, String, and HashMap, the data structures you'll use in every Rust project.

1

Creating and Using Vectors

Vectors are growable arrays stored on the heap. They allow you to store multiple values of the same type.

Rust
// Creating vectors
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
// Using macro shorthand
let v = vec![1, 2, 3, 4, 5];
// Accessing elements
let third = &v[2]; // Index access, panics if out of bounds
println!("Third element: {}", third);
match v.get(10) {
    Some(element) => println!("Element: {}", element),
    None => println!("No element at index 10"),
// Modifying vectors
let mut v = vec![1, 2, 3];
v.push(4);
v.pop(); // Returns Option<i32>
v.insert(1, 99); // Insert at index
v.remove(1); // Remove and return element
💡

Tip

Prefer v.get(index) over v[index] when the index might be out of bounds. get() returns an Option instead of panicking.

2

Iterating Over Vectors

Rust
let v = vec![1, 2, 3, 4, 5];
// Immutable iteration
for element in &v {
    println!("Element: {}", element);
// Mutable iteration - modify elements
let mut v = vec![1, 2, 3];
for element in &mut v {
    *element += 50; // Dereference to modify
// Taking ownership
let v = vec![1, 2, 3];
for element in v {
    println!("Element: {}", element);
} // v is consumed here
// Using enumerate
let v = vec!["a", "b", "c"];
for (index, value) in v.iter().enumerate() {
    println!("Index: {}, Value: {}", index, value);
}
⚠️

Warning

Iterating with for element in v (without &) moves the vector. After the loop, v is no longer usable. Use &v for borrowing or &mut v for mutable access.

3

Storing Multiple Types

Rust
// Using enums to store multiple types
#[derive(Debug)]
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];
for cell in &row {
    match cell {
        SpreadsheetCell::Int(i) => println!("Integer: {}", i),
        SpreadsheetCell::Float(f) => println!("Float: {}", f),
        SpreadsheetCell::Text(s) => println!("Text: {}", s),
    }
}
Rust
// Using trait objects for polymorphism
trait Shape {
    fn area(&self) -> f64;
struct Circle { radius: f64 }
impl Shape for Circle {
    fn area(&self) -> f64 { 3.14 * self.radius * self.radius }
struct Square { side: f64 }
impl Shape for Square {
    fn area(&self) -> f64 { self.side * self.side }
let shapes: Vec<Box<dyn Shape>> = vec![
    Box::new(Circle { radius: 5.0 }),
    Box::new(Square { side: 4.0 }),
];
for shape in shapes {
    println!("Area: {}", shape.area());
}

Key Insight

Vectors can only hold one type, but you can work around this using enums (when you know all types at compile time) or trait objects with Box<dyn Trait> (for open-ended polymorphism).

4

Strings (String vs &str)

Creating and Concatenating Strings

Rust
// String literals are &str (string slices)
let s: &str = "hello";
// String is an owned, growable UTF-8 encoded string
let mut s = String::new();
s.push_str("hello");
s.push(' ');
s.push_str("world");
// Creating strings
let s1 = String::from("hello");
let s2 = "world".to_string();
// String slices
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
// Concatenation with +
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = s1 + &s2; // s1 moved, s2 borrowed
// println!("{}", s1); // Error: s1 moved
// Using format! macro for cleaner concatenation
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3); // All s1, s2, s3 still valid
💡

Tip

Use format! instead of + for concatenating multiple strings. It doesn't take ownership of any of its arguments and produces cleaner code.

5

Indexing, Slicing, and Chars vs Bytes

Rust
let s = String::from("hello");
// Bytes
for b in s.as_bytes() {
    println!("Byte: {}", b);
// Characters - use chars() not indexing!
let s = "Привет"; // Russian, multi-byte chars
for c in s.chars() {
    println!("Char: {}", c);
// Indexing a String panics - strings use UTF-8
// let ch = s[0]; // ERROR!
// Byte access
let hello = "Здравствуй"; // 4-byte UTF-8 chars
println!("Byte length: {}", hello.len()); // 20 bytes, not 10 chars
// Correct slicing by byte boundaries
let s = "hello";
let slice = &s[0..4]; // "hell" - valid boundary
// Safe iteration
let word = "Привет";
let first_char = word.chars().next().unwrap(); // 'П'
let char_count = word.chars().count(); // 6
let byte_count = word.len(); // 12 (UTF-8 encoded)
// Grapheme clusters (combining characters)
let s = "e\u{0301}"; // e with accent
println!("Char count: {}", s.chars().count()); // 2
println!("String: {}", s); // Appears as 1 character
⚠️

Warning

You cannot index into a Rust String with s[0]. Rust strings are UTF-8 encoded, and a single character may span multiple bytes. Use .chars(), .bytes(), or byte-boundary slicing instead.

6

Creating and Accessing HashMaps

Rust
use std::collections::HashMap;
// Creating a new HashMap
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// Getting values
let team = String::from("Blue");
let score = scores.get(&team);
match score {
    Some(s) => println!("Score: {}", s),
    None => println!("Team not found"),
// Using unwrap_or
let score = scores.get(&String::from("Red")).unwrap_or(&0);
// Iterating over HashMap
for (key, value) in &scores {
    println!("{}: {}", key, value);
// Creating HashMap from vectors
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.into_iter()
    .zip(initial_scores.into_iter())
    .collect();
7

Updating HashMaps

Rust
use std::collections::HashMap;
let mut map = HashMap::new();
// Overwriting a value
map.insert("a", 10);
map.insert("a", 25); // "a" now maps to 25
// Only insert if key doesn't exist
map.entry("a").or_insert(50); // "a" still 25
map.entry("b").or_insert(50); // "b" now 50
// Updating based on old value
let mut map = HashMap::new();
map.insert("word", 1);
for word in vec!["word", "hello", "word"] {
    let count = map.entry(word).or_insert(0);
    *count += 1;
// word: 2, hello: 1
// Entry API for complex updates
let mut map = HashMap::new();
let entry = map.entry(String::from("key"));
let value = entry.or_insert(vec![]);
value.push(1);
// Getting mutable reference
let mut map = HashMap::new();
map.insert(String::from("key"), 5);
if let Some(v) = map.get_mut(&String::from("key")) {
    *v = 10;
}

Key Insight

The Entry API (map.entry(key).or_insert(default)) is a powerful pattern that avoids double lookups. It checks for a key and inserts a default in one step, returning a mutable reference to the value.

8

Custom Types and Practical Examples

Rust
use std::collections::HashMap;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct Person {
    name: String,
    age: u32,
// Using custom types as keys
let mut people: HashMap<Person, String> = HashMap::new();
let alice = Person {
    name: String::from("Alice"),
    age: 30,
};
people.insert(alice, String::from("Engineer"));
Rust
// Word frequency counter
fn count_words(text: &str) -> HashMap<&str, u32> {
    let mut map = HashMap::new();
    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }
    map
let text = "hello world hello rust rust rust";
let frequencies = count_words(text);
for (word, count) in frequencies {
    println!("{}: {}", word, count);
}
Rust
// Grouping values by key
let mut groups: HashMap<char, Vec<u32>> = HashMap::new();
let data = vec![(1, 'a'), (2, 'b'), (3, 'a'), (4, 'c'), (5, 'b')];
for (num, letter) in data {
    groups.entry(letter).or_insert_with(Vec::new).push(num);
for (letter, nums) in groups {
    println!("{}: {:?}", letter, nums);
}
💡

Tip

To use a custom struct as a HashMap key, derive Eq, PartialEq, and Hash. Rust requires keys to be hashable and comparable for equality.

← Previous Chapter 6 of 17 Next →