“See that vec of vecs over there?”
“Yeah. Can’t stand it.”
“Why?”
“It’s full of itself.”
A [u8; 4] has room for exactly four bytes; that's it.
When the number of items can vary, use a Vec<T> instead. It stores a sequence
of values that can grow or shrink, without asking you to predict the final
length.
To see why a Vec can grow, compare it with an 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 arrays have no push method: [u8; 4] holds exactly
four elements, wherever it is stored.
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 requests more space and may move the elements to a new
allocation. 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] | Inline in its owner (often on the 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.
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 follows the same rule as &str in function parameters. A &[i32]
parameter 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, without an
explicit 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, so you can handle a missing element without panicking.
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. It takes&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");
}
You only need to read the list here, comparing each element against the item
you're looking for. The Vec holds Strings, but we're searching with a
&str.
The parameter is &[String], a shared slice, because searching doesn't need to
grow the list. It accepts borrowed vectors and arrays without copying their
elements; the tests call it with both. An &Vec<String> parameter would
unnecessarily rule out the array caller.
Useful from the Standard Library
<[T]>::containstakes a reference to the element type: its signature isfn contains(&self, x: &T) -> bool. Here that's&String, while the parameter is&str. A loop lets you compare the strings without allocating a temporaryString.- A
for entry in listloop yields&Stringon each iteration. Compareentry == itemdirectly; no temporaryStringor.as_str()call is 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: &[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, "banana"), true);
assert_eq!(contains_item(&list, "orange"), false);
assert_eq!(contains_item(&[], "apple"), false);
let pantry = ["rice".to_string(), "beans".to_string()];
assert_eq!(contains_item(&pantry, "beans"), true);
}
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.Vec::pushappends one item.str::to_stringcopies the text into an ownedString, as inadd_item.
/// 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 read a vector through a shared borrow and changed one through a mutable
borrow. You also built a fresh Vec<String> 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>,lenreturns the number of items, andis_emptytells you whether there are any.- 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).
Extra practice to explore at your own pace. These chapters do not count toward course progress.