Chapter 7

Vectors

👋 Anyone can read and edit this exercise. Sign up to save your progress.

If you stare at a problem for long enough, it starts turn into a vector. Vec<T> is the workhorse of Rust's collection types.

Arrays first: where vectors come from

Before we get to Vec, it's worth a minute on its older sibling, the array. An array [T; N] is a fixed-size, contiguous chunk of values whose length is part of the type:

let bytes: [u8; 4] = [10, 20, 30, 40];   // exactly four u8s, forever

Because the length is known at compile time, the whole array lives on the stack, the same place your local variables and function parameters live. Stack storage is essentially free: allocation is "move the stack pointer by 4 * size_of::<u8>() bytes," and cleanup happens automatically when the function returns. The catch is that you can't grow it. bytes.push(50) doesn't compile, because there's nowhere to grow into: the next bytes on the stack already belong to somebody else.

Vec<T> solves that by storing the elements on the heap instead. A Vec value is a tiny header on the stack (pointer + length + capacity) that points at a buffer the allocator hands you. When you push and the buffer fills up, Vec asks for a bigger one and copies the elements over. The header stays the same size; the buffer behind it grows.

A quick mental model:

TypeWhere the data livesSize known atCan grow?
[T; N]StackCompile timeNo
Vec<T>HeapRun timeYes
&[T]Wherever the owner put it (just a pointer + length)n/an/a

This distinction is one of the things Rust makes you confront that many languages hide. In Python or Java, every list is heap-backed and you don't get a choice; in C you'd reach for either a fixed-size array or malloc by hand. Rust gives you both, with the same ownership rules applied to either.

Vectors: growable, heap-allocated

Vec<T> is what you reach for most of the time. The <T> is a generic parameter: it works with any type, but a single Vec only holds one type at a time. So Vec<i32> is a vector of 32-bit integers, Vec<String> is a vector of owned strings.

Two ways to create one:

let mut empty: Vec<i32> = Vec::new();
let with_items = vec![1, 2, 3]; // the vec! macro is the usual way

Most operations need a mutable reference. Note the &mut:

let mut list = vec!["bread"];
list.push("milk");          // requires `mut`
let count = list.len();     // borrow without mut

A few rules of thumb that will save you trouble:

Index access (list[0]) panics if out of bounds. list.get(0) returns Option<&T> instead, which is the safer default. Use this unless you like panics.

Counting items

The simplest possible operation on a vector is to ask it how many items it currently holds. Notice the parameter is &Vec<String>: a borrow, so the caller keeps ownership of the list.

Useful from the standard library

  • Vec::len returns the number of items as a usize. Constant time, no allocation.
  • Vec::is_empty reads better than len() == 0 when that's all you need to know.

Press Ctrl/Cmd + Enter to run the tests without leaving the keyboard. The Run button does the same thing.

Exercise 1 of 4
Open in Web Editor

Results

    Compiler / runtime output
    
                

    Adding items

    Now we modify the list in place. Where the previous step only read from the Vec, this one needs to change it. The &mut Vec<String> says "I need exclusive access for a moment", and that's what lets us push a new item onto the end.

    Useful from the standard library

    • Vec::push appends one item to the end of the vector. Requires &mut self, which is why the parameter here is &mut Vec<String>.
    • str::to_string (or String::from) turns the borrowed &str parameter into the owned String the vector wants to hold.
    Exercise 2 of 4
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  

      Searching the list

      Back to a read-only operation, but now we have to compare each element against the item we're looking for. This is where the borrowed-vs-owned distinction starts to bite: the Vec holds Strings, but we're searching with a &str.

      Useful from the standard library

      • <[T]>::contains is the obvious tool, but its signature is fn contains(&self, x: &T) -> bool. Here that's &String, while the parameter is &str. The mismatch is real, hence the loop.
      • A for item in list loop yields &String on each iteration. Comparing item == search works because String and &str know how to compare against each other.
      • str::eq via == is the cleanest way to compare two strings regardless of ownership; no manual .as_str() needed.
      Exercise 3 of 4
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    

        Building a list from borrowed slices

        The trickiest of the four: each input is a &str, but the output is a Vec<String>. Each borrowed slice has to become an owned String somewhere along the way. The String::from / .to_string() / .to_owned() family all do this conversion.

        Useful from the standard library

        • Vec::new creates an empty vector you can push into. The vec! macro is more common when you already know the contents.
        • Vec::push appends one item. Combine with a for loop over items to fill the result.
        • String::from, str::to_string, and str::to_owned all turn a &str into a fresh String. Pick whichever reads best.
        Exercise 4 of 4
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      

          Wrapping up vectors

          You worked through every form a Vec parameter can take: a shared borrow for reading, a mutable borrow for changing, and a fresh Vec<String> produced from borrowed &str inputs.

          What we learned

          • Vec<T> is a growable, heap-allocated array. The <T> is generic, but a single Vec only holds one type at a time.
          • Build them with Vec::new() for an empty one, or the vec![...] macro when you already have the contents.
          • The parameter version says what you intend to do: &[T] or &Vec<T> to read, &mut Vec<T> to add or remove, plain Vec<T> to consume the whole thing.
          • push appends, pop removes the last item and returns Option<T>, len and is_empty answer the obvious questions.
          • Index access (list[i]) panics on out-of-bounds; list.get(i) returns Option<&T> and is the safer default.
          • A for item in &list loop yields &T. That's usually what you want; iterating &mut list gives &mut T, and iterating list by value moves the items out.
          • A Vec<String> is not the same as a Vec<&str>. Converting between them needs .to_string() / String::from (one direction) or .as_str() (the other).
          Next chapter 8HashMaps