Arrays and slices¶
Level: 101 → 201 · for newcomers
One line: [T; N] is a separate type for every length, which is why you almost never write it in a signature — &[T] moves the length out of the type and into the value, and one function then serves every caller.
fn total(scores: &[u32]) -> u32 {
scores.iter().sum()
}
fn main() {
let five = [5u32, 3, 0, 4, 2];
let three = [1u32, 2, 3];
println!("{} {}", total(&five), total(&three)); // 14 6
println!("{}", total(&five[1..3])); // 3
}
One function, three callers, and the third is part of an array it does not own.
The array: length in the type¶
[u32; 5] and [u32; 3] are as different as u32 and String. The elements sit inline — size_of::<[u32; 5]>() is 20, five values and no header — and if T: Copy then the array is Copy too, so let mut sorted = five; copies all five and leaves the original alone.
The slice: length in the value¶
| size | holds | |
|---|---|---|
&[u32; 5] |
8 | a pointer |
&[u32] |
16 | a pointer and a length |
That second row is a fat pointer, and it is the whole trick. The length stopped being compile-time information and became a number carried alongside the address, so a slice can point at an array, a Vec, or a run of either.
Ranges are half-open — five[1..3] is indices 1 and 2 — and .. on its own is the whole thing. &v[..] is the idiom for "this Vec, as a slice".
Out of bounds is a panic, not a wrong answer¶
fn main() {
let five = [5u32, 3, 0, 4, 2];
println!("{:?}", five.get(9)); // None
println!("{:?}", five.get(1)); // Some(3)
}
five[9] does not compile at all — rustc constant-folds the index and refuses with "this operation will panic at runtime", under the deny-by-default unconditional_panic lint. With an index it cannot see, the check happens at run time and the program aborts.
The split is the same one HashMap makes: [i] is a claim, .get(i) is a question. C reads past the end and carries on with whatever was there; Rust stops. Every indexing operation costs a comparison, which the optimizer removes wherever it can prove the index is in range — which is most of the time, and is why for x in &arr is faster than for i in 0..arr.len().
The methods are on the slice¶
Almost nothing is defined on the array type itself. first, last, contains, sort, windows, chunks, split_at ↗, iter — all of these live on [T], and the array and Vec reach them by deref coercion. So learning slice methods once covers both, and the slice methods reference gives each of the everyday ones a page.
fn main() {
let five = [5u32, 3, 0, 4, 2];
println!("{:?}", five.windows(2).collect::<Vec<_>>());
// [[5, 3], [3, 0], [0, 4], [4, 2]]
println!("{:?}", five.chunks(2).collect::<Vec<_>>());
// [[5, 3], [0, 4], [2]]
}
windows overlaps and never yields a short one; chunks does not overlap and the last one may be short. Reaching for the wrong one is the most common off-by-one in this corner of std.
Two of them answer nothing. sort and reverse write back into the array and return (), so println!("{:?}", a.reverse()) prints () — clean compile, no warning, and the array reversed behind you. The forms that bind or chain the receipt are caught, by clippy and by rustc respectively; the printed form is caught by neither, which is the kata below.
The trap: &Vec<T> in a signature¶
Writing fn total(scores: &Vec<u32>) compiles and looks equivalent. It is not: it refuses arrays, refuses slices, refuses &v[1..], and buys nothing at all, because the only things it can do with a &Vec are the things &[T] already offers. Take &[T] unless you need to push. Clippy has a lint for it (ptr_arg), which is how most people find out.
The mutable half, and a sentence to disbelieve¶
Vec's own documentation says "A Vec can be mutable. On the other hand, slices are read-only objects." Read that as being about &[T] specifically, because as a statement about slices it is false, and believing it costs you the better half of the parameter rule above.
fn zero_the_lowest(scores: &mut [u32]) {
if let Some(slot) = scores.iter_mut().min() { *slot = 0; }
}
That takes a slice, and it writes. So do sort, reverse, fill, swap and iter_mut — half the slice methods reference, in fact, and sort on an array works only because the array coerces to a &mut [T] first.
The line is length, not writing. A slice can reorder and overwrite every element it can see; it can never add or remove one, because the capacity — the number that would have to change — was left behind with the Vec. So the parameter rule has two halves that are the same rule:
| the function | the parameter |
|---|---|
| reads | &[T] |
| reorders, overwrites, fills | &mut [T] |
| pushes, removes, resizes | &mut Vec<T> |
Only the third one has any business naming Vec, and that is the whole of when &mut Vec<T> is right.
If you are coming from another language¶
- Python. A Python list is Rust's
Vec, and Rust's array is the thing Python does not have — a fixed-length, stack-allocated block whose length the compiler knows. The slicing syntax is nearly identical and half-open in both,xs[1:3]versus&xs[1..3], so the off-by-one instincts transfer. Two real differences: a Python slice copies, and a Rust slice borrows —&v[1..]is a view intov, sovcannot be mutated while it is alive, which is a compile error rather than the aliasing surprise it would be in Python. And negative indices do not exist:xs[-1]isxs.last(), which returnsOptionbecause the list may be empty, and thatOptionis Python'sIndexErrormoved from run time to the type. - ABAP. An internal table is closest to
Vec, and[T; N]has no real counterpart — the nearest thing is a fixed-size field likeTYPE c LENGTH 5, where the length is likewise part of the type. What transfers well is the reading habit:READ TABLE itab INDEX isetssy-subrcand you check it, which is exactly.get(i)returningOption. Rust'sarr[i]is the version that skips the check and dumps instead —TABLE_INVALID_INDEXis the same failure with a different name. The genuine difference is the slice: ABAP has no borrowed view over part of a table, so a helper that works on a range takesFROM/TOindices and works on the whole table, which is exactly the aliasing bug slices exist to make impossible. - C. An array decaying to a bare pointer is what a slice fixes.
void total(uint32_t *scores, size_t n)is a slice split into two arguments that the compiler cannot check agree;&[u32]is those two arguments welded into one value. Every buffer overrun that has ever come from passing the wrongnis unrepresentable here. - Java / C#.
int[]carries its own length, so it is closer to a slice than to a C array — but there is no view type, so a sub-range meansArrays.copyOfRange(a copy) or passing offsets around.Span<T>in modern C# is Rust's slice, arrived at from the same direction.
The verified output¶
Verified output of arrays_and_slices.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The length is part of the type
five : [u32; 5] = [5, 3, 0, 4, 2]
three : [u32; 3] = [1, 2, 3]
[u32; 5] and [u32; 3] are as different as u32 and String. A
function taking [u32; 5] rejects a four-element array.
size_of::<[u32; 5]>() = 20 — five values, no header
2. A slice is a view: pointer plus length, and no ownership
size_of::<&[u32; 5]>() = 8 — a plain pointer
size_of::<&[u32]>() = 16 — pointer AND length
The length moved out of the type and into the value, which is
why one function can serve every length.
total(&five) = 14, total(&three) = 6
total(&five[1..3]) = 3 — same function, part of the array
3. Ranges are half-open: the end is not included
five[1..3] = [3, 0] (indices 1 and 2)
five[..2] = [5, 3] five[3..] = [4, 2]
five[..] = [5, 3, 0, 4, 2] — the whole thing, as a slice
4. Out of bounds is a panic, not a wrong answer
five[9] does not even compile — rustc constant-folds the index
and refuses: `error: this operation will panic at runtime`.
five[i] where i = 9 -> panicked
five.get(9) -> None
five.get(1) -> Some(3)
`[i]` asserts the index is in range; `.get(i)` asks. C reads
past the end and keeps going; Rust stops the program.
5. The methods live on the slice, so the array gets them free
five.first() = Some(5), five.last() = Some(2)
five.contains(&4) = true
a copy, sorted: [0, 2, 3, 4, 5] (the original is still [5, 3, 0, 4, 2])
`five` is Copy because u32 is, so `let mut sorted = five` copied it.
windows(2): [[5, 3], [3, 0], [0, 4], [4, 2]]
chunks(2): [[5, 3], [0, 4], [2]]
6. A slice is not read-only — but its length is fixed
zero_the_lowest(&mut [u32]) on an array: [5, 3, 9, 4, 0]
...and the same function on a Vec: [5, 3, 9, 4, 0]
...and on part of one:
zero_the_lowest(&mut v[..2]) [5, 0, 9, 4, 0]
&mut [T] is a slice too, so "slices are read-only" is wrong:
sort, reverse, fill and iter_mut all write through one. The
line is LENGTH, not writing — a slice can change every element
it can see and can never add or remove one. Which is why a
function that sorts takes &mut [T] and only a function that
pushes needs &mut Vec<T>.
Practice¶
One function, four callers, and the signature that turns three away. Write average twice: once taking &[u32; 5], once taking &[u32]. Then call each with a five-element array, a two-element array, a Vec, and a sub-range of the first array, and write down which calls compile.
The slice version has a case the fixed version does not: the length can be zero. Decide what it returns for an empty slice, and — before you look — write down what the unguarded version prints for 0 / 0 in floating point, because it does not panic and it does not stop.
Solution
arrays_and_slices_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: one function, four callers, and the signature that rejects three.
//!
//! rustc --edition 2024 arrays_and_slices_kata.rs -o /tmp/aask && /tmp/aask
/// The signature that only ever serves one caller.
fn average_fixed(scores: &[u32; 5]) -> f64 {
f64::from(scores.iter().sum::<u32>()) / 5.0
}
/// The signature to write instead.
fn average(scores: &[u32]) -> Option<f64> {
if scores.is_empty() {
return None;
}
Some(f64::from(scores.iter().sum::<u32>()) / scores.len() as f64)
}
/// Runs of equal values, without allocating a Vec per run.
fn longest_run(scores: &[u32]) -> usize {
let mut best = 0;
let mut current = 0;
let mut previous: Option<u32> = None;
for &s in scores {
current = if Some(s) == previous { current + 1 } else { 1 };
previous = Some(s);
best = best.max(current);
}
best
}
fn main() {
let ballot: [u32; 5] = [5, 3, 3, 3, 2];
let short = [4u32, 4];
let owned: Vec<u32> = vec![1, 2, 3, 4, 5, 6];
let empty: [u32; 0] = [];
println!("1. The fixed-length signature, and who it turns away");
println!(" average_fixed(&ballot) = {:.2}", average_fixed(&ballot));
println!(" average_fixed(&short) does not compile:");
println!(" expected `&[u32; 5]`, found `&[u32; 2]` [E0308]");
println!(" average_fixed(&owned) does not compile either — a Vec is not");
println!(" an array, however many elements it happens to hold.");
println!();
println!("2. The slice signature, and the same four callers");
println!(" average(&ballot) = {:?}", average(&ballot).map(|a| (a * 100.0).round() / 100.0));
println!(" average(&short) = {:?}", average(&short));
println!(" average(&owned) = {:?}", average(&owned));
println!(" average(&ballot[1..]) = {:?}", average(&ballot[1..]).map(|a| (a * 100.0).round() / 100.0));
println!(" average(&empty) = {:?} <- the length can be zero, so the", average(&empty));
println!(" function has to say what it does about that. `&[u32; 5]` never");
println!(" had to, which is the one thing it bought.");
println!();
println!("3. `len()` is a run-time value, so the empty case is a real case");
println!(" The fixed version divides by the 5 in its own type and cannot");
println!(" be handed nothing. The slice version divides by len(), so 0/0");
println!(" is reachable: in floating point that is NaN, silently.");
let bad = f64::from(empty.iter().sum::<u32>()) / empty.len() as f64;
println!(" without the guard: {bad} <- prints, compares false to itself");
println!(" with the guard: {:?}", average(&empty));
println!();
println!("4. What the slice methods give you for free");
println!(" longest_run({ballot:?}) = {}", longest_run(&ballot));
println!(" same, via windows(2): {}", 1 + ballot.windows(2).filter(|w| w[0] == w[1]).count());
println!(" (that shortcut is only right because this array has ONE run of");
println!(" repeats — count adjacent equal pairs and you have counted every");
println!(" run at once, not the longest. windows is a tool, not an answer.)");
println!(" ballot.split_at(2) = {:?}", ballot.split_at(2));
println!(" ballot.iter().rev().collect::<Vec<_>>() = {:?}", ballot.iter().rev().collect::<Vec<_>>());
}
Verified output of arrays_and_slices_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The fixed-length signature, and who it turns away
average_fixed(&ballot) = 3.20
average_fixed(&short) does not compile:
expected `&[u32; 5]`, found `&[u32; 2]` [E0308]
average_fixed(&owned) does not compile either — a Vec is not
an array, however many elements it happens to hold.
2. The slice signature, and the same four callers
average(&ballot) = Some(3.2)
average(&short) = Some(4.0)
average(&owned) = Some(3.5)
average(&ballot[1..]) = Some(2.75)
average(&empty) = None <- the length can be zero, so the
function has to say what it does about that. `&[u32; 5]` never
had to, which is the one thing it bought.
3. `len()` is a run-time value, so the empty case is a real case
The fixed version divides by the 5 in its own type and cannot
be handed nothing. The slice version divides by len(), so 0/0
is reachable: in floating point that is NaN, silently.
without the guard: NaN <- prints, compares false to itself
with the guard: None
4. What the slice methods give you for free
longest_run([5, 3, 3, 3, 2]) = 3
same, via windows(2): 3
(that shortcut is only right because this array has ONE run of
repeats — count adjacent equal pairs and you have counted every
run at once, not the longest. windows is a tool, not an answer.)
ballot.split_at(2) = ([5, 3], [3, 3, 2])
ballot.iter().rev().collect::<Vec<_>>() = [2, 3, 3, 3, 5]
The call that printed its own receipt. Reverse a three-element array inside a println! — println!("{:?}", a.reverse()) — and predict what it prints before you run it. Then write the same mistake two more ways: bind it (let x = a.reverse();) and chain off it (a.reverse().len()). Compile all three with -W warnings, then again under clippy::all, and record which tool catches which. One of the three is caught by nobody, and it is the one you wrote first.
Then say why. Two mechanisms are doing the hiding, and only one of them is about (): work out which trait {:?} needs that {} does not, compile the {} version, and read rustc's note: line — it recommends the change that turns its own error into the silent bug. Finally, fix it three ways, and explain why the third (let mut snapshot = original;) is available here but not for a Vec.
Solution
the_silent_receipt_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: the receipt is caught when you keep it, not when you print it.
//!
//! rustc --edition 2024 the_silent_receipt_kata.rs -o /tmp/tsrk && /tmp/tsrk
fn main() {
println!("1. The call that printed its own receipt");
let mut a = ['x', 'c', 'z'];
println!(" println!(\"{{:?}}\", a.reverse()) prints {:?}", a.reverse());
println!(" and a is now {a:?} — it DID reverse. You printed the receipt.");
println!(" reverse() writes the answer back into the receiver and hands");
println!(" you (), the unit value. Nothing was lost; nothing was returned.");
println!();
println!("2. Three ways to make the same mistake, and who catches each");
println!(" a.reverse().len() rustc, immediately:");
println!(" error[E0599]: no method named `len` found for unit type `()`");
println!(" let x = a.reverse(); clippy, not rustc:");
println!(" warning: this let-binding has unit value [let_unit_value]");
println!(" println!(\"{{:?}}\", a.reverse()) NOBODY.");
println!(" Verified on rustc 1.98.0: silent under -W warnings, and silent");
println!(" under clippy::all + pedantic + nursery. The lint is on the LET,");
println!(" so calling inside the println! steps around the one tool that");
println!(" would have told you.");
println!();
println!("3. Why it prints instead of failing — and why an ARRAY makes it likelier");
println!(" () implements Debug, so {{:?}} accepts it. It does NOT implement");
println!(" Display, so println!(\"{{}}\", a.reverse()) is E0277 and safe:");
println!(" error[E0277]: `()` doesn\'t implement `std::fmt::Display`");
println!(" note: in format strings you may be able to use `{{:?}}` instead");
println!(" Read that note again. The one diagnostic standing between you");
println!(" and the silent version RECOMMENDS the silent version — it is");
println!(" answering a formatting question, and it is right about that.");
println!(" An array does not implement Display either, so printing one");
println!(" REQUIRES {{:?}} anyway. The formatter a beginner is pushed into");
println!(" from both directions is the one that swallows the unit value.");
println!();
println!("4. Nor is #[must_use] the missing guard");
println!(" must_use fires when a return value is DISCARDED. Here it was not");
println!(" discarded — it was printed, and the real answer went into the");
println!(" receiver. There is no value being ignored for a lint to notice.");
println!();
println!("5. The three right answers");
let mut b = ['x', 'c', 'z'];
b.reverse();
println!(" mutate, then look: b.reverse(); b -> {b:?}");
let c = ['x', 'c', 'z'];
let backwards: Vec<char> = c.iter().rev().copied().collect();
println!(" ask for a new one: c.iter().rev() -> {backwards:?}, c still {c:?}");
let original = ['x', 'c', 'z'];
let mut snapshot = original;
snapshot.reverse();
println!(" copy, then mutate: snapshot {snapshot:?}, original {original:?}");
println!(" That third one is an ARRAY privilege: [char; 3] is Copy, so");
println!(" `let mut snapshot = original;` duplicates all three elements and");
println!(" the original keeps its name. A Vec is not Copy — the same line");
println!(" MOVES it, and you need .clone() to get the snapshot back.");
println!();
println!("6. The same split, named twice in std");
println!(" in place, returns () returns a new value");
println!(" --------------------- -----------------------------");
println!(" slice::reverse Iterator::rev");
println!(" slice::sort (collect the sorted iterator)");
println!(" str::make_ascii_uppercase str::to_ascii_uppercase");
println!(" Vec::push / clear / dedup Vec::iter().filter().collect()");
println!(" The naming is the tell: an imperative verb mutates, and the");
println!(" to_/into_/iter_ forms hand something back.");
}
Verified output of the_silent_receipt_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The call that printed its own receipt
println!("{:?}", a.reverse()) prints ()
and a is now ['z', 'c', 'x'] — it DID reverse. You printed the receipt.
reverse() writes the answer back into the receiver and hands
you (), the unit value. Nothing was lost; nothing was returned.
2. Three ways to make the same mistake, and who catches each
a.reverse().len() rustc, immediately:
error[E0599]: no method named `len` found for unit type `()`
let x = a.reverse(); clippy, not rustc:
warning: this let-binding has unit value [let_unit_value]
println!("{:?}", a.reverse()) NOBODY.
Verified on rustc 1.98.0: silent under -W warnings, and silent
under clippy::all + pedantic + nursery. The lint is on the LET,
so calling inside the println! steps around the one tool that
would have told you.
3. Why it prints instead of failing — and why an ARRAY makes it likelier
() implements Debug, so {:?} accepts it. It does NOT implement
Display, so println!("{}", a.reverse()) is E0277 and safe:
error[E0277]: `()` doesn't implement `std::fmt::Display`
note: in format strings you may be able to use `{:?}` instead
Read that note again. The one diagnostic standing between you
and the silent version RECOMMENDS the silent version — it is
answering a formatting question, and it is right about that.
An array does not implement Display either, so printing one
REQUIRES {:?} anyway. The formatter a beginner is pushed into
from both directions is the one that swallows the unit value.
4. Nor is #[must_use] the missing guard
must_use fires when a return value is DISCARDED. Here it was not
discarded — it was printed, and the real answer went into the
receiver. There is no value being ignored for a lint to notice.
5. The three right answers
mutate, then look: b.reverse(); b -> ['z', 'c', 'x']
ask for a new one: c.iter().rev() -> ['z', 'c', 'x'], c still ['x', 'c', 'z']
copy, then mutate: snapshot ['z', 'c', 'x'], original ['x', 'c', 'z']
That third one is an ARRAY privilege: [char; 3] is Copy, so
`let mut snapshot = original;` duplicates all three elements and
the original keeps its name. A Vec is not Copy — the same line
MOVES it, and you need .clone() to get the snapshot back.
6. The same split, named twice in std
in place, returns () returns a new value
--------------------- -----------------------------
slice::reverse Iterator::rev
slice::sort (collect the sorted iterator)
str::make_ascii_uppercase str::to_ascii_uppercase
Vec::push / clear / dedup Vec::iter().filter().collect()
The naming is the tell: an imperative verb mutates, and the
to_/into_/iter_ forms hand something back.
See also¶
Vec— the growable one, which derefs to exactly the slice type on this page- Grids and nested
Vecs —[[T; N]; M]againstVec<Vec<T>>, andchunksturning one block back into rows - Tuples — the other built-in compound type, for fields of different types
- String slices —
&stris&[u8]with a promise about its contents, and the same half-open ranges - Borrowing — why a slice cannot outlive what it points at
- The unit type
()— whatsortandreversehand back, and why nothing complains - Stack and heap — where the array's elements actually are
Sources¶
Primitives: Arrays and Slices ↗ in Rust by Example; the slice ↗ and array ↗ primitive pages in std, which are where the method list actually lives.
Po polsku¶
Tablica (array) [T; N] nosi długość w typie: [u32; 5] i [u32; 3] to dwa osobne typy, tak samo różne jak u32 i String. To pierwsza rzecz, która zaskakuje po Pythonie czy Javie, gdzie tablica jest jedna, a długość to zwykła właściwość obiektu. Elementy leżą w niej jeden przy drugim, bez żadnego nagłówka (size_of::<[u32; 5]>() to 20), najczęściej na stosie — a jeśli T: Copy, to cała tablica też jest Copy, więc let mut sorted = five; kopiuje pięć wartości i zostawia oryginał nietknięty. Odpowiednikiem, który rośnie, jest wektor (Vec), a nie tablica.
Wycinek (slice) &[T] przenosi długość z typu do wartości i na tym polega cały trik tej lekcji. &[u32; 5] zajmuje 8 bajtów (sam adres), a &[u32] szesnaście: adres i długość — stąd angielska nazwa fat pointer. Dzięki temu jedna funkcja fn total(scores: &[u32]) obsługuje tablicę dowolnej długości, wektor i fragment &five[1..3]. Uwaga na nawyk przyniesiony z Pythona: tam xs[1:3] kopiuje, a w Ruscie &xs[1..3] to pożyczenie — żywy widok na cudze dane, więc dopóki wycinek istnieje, źródła nie wolno zmienić, i mówi o tym kompilator, a nie dziwne zachowanie w czasie działania. Zakresy są w obu językach półotwarte: five[1..3] to indeksy 1 i 2.
Resztę strony da się zapamiętać jako dwie pary przeciwieństw. Pierwsza: [i] to twierdzenie, .get(i) to pytanie. five[9] w ogóle się nie kompiluje — rustc wylicza stały indeks i odrzuca kod lintem unconditional_panic z komunikatem „this operation will panic at runtime” — przy indeksie znanym dopiero w czasie działania program kończy się paniką, a five.get(9) po prostu zwraca None. Druga: windows(2) kontra chunks(2) — okna zachodzą na siebie i nigdy nie są krótsze, kawałki nie zachodzą, ale ostatni bywa krótszy; pomylenie ich to klasyczny błąd o jeden (off-by-one) w tym zakątku std. I jedna rada, której polskie kursy zwykle nie dają: w sygnaturze funkcji pisz &[T], nigdy &Vec<T> — ta druga forma nie daje nic ponadto, a odrzuca tablice i wycinki; clippy zgłasza to jako ptr_arg.
Szukaj po polsku: tablice w Ruscie · wycinek w Ruscie · rust slice vs array · rust &[T] vs &Vec<T> ptr_arg · rust windows vs chunks