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:
-
Empty Vector:
#![allow(unused)] fn main() { let v: Vec<i32> = Vec::new(); }
Here,
i32
is specified explicitly. If omitted, Rust tries to infer it. -
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> }
- Empty vector:
-
Vector with Repeated Elements:
#![allow(unused)] fn main() { let v = vec![0; 5]; // Vec<i32> of length 5, all zeros }
-
From Iterators:
#![allow(unused)] fn main() { let v: Vec<i32> = (1..=5).collect(); // [1,2,3,4,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); }
- From slices:
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:
- A pointer to a heap-allocated buffer.
- A
len
field: the current number of elements. - 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 orNone
if empty.get
: Returns anOption<&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.