Array or Vec?¶
Level: 101 → 201 · for newcomers
One line: Use a Vec unless the length is a fact about the problem rather than about this run — and know the four things an array buys when it is, because "arrays are less flexible" undersells all of them.
const SCALE: usize = 6; // 0..=5, decided by the ballot design
let histogram = [0u32; SCALE]; // a fact about STAR, not about today
let candidates = vec!["Ada", "Ben"]; // a fact about this election
println!("{} {}", histogram.len(), candidates.len()); // 6 2
The usual advice — "if you're unsure, use a Vec" — is right, and it is right for a reason worth knowing: almost every length in a real program comes from input, and input length is not knowable when you compile. So the default follows from the data, not from Vec being the better type.
What is actually different¶
[T; N] |
Vec<T> |
|
|---|---|---|
| length | in the type — [u32; 5] and [u32; 3] are different types |
in the value, and changeable |
| where the data lives | inline, wherever the array is — usually the stack | on the heap; three numbers on the stack point at it |
size_of |
N * size_of::<T>(), no header |
pointer + length + capacity, whatever N is |
| grow / shrink | never | push, pop, insert, retain |
Copy |
yes, if T: Copy |
never |
in a const |
yes | no — allocation needs a run time |
| needs an allocator | no | yes |
size_of::<[u32; 5]>() is 20 — five values and nothing else — while size_of::<Vec<u32>>() is three pointer-sized numbers no matter how many elements it holds. Both are measured in the verified output rather than quoted.
The four things an array buys¶
1. The length is part of the type, so the wrong one cannot be passed. A function taking [u8; 32] cannot be handed 31 bytes, and a function taking [u32; 6] cannot be handed a five-bucket histogram:
error[E0308]: mismatched types
--> arrlen.rs:3:30
|
3 | println!("{}", summarise([1, 2, 3, 4, 5]));
| --------- ^^^^^^^^^^^^^^^ expected an array with a size of 6, found one with a size of 5
The equivalent Vec mistake compiles and produces a report that is merely wrong. This is the case people mean by "a hash is [u8; 32], not Vec<u8>" — the length is not a quantity, it is part of what the thing is.
2. An array of Copy types is Copy. So a snapshot costs nothing to say:
let mut live = [4u32, 0, 2];
let snapshot = live; // a real copy — no .clone(), and `live` is still usable
live[0] += 100;
println!("{live:?} {snapshot:?}"); // [104, 0, 2] [4, 0, 2]
The same two lines over a Vec are error[E0382]: borrow of moved value, and the fix is .clone() — a heap allocation you now know you made. That is the trade rather than a wart: the Vec version makes the cost visible, the array version makes it zero.
3. It works in a const. const WEIGHTS: [u32; 6] = [0, 1, 2, 3, 4, 5]; is a compile-time value with no initialisation code; const W: Vec<u32> = vec![…] cannot exist, because there is no allocator at compile time. Lookup tables, opcode tables, day-length tables — all arrays for this reason.
4. No allocator at all. Which is why no_std and embedded Rust are written in arrays, and why a hot loop that must not touch the heap uses one.
Where the Vec is simply right¶
The length came from somewhere you cannot see at compile time — and that is most lengths:
let ballots = ["5,3,0", "4,4,4"];
let n = ballots.len();
// let counts = [0u32; n]; // error[E0435]: attempt to use a non-constant value in a constant
let counts = vec![0u32; n]; // the answer
println!("{}", counts.len()); // 2
An array length is a constant, full stop. n came from the input, so vec![0; n] is not a fallback — it is the only thing that expresses what you meant. Add to that: anything that grows or shrinks, anything returned from a function whose size depends on its arguments, and anything large, since an array lives on the stack and [u8; 10_000_000] is a stack overflow rather than an error message.
The question mostly dissolves at the signature¶
fn total(counts: &[u32]) -> u32 { counts.iter().sum() }
let fixed = [4u32, 0, 2];
let grown = vec![4u32, 0, 2];
println!("{} {}", total(&fixed), total(&grown)); // 6 6
Take &[T] and both callers work — a Vec derefs to a slice, an array coerces to one. So the choice is about how you store the data, not about what your functions accept, and getting it wrong is a local decision you can change later without touching a single signature. Arrays and slices is that rule in full, and its kata is the one that shows which callers a fixed-length signature turns away.
If you are coming from another language¶
Python. There is nothing to choose here, because Python's list is Vec and there is no array in the language — [1, 2, 3] grows, and always has. The nearest counterparts to a Rust array are tuple (fixed length, but heterogeneous and immutable) and array.array / numpy.ndarray (fixed element type, still heap, still growable in NumPy's case only by copying).
The habit that transfers badly is reaching for the list every time, which in Rust means a heap allocation for a three-element lookup table that could have been a const. And one that transfers well: Python's own advice for the fixed case is tuple, for the same reason Rust's is an array — this collection's shape is part of what it is. A Python programmer who already writes RGB = (255, 0, 0) rather than a list has the instinct; in Rust it becomes const RGB: [u8; 3], and the compiler enforces it rather than relying on the reader.
| Python | Rust |
|---|---|
[1, 2, 3] — grows |
vec![1, 2, 3] |
(1, 2, 3) — fixed, heterogeneous |
(1, 2, 3) — a tuple, same idea |
| no fixed-length homogeneous type | [1u32, 2, 3] — an array |
len() on either |
.len() on either |
| a list of a million ints — a million heap objects | Vec<i64> — one allocation, a million machine words |
ABAP. The split maps onto ABAP's own, and closely enough to be useful. A Vec<T> is an internal table (DATA lt_scores TYPE STANDARD TABLE OF i) — it grows with APPEND, shrinks with DELETE, and lives on the heap. A Rust array is closer to a fixed-length structure or a TYPE c LENGTH n field: the length is part of the type, declared once, and the runtime will not let you extend it.
Two differences worth having in mind. First, ABAP has no Copy distinction — assigning an internal table copies it (with copy-on-write underneath), so the let snapshot = live; question does not arise; ABAP's version of the choice is DATA(lt_copy) = lt_orig versus FIELD-SYMBOLS / reference variables, which is borrowing, not array-versus-Vec. Second, ABAP's tables carry the feature Rust puts in a different type entirely: SORTED and HASHED tables with a KEY, which in Rust are BTreeMap and HashMap rather than a flavour of Vec. So an ABAP developer choosing "which table type" is answering a question Rust splits across three types, and the array/Vec decision is only the first third of it.
Practice¶
Two lengths in one election, and only one of them is a fact about the problem.
A STAR ballot scores 0–5 — six buckets, in every election ever held. The candidate list is different every time. Build the score histogram and the per-candidate totals, choosing the storage for each, and count four ballots into them.
Then show the three consequences of the choice. Take a snapshot of the histogram mid-count with a plain let, and say why it works here and what rustc says when you try it on the Vec. Write a total that takes the fixed length, and name the error a five-bucket argument produces. Finally, try to size the per-candidate array from candidates.len() and read what the compiler calls that.
Finish by passing both to the same function.
Solution
array_or_vec_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: one election has two lengths, and only one of them is a fact
//! about the problem rather than about this run.
const SCALE: usize = 6; // 0..=5 -- decided by the ballot design, not by the input
fn print_histogram(counts: &[u32]) -> String {
counts.iter().enumerate().map(|(s, n)| format!("{s}={n}")).collect::<Vec<_>>().join(" ")
}
fn main() {
println!("=== the two lengths ===");
println!(" the score scale is 0..=5 in every STAR election ever held -> [u32; {SCALE}]");
println!(" the candidate list is different every time -> Vec<String>");
let candidates: Vec<String> = ["Ada", "Ben", "Cara"].iter().map(|s| s.to_string()).collect();
let ballots = ["5,3,0", "4,4,4", "0,0,5", "2,5,1"];
println!(" this election: {} candidates, {} ballots", candidates.len(), ballots.len());
println!("\n=== count into the fixed-length one ===");
let mut histogram = [0u32; SCALE];
for line in ballots {
for field in line.split(',') {
histogram[field.parse::<usize>().unwrap()] += 1;
}
}
println!(" histogram = {}", print_histogram(&histogram));
println!("\n=== consequence 1: the array is Copy, so a snapshot is free ===");
let before = histogram; // no .clone(), no move
histogram[5] += 1000; // the original goes on changing
println!(" before = {}", print_histogram(&before));
println!(" after = {}", print_histogram(&histogram));
println!(" the same two lines over a Vec do not compile. rustc opens with");
println!(" error[E0382]: borrow of moved value: `live`");
println!(" and closes with");
println!(" help: consider cloning the value if the performance cost is acceptable");
println!(" between them it says the move happened because Vec<u32> is not Copy.");
println!(" So the fix is `let before = live.clone();` -- a heap allocation you");
println!(" now know you made, which is the difference the two types are for.");
histogram[5] -= 1000;
println!("\n=== consequence 2: a wrong length is a compile error, not a bad report ===");
fn total(counts: [u32; SCALE]) -> u32 { counts.iter().sum() }
println!(" total(histogram) = {}", total(histogram));
println!(" total([0u32; 5]) -> error[E0308]: mismatched types");
println!(" expected an array with a size of 6, found one with a size of 5");
println!(" a Vec of the wrong length would have produced a report that was merely wrong");
println!("\n=== consequence 3: a run-time length cannot be an array length ===");
let n = candidates.len();
let mut per_candidate = vec![0u32; n]; // the only thing that works here
for line in ballots {
for (i, field) in line.split(',').enumerate() {
per_candidate[i] += field.parse::<u32>().unwrap();
}
}
for (name, total) in candidates.iter().zip(&per_candidate) {
println!(" {name:<5} {total}");
}
println!(" `let c = [0u32; n];` -> error[E0435]: attempt to use a non-constant value in a constant");
println!(" an array length is a constant; n came from the input, so it is vec![0; n]");
println!("\n=== and both end up at the same signature ===");
println!(" print_histogram(&histogram) -> {}", print_histogram(&histogram));
println!(" print_histogram(&per_candidate) -> {}", print_histogram(&per_candidate));
println!(" one fn taking &[u32], two callers -- the decision was about storage, not API");
}
Verified output of array_or_vec_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
=== the two lengths ===
the score scale is 0..=5 in every STAR election ever held -> [u32; 6]
the candidate list is different every time -> Vec<String>
this election: 3 candidates, 4 ballots
=== count into the fixed-length one ===
histogram = 0=3 1=1 2=1 3=1 4=3 5=3
=== consequence 1: the array is Copy, so a snapshot is free ===
before = 0=3 1=1 2=1 3=1 4=3 5=3
after = 0=3 1=1 2=1 3=1 4=3 5=1003
the same two lines over a Vec do not compile. rustc opens with
error[E0382]: borrow of moved value: `live`
and closes with
help: consider cloning the value if the performance cost is acceptable
between them it says the move happened because Vec<u32> is not Copy.
So the fix is `let before = live.clone();` -- a heap allocation you
now know you made, which is the difference the two types are for.
=== consequence 2: a wrong length is a compile error, not a bad report ===
total(histogram) = 12
total([0u32; 5]) -> error[E0308]: mismatched types
expected an array with a size of 6, found one with a size of 5
a Vec of the wrong length would have produced a report that was merely wrong
=== consequence 3: a run-time length cannot be an array length ===
Ada 11
Ben 12
Cara 10
`let c = [0u32; n];` -> error[E0435]: attempt to use a non-constant value in a constant
an array length is a constant; n came from the input, so it is vec![0; n]
=== and both end up at the same signature ===
print_histogram(&histogram) -> 0=3 1=1 2=1 3=1 4=3 5=3
print_histogram(&per_candidate) -> 0=11 1=12 2=10
one fn taking &[u32], two callers -- the decision was about storage, not API
The verified output¶
Verified output of array_or_vec.rs — regenerated by tools/run_examples.py, never hand-typed.
=== what is actually different ===
size_of::<[u32; 5]>() 20
five u32 values 20 <- five values inline, no header
size_of::<Vec<u32>>() == 3 * usize true
a Vec is pointer + length + capacity on the stack; the data is elsewhere
size_of::<[u32; 5000]>() 20000
...and that whole block is on the STACK when you make one
=== case 1: the length is part of the type, so a wrong one cannot be passed ===
summarise([u32; 6]) = 23
summarise([u32; 5]) -> error[E0308]: mismatched types
expected an array with a size of 6, found one with a size of 5
a Vec<u32> of the wrong length is a run-time surprise; this is a compile error
=== case 2: an array of Copy types is Copy ===
live = [104, 0, 2, 1, 7, 9]
snapshot = [4, 0, 2, 1, 7, 9] <- untouched, and `live` is still usable
Vec needs .clone() for the same thing: [4, 0, 2]
=== case 3: an array works in a const context; a Vec does not ===
const WEIGHTS: [u32; 6] = [0, 1, 2, 3, 4, 5]
const TOP_WEIGHT = 5 <- indexed at compile time
`const W: Vec<u32> = vec![..]` does not compile: allocation needs a run time
=== case 4: no allocation at all ===
an array needs no allocator, so it is what no_std and embedded code use,
and what a hot loop uses to avoid touching the heap at all.
=== where the Vec is simply right ===
parsed 4 ballots -> counts = [3, 1, 1, 1, 3, 3]
the number of ballots is not known until the input is read, and
`let n = ballots.len(); let c = [0u32; n];` does not compile --
error[E0435]: attempt to use a non-constant value in a constant
an array length is a constant; `vec![0; n]` is the answer
=== and the question mostly dissolves at the signature ===
report(&fixed) = 0:4 1:0 2:2 3:1 4:7 5:9
report(&grown) = 0:4 1:0 2:2 3:1 4:7 5:9
one function, `&[u32]`, and both callers work -- so the choice is about
how you STORE the data, not about what your functions accept
See also¶
- Arrays and slices — the array in full, and why
&[T]is what belongs in a signature Vec— the three numbers, the doubling growth, and the two removals- Tuples — the other fixed-length option, for values of different types
- Copy vs Clone — why an array of
CopyisCopyand aVecnever is - Values — where the widths in the
size_oftable come from BTreeMapandBTreeSet— when the thing you wanted was a keyed table, not a list
Po polsku¶
Domyślną odpowiedzią jest Vec, a powód warto znać, bo nie brzmi „tablice są gorsze”: prawie każda długość w prawdziwym programie pochodzi z danych wejściowych, a długości wejścia nie da się poznać w czasie kompilacji. Tablica [T; N] ma długość w typie — [u32; 5] i [u32; 3] to dwa różne typy — leży w miejscu, w którym leży sama tablica (zwykle na stosie), nigdy nie rośnie i jest Copy, o ile T jest Copy. Vec<T> trzyma na stosie trzy liczby (wskaźnik, długość, pojemność), a dane na stercie, rośnie i kurczy się, i Copy nie jest nigdy.
Cztery rzeczy, które kupuje tablica, i żadna z nich nie jest tylko „mniejszą elastycznością”. Po pierwsze, długość jest częścią typu, więc złej nie da się przekazać: funkcja biorąca [u8; 32] nie przyjmie 31 bajtów, a błąd brzmi „expected an array with a size of 6, found one with a size of 5”. Ten sam błąd popełniony na Vec skompiluje się i wyprodukuje po prostu zły raport — dlatego skrót mieszający jest typu [u8; 32], a nie Vec<u8>: długość nie jest tam wielkością, tylko częścią tego, czym ta rzecz jest. Po drugie, tablica typów Copy sama jest Copy, więc let snapshot = live; robi prawdziwą kopię, a oryginał dalej działa; te same dwie linijki na Vec to error[E0382]: borrow of moved value i trzeba dopisać .clone() — czyli alokację, o której teraz wiadomo. Po trzecie, tablica działa w kontekście const (const WEIGHTS: [u32; 6] = …), a Vec nie, bo w czasie kompilacji nie ma alokatora — stąd wszystkie tablice przeglądowe są tablicami. Po czwarte, tablica nie potrzebuje alokatora w ogóle, dlatego pisze się nimi kod no_std i wbudowany.
Vec jest po prostu właściwy, gdy długość przyszła skądś, czego nie widać w czasie kompilacji — a to większość przypadków. let n = ballots.len(); let c = [0u32; n]; nie skompiluje się: error[E0435]: attempt to use a non-constant value in a constant. Długość tablicy jest stałą, kropka, więc vec![0; n] nie jest obejściem, tylko jedynym zapisem tego, o co chodziło. Do tego dochodzi wszystko, co rośnie lub maleje, oraz wszystko duże — tablica leży na stosie, a [u8; 10_000_000] to przepełnienie stosu, nie komunikat o błędzie.
Na koniec rzecz, która ten dylemat w dużej mierze rozpuszcza: w sygnaturze pisze się &[T] i działają oba wywołania, bo Vec dereferencjuje się do wycinka, a tablica się do niego dopasowuje. Wybór dotyczy więc sposobu przechowywania, a nie tego, co przyjmują twoje funkcje — i jest decyzją lokalną, którą można później zmienić, nie ruszając ani jednej sygnatury. Dla czytelnika z ABAP-a: Vec<T> to tabela wewnętrzna (STANDARD TABLE OF), a tablica jest bliżej struktury o stałej długości albo pola TYPE c LENGTH n. Uwaga na dwie różnice — w ABAP-ie nie ma rozróżnienia Copy, bo przypisanie tabeli kopiuje ją (z kopiowaniem przy zapisie pod spodem), a odpowiednikiem pożyczania są FIELD-SYMBOLS i zmienne referencyjne; oraz tabele SORTED i HASHED z kluczem to w Ruscie nie odmiany Vec, tylko osobne typy — BTreeMap i HashMap. Wybór „rodzaju tabeli” w ABAP-ie odpowiada więc pytaniu, które Rust rozdziela na trzy typy, a dylemat tablica-czy-Vec jest tylko pierwszą jego trzecią.
Szukaj po polsku: tablica a wektor · długość w typie · typ Copy · tablica przeglądowa const · rust array vs Vec · rust E0435 non-constant value