If you stare at a problem for long enough, it starts to 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 this array is a local variable with a compile-time length, its elements can live directly on the stack alongside the function's other local data.
Setting aside that stack space is cheap, and Rust reclaims it 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.
The local Vec value is a small header containing a pointer, a length, and a capacity, while the allocator provides the buffer it points to.
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 |
If you're coming from Python or Java, Vec<T> is the closer match for the lists you use every day.
In C, the same choice is closer to picking a fixed-size array or managing an allocation yourself.
Rust gives you both choices, and its ownership rules apply to either one.
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.
You can start with an empty vector or with its initial items:
let mut empty: Vec<i32> = Vec::new();
let with_items = vec![1, 2, 3]; // vec! starts with these three items
Changing a vector requires mutable access, while reading it only needs a shared borrow:
let mut list = vec!["bread"];
list.push("milk"); // requires `mut`
let count = list.len(); // borrow without mut
When you choose a parameter type, start from what the function needs to do:
&[T]) as input when the function only needs to read the data.
This is the same idea as the &str rule you met with functions: &[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.
Here you change the list in place by pushing 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 you 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);
}
This time the input and output hold different string types: each input is a borrowed &str, while the output must own its Strings.
That means every item needs to become an owned String before it can live in the result.
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.- Choose the parameter from the operation:
&[T]to read,&mut Vec<T>to add or remove, and plainVec<T>to consume the whole vector.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).