If you stare at a problem for long enough, it starts turn into a vector.
Vec<T> is the workhorse of Rust's collection types.
Before we get to Vec, it's worth a minute on its older sibling, the 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 the length is known at compile time, the whole array lives on the stack, the same place your local variables and function parameters live.
Stack storage is essentially free: allocation is "move the stack pointer by 4 * size_of::<u8>() bytes," and cleanup happens automatically when the function returns.
The catch is that you can't grow it.
bytes.push(50) doesn't compile, because there's nowhere to grow into: the next bytes on the stack already belong to somebody else.
Vec<T> solves that by storing the elements on the heap instead.
A Vec value is a tiny header on the stack (pointer + length + capacity) that points at a buffer the allocator hands you.
When you push and the buffer fills up, Vec asks for a bigger one and copies the elements over.
The header stays the same size; the buffer behind it grows.
A quick mental model:
| Type | Where the data lives | Size known at | Can grow? |
|---|---|---|---|
[T; N] | Stack | Compile time | No |
Vec<T> | Heap | Run time | Yes |
&[T] | Wherever the owner put it (just a pointer + length) | n/a | n/a |
This distinction is one of the things Rust makes you confront that many languages hide.
In Python or Java, every list is heap-backed and you don't get a choice; in C you'd reach for either a fixed-size array or malloc by hand.
Rust gives you both, with the same ownership rules applied to either.
Vec<T> is what you reach for most of the time.
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.
Two ways to create one:
let mut empty: Vec<i32> = Vec::new();
let with_items = vec![1, 2, 3]; // the vec! macro is the usual way
Most operations need a mutable reference. Note the &mut:
let mut list = vec!["bread"];
list.push("milk"); // requires `mut`
let count = list.len(); // borrow without mut
A few rules of thumb that will save you trouble:
&[T]) as input when the function only needs to read the data.
This is the vector chapter's version of the &str rule from the functions chapter: &[i32] accepts a borrow of a Vec (&my_vec coerces to &[i32]), a borrow of an array (&[1, 2, 3]), or a sub-slice of either, all without conversion.
A parameter typed &Vec<i32> would only accept the first one and would offer nothing in return.&mut Vec<T> when you need to add or remove items.Vec<T> (no reference) when you actually want to consume the vector and take ownership.Index access (list[0]) panics if out of bounds.
list.get(0) returns Option<&T> instead, which is the safer default.
Use this unless you like panics.
A Vec isn't frozen once you build it.
This step changes the list in place: it pushes 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 us push.
Useful from the standard library
Vec::pushappends one item to the end of the vector. Requires&mut self, which is why the parameter here is&mut Vec<String>.str::to_string(orString::from) turns the borrowed&strparameter into the ownedStringthe vector wants to hold.
/// 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");
}
Back to a read-only operation, but now we have to compare each element against the item we're looking for.
This is where the borrowed-vs-owned distinction starts to bite: the Vec holds Strings, but we're searching with a &str.
Useful from the standard library
<[T]>::containsis the obvious tool, but its signature isfn contains(&self, x: &T) -> bool. Here that's&String, while the parameter is&str. The mismatch is real, hence the loop.- A
for item in listloop yields&Stringon each iteration. Comparingitem == searchworks becauseStringand&strknow how to compare against each other.str::eqvia==is the cleanest way to compare two strings regardless of ownership; no manual.as_str()needed.
/// 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: &Vec<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, "orange"), false);
}
The trickiest of the three: 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 conversion.
Useful from the standard library
Vec::newcreates an empty vector you can push into. Thevec!macro is more common when you already know the contents.Vec::pushappends one item. Combine with aforloop overitemsto fill the result.String::from,str::to_string, andstr::to_ownedall turn a&strinto a freshString. Pick whichever reads best.
/// 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");
}
You worked through every form a Vec parameter can take: a shared borrow for reading, a mutable borrow for changing, and a fresh Vec<String> produced from borrowed &str inputs.
What we learned
Vec<T>is a growable, heap-allocated array. The<T>is generic, but a singleVeconly holds one type at a time.- Build them with
Vec::new()for an empty one, or thevec![...]macro when you already have the contents.- The parameter version says what you intend to do:
&[T]or&Vec<T>to read,&mut Vec<T>to add or remove, plainVec<T>to consume the whole thing.pushappends,popremoves the last item and returnsOption<T>,lenandis_emptyanswer the obvious questions.- Index access (
list[i]) panics on out-of-bounds;list.get(i)returnsOption<&T>and is the safer default.- A
for item in &listloop yields&T. That's usually what you want; iterating&mut listgives&mut T, and iteratinglistby value moves the items out.- A
Vec<String>is not the same as aVec<&str>. Converting between them needs.to_string()/String::from(one direction) or.as_str()(the other).