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 chapter 4: &[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.
The simplest possible operation on a vector is to ask it how many items it currently holds.
Notice the parameter is &Vec<String>: a borrow, so the caller keeps
ownership of the list.
Useful from the standard library
Vec::lenreturns the number of items as ausize. Constant time, no allocation.Vec::is_emptyreads better thanlen() == 0when that's all you need to know.
Press
Ctrl/Cmd + Enterto run the tests without leaving the keyboard. The Run button does the same thing.
Now we modify the list in place. Where the previous step only read
from the Vec, this one needs to change it. The &mut Vec<String>
says "I need exclusive access for a moment", and that's what lets us
push a new item onto the end.
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.
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.
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 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.
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).