Vectors (Vec)

Rust · Lesson 15 of 31 6 min read Updated

Arrays have a fixed length, but most real lists grow and shrink: students joining a batch, orders arriving at a shop, readings from a sensor. For these, Rust gives you Vec<T>, usually called a vector: a list of values of one type that lives on the heap and can change size.

Vec is the collection you will use more than any other. In this lesson you will create vectors, read and change them safely, loop over them in the three ways Rust allows, and sort, filter and remove items.

Creating a vector and adding items

Create an empty vector with Vec::new() or a filled one with the vec! macro. push adds to the end and pop removes from the end:

Rust
fn main() {
    let mut queue: Vec<String> = Vec::new();
    queue.push(String::from("Aarav"));
    queue.push(String::from("Isha"));
    queue.push(String::from("Kabir"));

    println!("{queue:?}, {} waiting", queue.len());
    println!("First: {}", queue[0]);
    println!("Tenth: {:?}", queue.get(9));

    let last = queue.pop();
    println!("Popped: {last:?}");
    println!("Now: {queue:?}");

    let marks = vec![56, 78, 91];
    println!("{marks:?}");
}
Output
["Aarav", "Isha", "Kabir"], 3 waiting
First: Aarav
Tenth: None
Popped: Some("Kabir")
Now: ["Aarav", "Isha"]
[56, 78, 91]
  • Vec<String> is the type: "a vector of String". With vec![56, 78, 91] Rust infers Vec<i32>.
  • queue[0] returns the item directly but panics if the index is out of range. queue.get(9) returns an Option instead.
  • pop returns an Option too, because the vector might be empty.
  • The vector owns its items. When queue is dropped, every String inside it is dropped as well.

A vector stores its items next to each other on the heap. When it runs out of room it allocates a bigger block and moves the items there; this is why you cannot keep a reference to an item while pushing (the borrow checker stops you, as you saw in the Rust Introduction). If you know roughly how many items you will add, Vec::with_capacity(500) reserves space up front and avoids repeated moves.

Three ways to loop

How you loop decides whether you read, change or consume the vector:

Rust
fn main() {
    let mut prices = vec![120, 80, 250];

    for p in &prices {             // read: p is &i32
        print!("{p} ");
    }
    println!();

    for p in &mut prices {         // change: p is &mut i32
        *p += *p / 10;             // 10% price rise
    }
    println!("{prices:?}");

    let mut total = 0;
    for p in prices {              // consume: p is i32, the vector is moved
        total += p;
    }
    println!("Total: {total}");
    // println!("{prices:?}");     // error: `prices` was moved by the loop
}
Output
120 80 250 
[132, 88, 275]
Total: 495
LoopItem typeVector afterwards
for x in &v&TUnchanged and usable
for x in &mut v&mut TChanged and usable
for x in vTMoved, no longer usable

Most loops should borrow with &v. Consume the vector only when you really want to take its items out, for example to move them into another collection.

Changing a vector

Vec has a large toolbox of methods. These are the ones you will reach for most:

Rust
fn main() {
    let mut rolls = vec![14, 3, 27, 3, 9, 21, 14];

    rolls.sort();
    println!("Sorted:  {rolls:?}");
    rolls.dedup();
    println!("Unique:  {rolls:?}");

    rolls.insert(1, 5);
    println!("Insert:  {rolls:?}");
    let removed = rolls.remove(0);
    println!("Removed {removed}: {rolls:?}");

    rolls.retain(|&r| r % 2 == 1);
    println!("Odd:     {rolls:?}");
    println!("Has 21?  {}", rolls.contains(&21));

    rolls.extend([31, 33]);
    println!("Extend:  {rolls:?}, length {}", rolls.len());
}
Output
Sorted:  [3, 3, 9, 14, 14, 21, 27]
Unique:  [3, 9, 14, 21, 27]
Insert:  [3, 5, 9, 14, 21, 27]
Removed 3: [5, 9, 14, 21, 27]
Odd:     [5, 9, 21, 27]
Has 21?  true
Extend:  [5, 9, 21, 27, 31, 33], length 6
  • dedup removes neighbouring duplicates only, so sort first.
  • insert(i, x) and remove(i) shift every later item, which is slow for long vectors. swap_remove(i) is fast when order does not matter: it moves the last item into the gap.
  • retain keeps the items for which the small function |&r| r % 2 == 1 returns true. That |...| syntax is a closure; see Closures.
  • contains takes a reference, hence &21.

Vectors of structs and enums

A vector holds one type, but that type can be a struct, or an enum when you need different kinds of items in one list:

Rust
struct Player {
    name: String,
    runs: u32,
}

fn main() {
    let mut team = vec![
        Player { name: String::from("Rohan"), runs: 45 },
        Player { name: String::from("Farhan"), runs: 88 },
        Player { name: String::from("Ananya"), runs: 61 },
    ];

    team.sort_by(|a, b| b.runs.cmp(&a.runs));   // highest runs first

    for p in &team {
        println!("{:<8}{:>4}", p.name, p.runs);
    }
}
Output
Farhan    88
Ananya    61
Rohan     45

sort_by takes a closure that compares two items; b.runs.cmp(&a.runs) (with b first) sorts in descending order. sort_by_key(|p| p.runs) is shorter for ascending order. In the format string, {:<8} left-aligns the name in 8 characters and {:>4} right-aligns the runs in 4, which lines up the columns.

Floating-point numbers cannot use plain sort(), because f64 has the special value NaN that is not comparable to anything. Sort them with v.sort_by(|a, b| a.total_cmp(b)).

Common mistakes

  • Indexing past the end. v[10] on a 3-item vector panics. Use get when the index comes from outside your control.
  • Pushing while holding a reference to an item. Copy the value out first (let first = v[0];) or finish using the reference before you push.
  • Looping with for x in v and then using v. That loop moved the vector; loop over &v instead.
  • Calling dedup on unsorted data. It only removes duplicates that sit next to each other.
  • Removing from the front of a long vector in a loop. Each remove(0) shifts everything. Use VecDeque from std::collections for queues.

Key points

  • Vec<T> is a growable list on the heap; create it with Vec::new() or vec![...].
  • push and pop work at the end; v[i] panics on a bad index while get(i) returns an Option.
  • Loop with &v to read, &mut v to change items and v to consume the vector.
  • sort, dedup, retain, insert, remove, contains and extend cover most everyday changes.
  • Vectors can hold structs or enums; sort them with sort_by or sort_by_key.

Exercise

A coaching centre in Kochi records test marks: vec![67, 32, 88, 45, 29, 91, 74]. Write a program that: adds two late marks (55 and 30), counts how many students failed (below 33), removes the failed marks with retain, sorts the rest from highest to lowest, and prints the top three.

Show solution
Rust
fn main() {
    let mut marks = vec![67, 32, 88, 45, 29, 91, 74];
    marks.push(55);
    marks.push(30);

    let mut failed = 0;
    for m in &marks {
        if *m < 33 {
            failed += 1;
        }
    }
    println!("Failed: {failed}");

    marks.retain(|&m| m >= 33);
    marks.sort_by(|a, b| b.cmp(a));
    println!("Passed, highest first: {marks:?}");
    println!("Top three: {:?}", &marks[..3]);
}
Output
Failed: 3
Passed, highest first: [91, 88, 74, 67, 55, 45]
Top three: [91, 88, 74]

The marks 32, 29 and 30 are below 33. &marks[..3] is a slice of the first three items, so nothing is copied.

Quick Quiz

1. What does v.pop() return for an empty vector?

2. Which loop lets you change every item of v and still use v afterwards?

3. What is vec![2, 2, 5, 2].dedup() going to leave?

4. Why can't you call .sort() on a Vec<f64>?

Learn Rust with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call