18.1 The Vec<T> Vector Type

A Vec<T> (vector) is a growable, heap-allocated list that stores elements of a single type T contiguously in memory. Unlike fixed-size arrays, vectors can increase or decrease their length at runtime. As with arrays, vector indices start at zero, and indexing is done using usize. Attempting to access or assign to an invalid index will cause a panic rather than extending the vector.

18.1.1 Creating a Vector

There are various ways to create a vector:

  1. Empty Vector:

    #![allow(unused)]
    fn main() {
    let v: Vec<i32> = Vec::new();
    }

    Here, i32 is specified explicitly. If omitted, Rust tries to infer it.

  2. Using the vec! Macro:

    • Empty vector:
      #![allow(unused)]
      fn main() {
      let v: Vec<i32> = vec![];
      }
    • Pre-populated vector:
      #![allow(unused)]
      fn main() {
      let v = vec![1, 2, 3]; // Inferred as Vec<i32>
      }
  3. Vector with Repeated Elements:

    #![allow(unused)]
    fn main() {
    let v = vec![0; 5]; // Vec<i32> of length 5, all zeros
    }
  4. From Iterators:

    #![allow(unused)]
    fn main() {
    let v: Vec<i32> = (1..=5).collect(); // [1,2,3,4,5]
    }
  5. From Existing Data:

    • From slices:
      #![allow(unused)]
      fn main() {
      let slice: &[i32] = &[1, 2, 3];
      let v = slice.to_vec();
      }
    • From arrays:
      #![allow(unused)]
      fn main() {
      let array = [4, 5, 6];
      let v = Vec::from(array);
      }

Just like arrays, vector indices start at zero and must be usize. Attempting v[some_invalid_index] will cause a panic rather than resizing the vector.

Using Vec::with_capacity() for Performance

Vec::with_capacity() allows you to pre-allocate memory:

#![allow(unused)]
fn main() {
let mut v = Vec::with_capacity(10);
for i in 0..10 {
    v.push(i);
}
}

This can improve performance by reducing the number of reallocations if you know the approximate size beforehand.

18.1.2 Properties and Memory Management

Internally, a vector maintains:

  1. A pointer to a heap-allocated buffer.
  2. A len field: the current number of elements.
  3. A capacity field: how many elements it can hold before reallocating.

When elements are removed with pop(), the length decreases but capacity remains unchanged. Use shrink_to_fit() to release unused memory:

#![allow(unused)]
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
v.pop();
v.shrink_to_fit();
}

18.1.3 Basic Usage Methods

  • push: Adds an element to the end, reallocating if needed.
  • pop: Removes and returns the last element or None if empty.
  • get: Returns an Option<&T> for safe indexing without panics.
  • Indexing ([]): Returns &T but panics if out of bounds.
  • len: Returns the number of elements.
  • is_empty: Checks if the vector has no elements.
  • insert: Inserts at a specified index, shifting elements.
  • remove: Removes at a specified index, shifting elements down.

18.1.4 Accessing Elements

  • Indexing:

    #![allow(unused)]
    fn main() {
    let v = vec![10, 20, 30];
    println!("First element: {}", v[0]); // Panics if index is invalid
    }
  • get Method:

    #![allow(unused)]
    fn main() {
    let v = vec![10, 20, 30];
    if let Some(value) = v.get(1) {
        println!("Second element: {}", value);
    }
    }
  • pop:

    #![allow(unused)]
    fn main() {
    let mut v = vec![1, 2, 3];
    if let Some(last) = v.pop() {
        println!("Popped: {}", last);
    }
    }

18.1.5 Iteration Patterns

  • Immutable Iteration:

    #![allow(unused)]
    fn main() {
    let v = vec![1, 2, 3];
    for val in &v {
        println!("{}", val);
    }
    }
  • Mutable Iteration:

    #![allow(unused)]
    fn main() {
    let mut v = vec![1, 2, 3];
    for val in &mut v {
        *val += 1;
    }
    }
  • Ownership Transfer:

    #![allow(unused)]
    fn main() {
    let v = vec![1, 2, 3];
    for val in v {
        println!("{}", val); // v is consumed
    }
    }

18.1.6 Homogeneous Data Requirement

Like arrays, vectors are homogeneous: all elements must have the same type. For mixing types, wrap values in an enum or use trait objects.

18.1.7 Storing Heterogeneous Data with Enums

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("Hello".to_string()));

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

18.1.8 Using Trait Objects for Heterogeneous Data

If an enum isn't suitable (for example, types determined at runtime), use trait objects:

trait Describe {
    fn describe(&self) -> String;
}

struct Integer(i32);
struct Float(f64);
struct Text(String);

impl Describe for Integer {
    fn describe(&self) -> String { format!("Integer: {}", self.0) }
}
impl Describe for Float {
    fn describe(&self) -> String { format!("Float: {}", self.0) }
}
impl Describe for Text {
    fn describe(&self) -> String { format!("Text: {}", self.0) }
}

fn main() {
let mut mixed: Vec<Box<dyn Describe>> = Vec::new();
mixed.push(Box::new(Integer(42)));
mixed.push(Box::new(Float(3.14)));
mixed.push(Box::new(Text("Hello".to_string())));

for item in &mixed {
    println!("{}", item.describe());
}
}

This flexibility incurs runtime costs due to dynamic dispatch and heap allocations.

18.1.9 Memory Management

When a vector goes out of scope, all its elements are dropped, ensuring automatic memory cleanup without manual intervention.