Chapter 9

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 to 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 this array is a local variable with a compile-time length, its elements can live directly on the stack alongside the function's other local data. Setting aside that stack space is cheap, and Rust reclaims it 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. The local Vec value is a small header containing a pointer, a length, and a capacity, while the allocator provides the buffer it points to. 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

If you're coming from Python or Java, Vec<T> is the closer match for the lists you use every day. In C, the same choice is closer to picking a fixed-size array or managing an allocation yourself. Rust gives you both choices, and its ownership rules apply to either one.

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.

You can start with an empty vector or with its initial items:

let mut empty: Vec<i32> = Vec::new();
let with_items = vec![1, 2, 3]; // vec! starts with these three items

Changing a vector requires mutable access, while reading it only needs a shared borrow:

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

When you choose a parameter type, start from what the function needs to do:

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.

Adding items

A Vec isn't frozen once you build it. Here you change the list in place by pushing a new item onto the end. The &mut Vec<String> says "I need exclusive access for a moment," and that exclusive borrow is what lets you push.

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 1 of 3
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Reveal the full solution Spoiler: the complete answer
    /// Adds an item to the shopping list.
    ///
    /// Now we modify the list in place. The `&mut Vec<String>` says "I need
    /// exclusive access for a moment", and that's what lets us add to it.
    fn add_item(list: &mut Vec<String>, item: &str) {
        list.push(item.to_string());
    }
    
    #[test]
    fn test_add_item() {
        let mut list = vec!["bread".to_string()];
        add_item(&mut list, "butter");
        assert_eq!(list.len(), 2);
        assert_eq!(list[1], "butter");
    }
    

    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 2 of 3
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Reveal the full solution Spoiler: the complete answer
      /// Checks if the list contains a specific item.
      ///
      /// A read-only operation again, but this time we have to compare each
      /// element against `item`.
      ///
      /// Heads-up: you'll want to reach for `Vec::contains`, but its
      /// signature is `fn contains(&self, x: &T) -> bool`, and here that's `&String`,
      /// while we have a `&str`. The most direct fix at this point in the course
      /// is a `for` loop. We will cover iterators later.
      fn contains_item(list: &Vec<String>, item: &str) -> bool {
          for entry in list {
              if entry == item {
                  return true;
              }
          }
          false
      }
      
      #[test]
      fn test_contains_item() {
          let list = vec!["apple".to_string(), "banana".to_string()];
          assert_eq!(contains_item(&list, "apple"), true);
          assert_eq!(contains_item(&list, "orange"), false);
      }
      

      Building a list from borrowed slices

      This time the input and output hold different string types: each input is a borrowed &str, while the output must own its Strings. That means every item needs to become an owned String before it can live in the result.

      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 3 of 3
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Reveal the full solution Spoiler: the complete answer
        /// Creates a shopping list from the given items.
        ///
        /// 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.
        fn create_shopping_list(items: &[&str]) -> Vec<String> {
            let mut list = Vec::new();
            for &item in items {
                list.push(item.to_string());
            }
            list
        }
        
        #[test]
        fn test_create_shopping_list() {
            let items = ["bread", "milk", "eggs"];
            let list = create_shopping_list(&items);
            assert_eq!(list.len(), 3);
            assert_eq!(list[0], "bread");
        }
        

        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.
        • Choose the parameter from the operation: &[T] to read, &mut Vec<T> to add or remove, and plain Vec<T> to consume the whole vector.
        • 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 10HashMaps