Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Slices

Intermediate · Ownership

What & why

A slice is a borrowed view into part of a collection — a piece of a string or an array — without copying it and without owning it. It’s how you say “just this middle chunk” and pass it around cheaply. Slices are everywhere in Rust, and they’re the thing the mysterious &str type actually is.

The idea, slowly

A window onto a row of boxes

Picture a String as a row of labelled boxes, one per byte of text:

 h   e   l   l   o
 0   1   2   3   4

A slice is a window you slide over that row to see just some of the boxes — say boxes 0, 1, 2. The window doesn’t copy the boxes and doesn’t own them; it just says “look here, from this box up to that box.” You make one with &thing[start..end]:

fn main() {
    let s = String::from("hello");
    let hi = &s[0..2];    // a window over boxes 0 and 1: "he"
    let lo = &s[2..5];    // boxes 2,3,4: "llo"
    println!("{} + {}", hi, lo);   // he + llo
}

The range 0..2 means start at 0, stop before 2 — so it includes 0 and 1, not 2. That “stop before the end number” rule is the one to burn into memory; it’s the same everywhere ranges appear in Rust.

The & matters: a slice is a borrow

Notice the & in &s[0..2]. A slice is a kind of reference — a borrow — so all the borrowing rules from two lessons ago still apply. The slice points into s’s memory; it does not own it. That’s why it’s cheap (nothing is copied) and why it can’t outlive s (more on that in the mistakes section).

Handy shortcuts for the ends

You can leave off a number to mean “the very start” or “the very end”:

fn main() {
    let s = String::from("hello");
    println!("{}", &s[..2]);    // from the start up to 2  -> "he"
    println!("{}", &s[2..]);    // from 2 to the end       -> "llo"
    println!("{}", &s[..]);     // the whole thing         -> "hello"
}

&s[..] (the whole string as a slice) is common when a function wants a slice and you have the whole string.

Meet &str: the string slice

Here’s the payoff. That &str type you keep seeing? A &str is a string slice — a borrowed window into string text. When you slice a String, the result is a &str. And a plain text literal like "hello" in your code is also a &str (it’s a window into text baked into your program). So these are the same type:

fn main() {
    let owned = String::from("hello world");
    let piece: &str = &owned[0..5];   // "hello", a slice of the String
    let literal: &str = "hello";      // also a &str, baked into the program

    println!("{} == {} is {}", piece, literal, piece == literal);  // true
}

This is why the last lesson said to write read-only text parameters as &str: it accepts both literals and slices of Strings (and, thanks to deref coercion, whole &Strings too). One parameter type, lots of callers.

A real reason to slice: return part of a string

Say you want the first word of a sentence. Instead of copying characters into a new String, you return a slice — a window onto the original text. No allocation, no copy:

fn main() {
    let sentence = String::from("learning rust today");
    let word = first_word(&sentence);
    println!("first word: {}", word);   // learning
}

fn first_word(s: &str) -> &str {
    for (i, ch) in s.char_indices() {
        if ch == ' ' {
            return &s[..i];    // window from start up to the first space
        }
    }
    s                          // no space found: the whole thing is one word
}

first_word returns a &str that borrows from sentence — a view, not a copy. This is the classic example of why slices exist.

Slices work on arrays too

Slices aren’t just for strings. Any array or Vec can be sliced the same way, giving a &[T] (a borrowed window over a sequence of T):

fn main() {
    let numbers = [10, 20, 30, 40, 50];
    let middle = &numbers[1..4];   // a window over 20, 30, 40
    println!("{:?}", middle);      // [20, 30, 40]
    println!("sum = {}", middle.iter().sum::<i32>());   // 90
}

Same idea, same start..end rule, same “it’s a borrow” behavior. Whenever a function only needs to read a run of items, taking a slice (&[T] or &str) is the idiomatic choice — it works whether the caller has an array, a Vec, or another slice.

Common mistakes

  • Off-by-one from the exclusive end. &s[0..2] gives you indices 0 and 1, not 2. Forgetting that the end is exclusive is the #1 slice bug. [..2] = “the first two.”
  • Slicing a string in the middle of a character. Rust strings are UTF-8 bytes, and some characters (like é or emoji) take more than one byte. Slicing between the bytes of one character panics at runtime with byte index ... is not a char boundary. For plain ASCII (a-z, 0-9) every character is one byte, so you’re safe; just be careful with accented or non-Latin text.
  • Index out of range. &s[0..99] on a 5-byte string panics with byte index 99 is out of range. A slice can’t point past the end of what it borrows.
  • Letting the owner die first. A slice borrows from a value, so it can’t outlive that value. If you drop or move the original String while a slice of it is still in use, the compiler stops you — the window would be pointing at boxes that no longer exist.

More examples

Grabbing the middle innings of a game log

A sports app stores scores for every inning but only wants to show innings 3 through 5 without copying the whole game.

fn main() {
    let innings = vec![1, 0, 2, 3, 0, 1, 4];
    let middle = &innings[2..5];
    println!("{:?}", middle);
}

A function that works with arrays and Vecs alike

A stats helper shouldn’t care whether the caller has a fixed-size array or a growable Vec — accepting a slice lets it work with both.

fn average(nums: &[f64]) -> f64 {
    nums.iter().sum::<f64>() / nums.len() as f64
}

fn main() {
    let fixed = [1.0, 2.0, 3.0];
    let growable = vec![4.0, 5.0, 6.0, 7.0];
    println!("{}", average(&fixed));
    println!("{}", average(&growable));
}

Splitting a sentence into words

A search box wants each word a user typed on its own, so it can match them individually against an index.

fn main() {
    let query = String::from("best rust books for beginners");
    let words: Vec<&str> = query.split_whitespace().collect();
    println!("{:?}", words);
}

Splitting a buffer into two mutable halves

A packet buffer needs its header and payload processed separately, in place, without copying — split_at_mut hands back two non-overlapping mutable slices.

fn main() {
    let mut buffer = [1, 2, 3, 4, 5, 6];
    let (header, payload) = buffer.split_at_mut(2);
    header[0] = 99;
    payload[0] = 42;
    println!("header: {:?}, payload: {:?}", header, payload);
}

Dealing a deck into hands

A card game needs to split a shuffled deck into equal-sized hands without allocating a new Vec for each one.

fn main() {
    let deck: Vec<i32> = (1..=10).collect();
    let hands: Vec<&[i32]> = deck.chunks(5).collect();
    for hand in &hands {
        println!("{:?}", hand);
    }
}

Your turn

This program wants to print the first three letters, rus. It doesn’t compile. Fix it.

fn main() {
    let word = String::from("rust");
    let start = word[0..3];
    println!("{}", start);
}
Show solution

A slice is a borrow, so it needs the &. Without it, you’re asking to move a chunk of the string out by value, which isn’t allowed. Add &:

fn main() {
    let word = String::from("rust");
    let start = &word[0..3];    // a borrowed window: "rus"
    println!("{}", start);
}

&word[0..3] makes a &str viewing bytes 0, 1, 2 — the letters r, u, s — without copying or taking ownership.

Quick check

Remember this

  • A slice is a borrowed view into part of a collection — no copy, no ownership.
  • Make one with &thing[start..end]; the end index is exclusive (stops before it).
  • &str is a string slice — that’s why text literals and slices of a String share the type.
  • Slices work on arrays and Vecs too, giving &[T]; prefer slice parameters for read-only access.
  • A slice can’t outlive the value it borrows from.

Go deeper

Next: