18.1Introduction to collections

Last updated June 13, 2026

Every program of any size needs to hold many values: a list of scores, the words in a file, a count of each item. The fixed, named fields of a struct (chapter 10) can't do that, because you don't know in advance how many there'll be. The standard library's collections can: they hold a variable number of values, growing and shrinking at runtime. This chapter covers the three you'll reach for constantly, Vec, String, and HashMap, and it's where ownership, borrowing, and generics all show up in code you'll write every day.

Why collections live on the heap

Recall the stack-and-heap distinction from lesson 8.2. A value whose size is known at compile time can live in a stack frame; a value whose size changes at runtime cannot, because the frame is laid out when the function is compiled. A collection's whole point is that its size varies, you push onto it, it grows, so its contents live on the heap, with a small fixed-size handle on the stack pointing to them. This is exactly the anatomy of String from lesson 8.2: a stack handle (pointer, length, capacity) and heap-allocated contents. That wasn't a special String trick; it's how every growable collection works, and String was your first one.

Because they own heap data, collections follow the ownership rules you already know. A collection is dropped (and frees its heap memory) when its owner goes out of scope (lesson 8.3); moving a collection moves its handle (lesson 8.4); and a collection is never Copy (lesson 8.5), because it owns heap memory. Everything from chapters 8 and 9 applies, which is why those chapters came first.

Key insight

A collection is an owning handle to a variable amount of heap data. That single sentence connects it to everything you know: it's heap-allocated because its size isn't fixed (chapter 8), it owns its contents so it drops and moves like any owner (chapter 8), you borrow it with & to read or &mut to modify (chapter 9), and it's generic over what it holds (chapter 15, Vec<T>, HashMap<K, V>). Collections are where the whole course so far becomes daily, practical tooling.

The three core collections

This chapter focuses on three, because they cover the vast majority of real needs:

Vec<T> ("vector") is a growable list of values of the same type T, stored contiguously. It's the workhorse: whenever you want "a sequence of things" and the count varies, you reach for Vec. Lessons 18.2 through 18.4 cover it.

String is a growable, UTF-8 text buffer, a Vec of bytes specialized for text. You've used it since chapter 1, but lesson 18.5 finally examines it as a collection: why s[0] doesn't compile, what chars() versus bytes() give you, and why slicing can panic. This is the lesson that demystifies Rust strings for good.

HashMap<K, V> stores key-value pairs: look up a value by its key, like a dictionary. When you need "count how many times each word appears" or "find the score for this name," it's a HashMap. Lesson 18.6 covers it.

After those, lesson 18.7 revisits fixed-size arrays and slices in depth, and lesson 18.8 is a decision guide to the rest of the standard collections (VecDeque, HashSet, BTreeMap) for when the core three aren't the right fit.

Collection versus array

One distinction up front, since you've met fixed-size arrays in passing. An array [i32; 5] has a fixed length known at compile time and can live on the stack; a Vec<i32> has a length that varies at runtime and lives on the heap. Use an array when you know exactly how many elements there are and that never changes (the four bytes of an IP address, the seven days of a week); use a Vec when the count is dynamic, which is most of the time. Lesson 18.7 draws the line in full; for now, "fixed count: array; varying count: Vec" is the rule.

Quiz time

Question #1

Why do growable collections store their contents on the heap rather than the stack?

Show solution

Because their size changes at runtime (you push and they grow), and a stack frame is laid out at compile time with fixed sizes (lesson 8.2). A varying-size value can't fit in a fixed frame, so the contents live on the heap, with a small fixed-size handle on the stack pointing to them, exactly like String.

Question #2

A Vec<String> goes out of scope. What happens, and which chapter-8 rules are at work?

Show solution

The Vec is dropped, which frees its heap buffer and drops each String it contains (freeing their heap text too), all automatically (lesson 8.3). The Vec owns its contents, so cleanup nests all the way down. It also moves rather than copies (lesson 8.4) and is never Copy (lesson 8.5), because it owns heap memory.

Question #3

When would you use a fixed-size array instead of a Vec?

Show solution

When the number of elements is known at compile time and never changes, like the 4 bytes of an IPv4 address or 7 days of the week. An array has a fixed length and can live on the stack; a Vec has a runtime-varying length on the heap. Fixed count: array; varying count: Vec (lesson 18.7 covers the distinction fully).

The workhorse comes first. The next lesson creates and updates a Vec: the vec! macro, push, and the important difference between get (which returns Option) and [] indexing (which panics out of bounds).