9.6Slices

Last updated June 12, 2026

Lesson 5.4 made a promise it called "carving views by range": making a view of part of something, like the first word of a sentence, and it said the machinery lived in chapter 9. You now own every piece of that machinery. A partial view is just a reference with an opinion about where to start and stop, and Rust calls it a slice.

Strings are where slices shine brightest, and they get the next lesson. Today builds the concept on something simpler, and that something needs a one-minute introduction of its own.

A sequence to slice

Chapter 18 properly teaches Rust's collection types. We're borrowing the smallest one a chapter early, purely as slicing material: the array, a fixed-length run of values of one type, written in square brackets.

fn main() {
    let temperatures = [12, 14, 15, 13, 11];
    println!("{temperatures:?}");
    println!("first: {}", temperatures[0]);
}
[12, 14, 15, 13, 11]
first: 12

The type is written [i32; 5]: five i32s, length included in the type, fixed at compile time forever. Because the size is known up front, an array lives whole in the stack frame (lesson 8.2: no heap, no handle, just five boxes in a row). temperatures[0] reads one element, and counting starts at zero, which you'll be used to by chapter 18. Printing uses {:?} from lesson 5.5, the programmer's view. That's all the array we need today; growable sequences, the questions you're already forming about them, and arrays' own fine print all belong to chapter 18.

Carving a view

A slice expression is a borrow with a range attached, using lesson 7.5's half-open range syntax:

fn main() {
    let temperatures = [12, 14, 15, 13, 11];

    let middle = &temperatures[1..4];
    println!("{middle:?}");
    println!("{} readings", middle.len());
}
[14, 15, 13]
3 readings

Read &temperatures[1..4] aloud: borrow a view of temperatures, from element 1 up to but not including element 4. Same half-open convention as a for loop's range, and the same off-by-one protection: 1..4 has length 3, and the end index is the first element you don't get.

The ranges come with the shorthands you'd hope for. Omit the start to begin at the beginning, the end to run to the end, or both to view everything:

let first_two = &temperatures[..2];   // [12, 14]
let last_two = &temperatures[3..];    // [13, 11]
let everything = &temperatures[..];   // all five

The type of these views is written &[i32]: a slice of i32s. Notice what's missing compared to [i32; 5]: the length. A slice's length is whatever the range made it, known at runtime, carried inside the slice itself, which is why .len() works on one. Under the hood a slice is lesson 8.2's String handle stripped to two fields: the address where the view starts, plus a length. No capacity, because a view doesn't own storage; and no caring whether the viewed elements live on the stack (like this array) or the heap (like a String's text). A window is a window.

All of chapter 9 applies, because a slice is a reference. It borrows the array, so while a slice is alive the one-writer rule protects it, and rule 2 guarantees it can't outlive the array it watches. Nothing new to learn; the rules just got a third spelling: &T, &mut T, and now &[T].

Why functions take slices

Here's the payoff, and it's the 5.4 parameter argument generalized. Write a function against the view type and it accepts any contiguous run of i32s, whole or partial, any length:

fn sum(values: &[i32]) -> i32 {
    let mut total = 0;
    let mut i = 0;
    while i < values.len() {
        total += values[i];
        i += 1;
    }
    total
}

fn main() {
    let temperatures = [12, 14, 15, 13, 11];
    println!("week total: {}", sum(&temperatures));
    println!("weekend total: {}", sum(&temperatures[3..]));
}
week total: 65
weekend total: 24

One function, called once with the whole array and once with a two-element slice of it. Had the parameter been [i32; 5], the second call would be a type error (a [i32; 2] is a different type; the length is part of the type, remember), and so would every future call with six readings. &[i32] doesn't care: any window onto i32s, however wide, from whatever it was carved out of.

One small magic trick to flag before it slips past: sum(&temperatures) passes &[i32; 5] where &[i32] was requested, and the compiler quietly converts. You've seen this kindness before, lending a &String where &str was wanted, and next lesson finally names the mechanism. (The index-and-while body, meanwhile, is honest but clunky; chapter 19's iterators will retire it with prejudice.)

Best practice

Functions that read a sequence should take a slice (&[i32]), not an array type, for exactly the reason text parameters take &str: the function gets more useful, and callers lend instead of give.

When the range is wrong

A slice range is checked against the real length, and an out-of-range slice is a panic, the runtime refusal you know from lesson 5.6. To see one, we need the range to come from input (write [..9] on a five-element array with literals involved and you've learned by now, from lessons 3.1 and 4.4, that this compiler does arithmetic at compile time and may refuse outright):

use std::io;

fn main() {
    let temperatures = [12, 14, 15, 13, 11];

    let mut count = String::new();
    io::stdin().read_line(&mut count).expect("failed to read line");
    let count: usize = count.trim().parse().expect("not a number");

    let recent = &temperatures[..count];
    println!("first {count} readings: {recent:?}");
}

Type 3 and you get first 3 readings: [12, 14, 15]. Type 9:

thread 'main' (2304338) panicked at src/main.rs:10:31:
range end index 9 out of range for slice of length 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The message has everything a 3 a.m. debugger needs: what was asked (end index 9), what was available (length 5), and where (line 10). A view nine elements wide onto five elements would be a window into memory that isn't yours to watch; Rust's choice here is to stop the program rather than let it read mystery bytes. Chapter 12 teaches the graceful alternatives; until then, check lengths before you carve.

Quiz time

Question #1

For let scores = [10, 20, 30, 40];, give the contents and len() of each slice, no compiler allowed:

a) &scores[..2] b) &scores[2..] c) &scores[1..2] d) &scores[..]

Show solution

a) [10, 20], len 2. b) [30, 40], len 2. c) [20], len 1 (half-open: element 1, stop before 2; a one-element slice is still a slice). d) [10, 20, 30, 40], len 4.

Question #2

In the input-driven program above, describe what happens for each typed input: 5, 0, six.

Show solution

5: prints all five readings; an end index equal to the length is the largest legal one (half-open ranges again). 0: prints first 0 readings: []; an empty slice is legal and has len() 0. six: never reaches the slice. parse fails and the expect panics with your message not a number (lesson 5.6's machinery).

Question #3

Write fn last(values: &[i32]) -> i32 returning the final element. Then the follow-up: what does your function do if someone passes an empty slice, and how do you know?

Show solution
fn last(values: &[i32]) -> i32 {
    values[values.len() - 1]
}

For an empty slice, values.len() is 0, and 0 - 1 on a usize can't be -1: it's lesson 4.4's underflow, which panics in debug builds before the indexing even gets a chance to complain. Either way the program stops rather than reads out of range. (A last that gracefully handles emptiness wants to return "maybe an i32," and that type, Option, is chapter 11's headline.)

Slices of numbers are the rehearsal. The performance is the type you've been pronouncing since chapter 5 without seeing its full face: &str, the string slice. Next lesson, 5.4's IOU gets paid in full.