18.2 The Vec<T> Vector Type

A Vec<T>—often called a “vector”—is a dynamic, growable list stored contiguously on the heap. It provides fast indexing, can change size at runtime, and manages its memory automatically. This is conceptually similar to std::vector in C++ or a manually sized, dynamically allocated array in C, but with Rust’s safety guarantees and automated cleanup.

18.2.1 Creating a Vector

There are several ways to create a new vector:

  1. Empty Vector:

    let v: Vec<i32> = Vec::new(); 
    // If the type is omitted, Rust attempts type inference.
  2. Using the vec! Macro:

    let v1: Vec<i32> = vec![];           // Empty
    let v2 = vec![1, 2, 3];             // Infers Vec<i32>
    let v3 = vec![0; 5];                // 5 zeros of type i32
  3. From Iterators or Other Data:

    let v: Vec<_> = (1..=5).collect();   // [1, 2, 3, 4, 5]
    
    let slice: &[i32] = &[10, 20, 30];
    let v2 = slice.to_vec();
    
    let array = [4, 5, 6];
    let v3 = Vec::from(array);
  4. Vec::with_capacity for Pre-allocation:

    let mut v = Vec::with_capacity(10);
    for i in 0..10 {
        v.push(i);
    }

    This avoids multiple reallocations if you know roughly how many items you will store.

18.2.2 Properties and Memory Management

Under the hood, a Vec<T> maintains:

  1. A pointer to a heap-allocated buffer,
  2. A len (the current number of elements),
  3. A capacity (the total number of elements that can fit before a reallocation is needed).

When you remove elements, the length decreases but the capacity remains. You can call shrink_to_fit() if you want to reduce capacity:

let mut v = vec![1, 2, 3, 4, 5];
v.pop(); 
v.shrink_to_fit(); // Release spare capacity

Rust’s borrowing rules prevent dangling references and out-of-bounds access. If you try to use v[index] with an invalid index, the program panics at runtime. Meanwhile, v.get(index) returns None if the index is out of range.

18.2.3 Basic Methods

  • push(elem): Appends an element (reallocation may occur).
  • pop(): Removes the last element and returns it, or None if empty.
  • get(index): Returns Option<&T> safely.
  • Indexing ([]): Returns &T, panics if the index is invalid.
  • len(): Returns the current number of elements.
  • insert(index, elem): Inserts an element at a specific position, shifting subsequent elements.
  • remove(index): Removes and returns the element at the given position, shifting elements down.

18.2.4 Accessing Elements

let v = vec![10, 20, 30];

// Panics on invalid index
println!("First element: {}", v[0]);

// Safe access using `get`
if let Some(value) = v.get(1) {
    println!("Second element: {}", value);
}

// `pop` removes from the end
let mut v2 = vec![1, 2, 3];
if let Some(last) = v2.pop() {
    println!("Popped: {}", last);
}

18.2.5 Iteration Patterns

// Immutable iteration
let v = vec![1, 2, 3];
for val in &v {
    println!("{}", val);
}

// Mutable iteration
let mut v2 = vec![10, 20, 30];
for val in &mut v2 {
    *val += 5;
}

// Consuming iteration (v3 is moved)
let v3 = vec![100, 200, 300];
for val in v3 {
    println!("{}", val);
}

18.2.6 Handling Mixed Data

All elements in a Vec<T> must be of the same type. If you need different types, consider:

  • An enum that encompasses all possible variants.
  • Trait objects (e.g., Vec<Box<dyn Trait>>) for runtime polymorphism.

For example, using an enum:

enum Value {
    Integer(i32),
    Float(f64),
    Text(String),
}

fn main() {
    let mut mixed = Vec::new();
    mixed.push(Value::Integer(42));
    mixed.push(Value::Float(3.14));
    mixed.push(Value::Text(String::from("Hello")));

    for val in &mixed {
        match val {
            Value::Integer(i) => println!("Integer: {}", i),
            Value::Float(f)   => println!("Float: {}", f),
            Value::Text(s)    => println!("Text: {}", s),
        }
    }
}

Using trait objects adds overhead due to dynamic dispatch and extra heap allocations. Choose the approach that best meets your performance and design needs.

18.2.7 Summary: Vec<T> vs. C

In C, you might manually manage an array with malloc/realloc/free, tracking capacity yourself. Rust’s Vec<T> automates these tasks, prevents out-of-bounds access, and reclaims memory when the vector goes out of scope. This significantly reduces memory-management errors while still allowing fine-grained performance tuning (e.g., pre-allocation via with_capacity).