Chapter 9

Vectors

“See that vec of vecs over there?”
“Yeah. Can’t stand it.”
“Why?”
“It’s full of itself.”

A [u8; 4] has room for exactly four bytes; that's it.

When the number of items can vary, use a Vec<T> instead. It stores a sequence of values that can grow or shrink, without asking you to predict the final length.

Arrays First: Where Vectors Come From

To see why a Vec can grow, compare it with an 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 arrays have no push method: [u8; 4] holds exactly four elements, wherever it is stored.

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 requests more space and may move the elements to a new allocation. The header stays the same size; the buffer behind it grows.

A quick mental model:

TypeWhere the data livesSize known atCan grow?
[T; N]Inline in its owner (often on the stack)Compile 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

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, so you can handle a missing element without panicking.

Adding Items

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. It takes &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

    You only need to read the list here, comparing each element against the item you're looking for. The Vec holds Strings, but we're searching with a &str.

    The parameter is &[String], a shared slice, because searching doesn't need to grow the list. It accepts borrowed vectors and arrays without copying their elements; the tests call it with both. An &Vec<String> parameter would unnecessarily rule out the array caller.

    Useful from the Standard Library

    • <[T]>::contains takes a reference to the element type: its signature is fn contains(&self, x: &T) -> bool. Here that's &String, while the parameter is &str. A loop lets you compare the strings without allocating a temporary String.
    • A for entry in list loop yields &String on each iteration. Compare entry == item directly; no temporary String or .as_str() call is 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: &[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, "banana"), true);
          assert_eq!(contains_item(&list, "orange"), false);
          assert_eq!(contains_item(&[], "apple"), false);
      
          let pantry = ["rice".to_string(), "beans".to_string()];
          assert_eq!(contains_item(&pantry, "beans"), true);
      }
      

      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

      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 read a vector through a shared borrow and changed one through a mutable borrow. You also built a fresh Vec<String> 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 returns the number of items, and is_empty tells you whether there are any.
        • 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

        Optional Chapters

        Extra practice to explore at your own pace. These chapters do not count toward course progress.

        ← Return to the Rust course curriculum