New: Try Voli The Bear, Fast package manager (and not only) for Windows
Updated Jun 19, 2026 Edit on GitHub

Collections

A single value is rarely enough. You'll want a list of scores, a table of users by name, a line of text built up piece by piece. This phase covers the containers you'll reach for every day, and tackles the single most confusing thing for Rust newcomers head-on: why there are two kinds of string, String and &str, and which to use when.

Vec<T> - the growable list you'll actually use

What it actually is. A Vec (say "vector") is a list that can grow and shrink. <T> means "a Vec of some type T" - Vec<i32> is a list of integers, Vec<String> a list of strings. Every element is the same type. This is the workhorse collection; reach for it by default.

fn main() {
    let mut scores = vec![10, 20, 30];
    scores.push(40);
    println!("{:?}", scores);
    println!("first: {}", scores[0]);
}
$ cargo run
[10, 20, 30, 40]
first: 10

What just happened: vec![10, 20, 30] is a macro that builds a Vec with three starting values. scores.push(40) added a fourth - why it's mut. scores[0] reads the first element (counting starts at 0). {:?} is a new placeholder: {} prints for humans, {:?} prints for debugging - handy for whole collections, which have no "pretty" human form. (Most standard types support it.)

📝 Terminology. {} is Display formatting (clean, for users). {:?} is Debug formatting (for you, the programmer). When Rust complains a type "doesn't implement Display," try {:?} instead.

Iterating - for ... in

To do something with each element, loop over a reference to the collection with for ... in:

fn main() {
    let scores = vec![10, 20, 30, 40];
    for s in &scores {
        println!("score: {s}");
    }
}
$ cargo run
score: 10
score: 20
score: 30
score: 40

What just happened: for s in &scores walked the list, binding each element to s in turn. & means "borrow the list to read it, don't consume it," so scores is still usable afterward. (Why & matters is the heart of Phase 6: Ownership; for now: loop over &collection when you just want to read.)

Arrays - fixed size, known up front

What it actually is. An array is like a Vec, but its length is fixed when you write it and never changes. You'll use these less than Vec, but they show up for small, known-size groups of data.

fn main() {
    let days = [1, 2, 3];
    println!("{:?}", days);
}
$ cargo run
[1, 2, 3]

What just happened: [1, 2, 3] is an array of exactly three integers - its size is part of its type ([i32; 3]), and you can't push to it. Rule of thumb: use Vec if the count can change; an array only when it's truly fixed and small. When in doubt, Vec.

String vs &str - the confusion, finally cleared up

This is the one. Two types for text, and beginners can never remember which is which. Here's the mental model that makes it stick:

  • String is an owned, growable buffer of text - you own it, it lives on the heap, and you can modify and grow it. Think of it as a notebook you bought: yours, and you can keep writing in it.
  • &str (a "string slice") is a borrowed view into text someone else owns - read-only, fixed. Think of it as a window looking at text: you can read what's there, but not change it through the window. A string literal like "Ada" is a &str (baked into your program - you're just viewing it).

📝 Terminology. Owned means "this value is responsible for its data and will clean it up." Borrowed (the &) means "a temporary reference to data owned elsewhere." This split runs through all of Rust - strings are just where you meet it first. Phase 6 makes it the main event.

Here's both in one place:

fn main() {
    let mut owned = String::from("Hello"); // owned, growable
    owned.push_str(", world");             // we can grow it
    println!("{owned}");

    let slice: &str = "literal";           // borrowed view, read-only
    println!("{slice}");
}
$ cargo run
Hello, world
literal

What just happened: String::from("Hello") made an owned String we can extend - push_str appended to it. "literal" is a &str, a fixed read-only view we can print but not grow. owned is a notebook; slice is a window.

Why two types? And the rule that ends the confusion

Rust splits them because they answer different questions. Building text - concatenating, reading user input, assembling a message - needs ownership and growth, so use String. Looking at text - passing it to a function that reads it, comparing it, printing it - doesn't need ownership, so &str is lighter and more flexible.

That leads to the rule that solves 90% of the confusion in real code:

💡 Key point. Take &str as a function parameter; return / store String. A function that only reads text should accept &str, because then it accepts both a borrowed slice and a borrow of an owned String - the most flexible choice. Watch:

fn greet(name: &str) {        // accepts a view into any text
    println!("Hello, {name}!");
}

fn main() {
    let owned = String::from("world");
    greet("typed inline"); // a &str literal - works
    greet(&owned);         // &owned borrows the String *as* a &str - also works
}
$ cargo run
Hello, typed inline!
Hello, world!

What just happened: greet asks for &str, and happily took both a literal and &owned (a borrow of a String). That's the payoff: callers can hand you either kind of text. If greet demanded a String, the literal call wouldn't compile and callers would be forced to allocate. So: read with &str, own with String.

⚠️ Gotcha. You can't add a &str to a String with + the way you might guess from other languages, and comparing a String to a &str needs care. Until ownership clicks, lean on the methods: my_string.push_str("more") to append, String::from("text") to make one, and &my_string to pass it where a &str is wanted. These cover almost everything early on.

HashMap - look things up by key

What it actually is. A HashMap<K, V> stores key → value pairs and looks up a value by key fast - the "dictionary" / "associative array" from other languages: an age by a person's name, a price by a product code, a count by a word.

Unlike Vec, HashMap isn't in scope automatically - bring it in with a use line at the top of the file (more on use in Phase 5):

use std::collections::HashMap;

fn main() {
    let mut ages: HashMap<String, i32> = HashMap::new();
    ages.insert(String::from("Ada"), 36);
    ages.insert(String::from("Linus"), 54);

    match ages.get("Ada") {
        Some(age) => println!("Ada is {age}"),
        None => println!("Ada not found"),
    }
}
$ cargo run
Ada is 36

What just happened: We created an empty map from String keys to i32 values, inserted two pairs, then looked one up. ages.get("Ada") doesn't return an age directly, but an Option: Some(age) if the key exists, None if it doesn't. Rust makes you handle the "missing key" case right there with match, so you can never accidentally use a value that wasn't found.

⏭️ Option, Some, None, and match are coming up properly: match in Phase 4, and Option's "no null in Rust" story in Phase 7. For now: get returns Some(value) or None, and you handle both.

⚠️ Gotcha - HashMaps have no order. Iterate a HashMap and the pairs come out in an unpredictable order that can differ run to run - that's by design, and what makes lookups fast. Need a stable order? Sort the keys yourself, or reach for BTreeMap (a sorted map) instead.

Recap

  1. Vec<T> is the growable list - your default container; vec![...] builds one, .push() grows it.
  2. for x in &collection iterates by borrowing (so the collection survives the loop).
  3. Arrays ([1, 2, 3]) are fixed-size; use Vec when the count can change.
  4. String is owned and growable; &str is a borrowed, read-only view. Read with &str, own with String.
  5. HashMap<K, V> stores key → value; .get(key) returns Some(value) or None, and it has no guaranteed order.
  6. {} prints for humans (Display); {:?} prints for debugging (Debug) - use the latter for whole collections.

You can store data now. Next: make decisions about it and bundle logic into functions, where you'll meet match, the feature Rust programmers love most.


← Phase 2: Syntax, Values & Types · Guide overview · Phase 4: Control Flow & Functions →

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. What is Vec<T> in Rust?

2. What is the rule for String versus &str?

3. What does HashMap's `.get(key)` return?