Skip to content

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 into v, so v cannot 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] is xs.last(), which returns Option because the list may be empty, and that Option is Python's IndexError moved 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 like TYPE c LENGTH 5, where the length is likewise part of the type. What transfers well is the reading habit: READ TABLE itab INDEX i sets sy-subrc and you check it, which is exactly .get(i) returning Option. Rust's arr[i] is the version that skips the check and dumps instead — TABLE_INVALID_INDEX is 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 takes FROM/TO indices 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 wrong n is 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 means Arrays.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] against Vec<Vec<T>>, and chunks turning one block back into rows
  • Tuples — the other built-in compound type, for fields of different types
  • String slices&str is &[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 () — what sort and reverse hand 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