Vec and String
Vec<T> — Growable Heap Array
Section titled “Vec<T> — Growable Heap Array”Vec<T> is Rust’s workhorse sequential collection. It stores elements of type T contiguously on the heap and automatically resizes when you push beyond its current capacity.
let mut v: Vec<i32> = Vec::new(); // empty Veclet w = vec![1, 2, 3]; // vec! macro shorthandPushing, Popping, and Indexing
Section titled “Pushing, Popping, and Indexing”let mut v = vec![10, 20, 30];
v.push(40); // append an elementlet last = v.pop(); // remove and return the last element → Some(40)let first = v[0]; // index — panics if out of boundslet safe = v.get(100); // returns Option<&T> — None if out of boundsPrefer .get(i) when the index might be out of range so you handle the None case instead of panicking.
Capacity and Growth, Measured
Section titled “Capacity and Growth, Measured”A Vec<T> tracks two numbers separately: len() (how many elements it currently holds) and capacity() (how many elements it can hold before it must allocate a bigger buffer). Vec::with_capacity(n) pre-allocates room for n elements up front — useful when you know roughly how big the Vec will get and want to avoid repeated reallocation.
flowchart LR A[push exceeds capacity] --> B[allocate a new, larger buffer] B --> C[copy every existing element into it] C --> D[free the old buffer] D --> E[push the new element]
fn main() { let mut v: Vec<i32> = Vec::with_capacity(2); println!("start: len={} cap={}", v.len(), v.capacity()); for i in 0..6 { v.push(i); println!("after push {}: len={} cap={}", i, v.len(), v.capacity()); } v.shrink_to_fit(); println!("after shrink_to_fit: len={} cap={}", v.len(), v.capacity());}This is a real, verified run — not the documented API contract (the standard library does not promise a specific growth factor), but the actual behavior of this toolchain:
start: len=0 cap=2after push 0: len=1 cap=2after push 1: len=2 cap=2after push 2: len=3 cap=4after push 3: len=4 cap=4after push 4: len=5 cap=8after push 5: len=6 cap=8after shrink_to_fit: len=6 cap=6Capacity doubles each time len() would exceed it — 2 → 4 → 8 — so pushing one element past capacity is amortized O(1): most pushes are free, and the occasional reallocation-and-copy is spread out over all the pushes that led up to it. shrink_to_fit() releases the unused slack (cap 8 → 6) once you know the Vec will not grow again.
Iterating a Vec: by Value, by Reference, by Mutable Reference
Section titled “Iterating a Vec: by Value, by Reference, by Mutable Reference”The same loop body reads very differently depending on what you iterate over:
fn main() { let v = vec![String::from("a"), String::from("b"), String::from("c")];
// by shared reference (&v): borrows, v still valid after for s in &v { print!("{} ", s); } println!("| still valid, len={}", v.len());
// by mutable reference (&mut v): borrows mutably, mutate elements in place let mut nums = vec![1, 2, 3]; for n in &mut nums { *n *= 10; } println!("{:?}", nums);
// by value (v): moves the Vec, v cannot be used after this loop for s in v { print!("{} ", s); } println!("(v moved)");}a b c | still valid, len=3[10, 20, 30]a b c (v moved)for s in &v borrows each element as &T; the Vec is untouched and usable afterward. for n in &mut v borrows each element as &mut T, so *n *= 10 mutates the original Vec in place. for s in v calls v.into_iter() implicitly, which takes ownership of the Vec and yields owned T values — after this loop, v no longer exists. Reach for &v by default; use &mut v only when you need to mutate elements in place; use v only when you are done with the Vec and want its elements moved out.
String — Owned, Growable Text
Section titled “String — Owned, Growable Text”String is Rust’s heap-allocated, mutable text type. It owns its bytes and manages deallocation.
let mut s = String::from("hello");s.push(' '); // push a single chars.push_str("world"); // append a string slicelet combined = s + " again"; // + moves s and appendsString vs &str
Section titled “String vs &str”String | &str | |
|---|---|---|
| Ownership | Owned | Borrowed slice |
| Location | Heap | Stack / static / inside a String |
| Mutable | Yes (with mut) | No |
| Typical use | Building/modifying text | Reading text |
A &String automatically coerces to &str, so prefer &str in function parameters for maximum flexibility.
fn shout(s: &str) -> String { s.to_uppercase()}
let owned = String::from("hello");shout(&owned); // &String coerces to &strshout("world"); // string literal is already &strThe Same Smell, One Type Over: Vec<String> vs &[String] / &[&str]
Section titled “The Same Smell, One Type Over: Vec<String> vs &[String] / &[&str]”The String-vs-&str decision above has a direct sibling one layer up the type: a function parameter of Vec<String> forces every caller to hand over ownership (or .clone() the whole Vec) even when the function only reads the elements. &[String] (or &[&str] if callers already have string slices) borrows instead, and still accepts &some_vec at the call site because Vec<String> derefs to &[String]:
fn print_lines_bad(lines: Vec<String>) { for line in &lines { println!("{}", line); }}
fn print_lines_good(lines: &[String]) { for line in lines { println!("{}", line); }}
fn main() { let owned = vec![String::from("a"), String::from("b")]; print_lines_good(&owned); println!("owned still usable: {:?}", owned); print_lines_bad(owned); // must move — cannot reuse afterward}len() Is Bytes, Not Characters
Section titled “len() Is Bytes, Not Characters”String is UTF-8, so .len() returns the number of bytes, not the number of characters. For ASCII text the two happen to match, which hides the difference until you meet multi-byte text — every Thai character below is 3 bytes:
fn main() { let s = "สวัสดี"; println!("len() = {}", s.len()); // bytes println!("chars().count() = {}", s.chars().count()); // characters}This prints len() = 18 and chars().count() = 6 — 6 characters, 18 bytes.
Byte-range slicing (&s[start..end]) indexes into that byte buffer directly. If start or end does not land on a character boundary, Rust panics rather than hand you a half-character:
// @expect-panicfn main() { let s = "สวัสดี"; let bad = &s[..1]; // byte index 1 lands inside the first character println!("{}", bad);}thread 'main' (<id>) panicked at src/main.rs:4:17:end byte index 1 is not a char boundary; it is inside 'ส' (bytes 0..3 of string)note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceReach for one of these instead of guessing at byte offsets:
fn main() { let s = "สวัสดี";
// char_indices() gives you (byte_offset, char) pairs — always on a boundary if let Some((offset, ch)) = s.char_indices().next() { println!("first char '{}' starts at byte {}", ch, offset); }
// chars().nth(n) gets the nth character without slicing bytes println!("2nd char = {:?}", s.chars().nth(1));
// is_char_boundary(i) lets you check before you slice println!("is_char_boundary(1) = {}", s.is_char_boundary(1)); println!("is_char_boundary(3) = {}", s.is_char_boundary(3));}Index Loop vs Iterator
Section titled “Index Loop vs Iterator”An index loop (for i in 0..v.len() { ... v[i] ... }) is what most languages without a first-class iterator protocol teach you to reach for. In Rust it is almost always the wrong default: it re-checks bounds on every v[i], obscures the intent (“walk every element” becomes “count from 0 to len, then index”), and clippy calls it out by name:
fn main() { let v = vec![10, 20, 30]; let mut total = 0; for i in 0..v.len() { total += v[i]; } println!("{}", total);}$ cargo clippywarning: the loop variable `i` is only used to index `v` --> src/main.rs:4:14 |4 | for i in 0..v.len() { | ^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.96.0/index.html#needless_range_loop = note: `#[warn(clippy::needless_range_loop)]` on by defaulthelp: consider using an iterator |4 - for i in 0..v.len() {4 + for <item> in &v { |clippy::needless_range_loop fires whenever the loop variable is used only to index the same collection — the fix is always for item in &v (or .iter().sum() here specifically), which the compiler can bounds-check once instead of once per access, and which reads as “for every item” instead of “for every index into this specific Vec”.
fn main() { // Vec<i32> basics let mut nums: Vec<i32> = Vec::new(); nums.push(10); nums.push(20); nums.push(30); println!("len={}, first={}", nums.len(), nums[0]);
// pop returns Option<T> let last = nums.pop(); println!("popped: {:?}, remaining: {:?}", last, nums);
// safe get returns Option<&T> println!("get(5) = {:?}", nums.get(5));
// iterate by reference — nums still valid after for n in &nums { println!("item: {}", n); } println!("nums still valid: {:?}", nums);
// String vs &str let literal: &str = "hello"; let mut owned: String = String::from(literal); owned.push_str(" world"); println!("{}", owned);
// slice into a String let slice: &str = &owned[0..5]; println!("slice: {}", slice);}Compiling…
Mental Model
Section titled “Mental Model”A Vec<T> is three words on the stack — a pointer, a length, and a capacity — pointing at a single contiguous buffer on the heap. len is how much of that buffer is “live”; capacity is how much is actually allocated. Every push either writes into already-allocated slack (cheap) or triggers “allocate bigger, copy everything, free the old buffer” (expensive, but rare because capacity doubles). A String is the exact same three-word shape, specialized to hold UTF-8 bytes instead of an arbitrary T — which is why .len() counts bytes: it is reading the same field a Vec<u8> would report.
Trade-offs
Section titled “Trade-offs”Vec::with_capacity(n) trades an up-front allocation for avoiding N-ish reallocations later — worth it whenever you know (or can estimate) the final size before the loop that fills it. shrink_to_fit() trades a possible reallocation now for a smaller long-term memory footprint — worth it for a Vec you are about to store for a long time, not for one you are about to grow again. Byte indexing into a String (&s[a..b]) is the fastest possible slice operation (no scanning), but it pushes the “did I land on a character boundary” responsibility entirely onto you; chars()/char_indices() do that scanning for you at the cost of walking the string.
Common Misconceptions
Section titled “Common Misconceptions”- “
Vec::new()andVec::with_capacity(0)behave differently.” They don’t — both start with zero capacity and allocate on the first push;with_capacityonly matters when you pass a number greater than zero. - “
.len()on aStringcounts characters, since Rust strings are Unicode-aware.” It counts bytes. Rust strings are UTF-8-aware, which is precisely why.len()must report bytes — a single “character” position doesn’t correspond to a fixed byte width. - “An index loop (
for i in 0..v.len()) is more explicit and therefore safer thanfor x in &v.” It is strictly less safe in practice: it is the only style capable of causing a bounds panic from a typo (v[i + 1]), and clippy flags it as unnecessary precisely becausefor x in &vbounds-checks nothing because it never computes an out-of-range index in the first place.
Reading AI-generated Code
Section titled “Reading AI-generated Code”AI-generated code defaults to index loops constantly, likely because they are the most common pattern across all the non-Rust code in its training data — but in Rust they are both slower (redundant bounds checks) and a clippy warning waiting to fire.
🐛 Spot the bug
fn sum_all(values: &[i32]) -> i32 { let mut total = 0; for i in 0..values.len() { total += values[i]; } total}Show the answer
i exists only to compute values[i] — the loop never uses the index for anything else. This is exactly the shape clippy::needless_range_loop warns about: total += values[i] re-derives a bounds-checked access on every iteration for no benefit. The fix is for v in values { total += v; }, or simply values.iter().sum() — both let the iterator handle traversal and the compiler generate tighter code.