Lesson 8 of 22·14 min

Slices

The previous lesson showed how to borrow a whole value with &. Often you only want part of it: the first three scores, everything after a header row, the word in the middle of a sentence. Copying that part into a new array or String would work, but it costs an allocation and a copy every time.

A slice borrows a run of elements that sit next to each other in memory, without copying them. You already met two kinds: &[T] in the arrays lesson and &str in the ownership lesson. This lesson looks at both properly, and at their mutable counterpart, &mut [T].

What a slice is

A slice is a reference to a sequence of elements that some other value owns. It is made of two things: a pointer to the first element and a length. Because it borrows, all the borrowing rules from the previous lesson apply to it.

You make one by indexing with a range and borrowing the result:

fn main() {
    let temps = [18, 21, 25, 24, 19, 16, 15];
    let midweek = &temps[2..5];
    println!("{midweek:?}");
    println!("{} days", midweek.len());
}
[25, 24, 19]
3 days

2..5 includes the start and excludes the end, so midweek holds the elements at indexes 2, 3 and 4. The type of midweek is &[i32]: a slice of i32s, with the length stored in the slice rather than in the type. That is the difference from an array, whose type [i32; 7] fixes the length at compile time.

Range syntax

Either end of the range can be left out:

fn main() {
    let letters = ['a', 'b', 'c', 'd', 'e', 'f'];
    println!("{:?}", &letters[1..4]);
    println!("{:?}", &letters[1..=4]);
    println!("{:?}", &letters[..2]);
    println!("{:?}", &letters[4..]);
    println!("{:?}", &letters[..]);
    println!("{:?}", &letters[3..3]);
}
['b', 'c', 'd']
['b', 'c', 'd', 'e']
['a', 'b']
['e', 'f']
['a', 'b', 'c', 'd', 'e', 'f']
[]
  • a..b runs from a up to, but not including, b.
  • a..=b includes b.
  • ..b starts at the beginning, a.. runs to the end, and .. is the whole thing.
  • A range with the same start and end is an empty slice, which is allowed.

These are the same ranges for loops use. A range that reaches past the end, or whose start is after its end, panics at runtime, as the common mistakes below show.

Slices as parameters

A function that takes &[T] works with any number of elements from any source: a whole array, part of one, or a vector. A Vec<T> is a growable list that owns its elements, covered in the collections lesson; for now, vec![...] makes one with the elements you list:

fn average(values: &[f64]) -> f64 {
    let mut sum = 0.0;
    for v in values {
        sum += v;
    }
    sum / values.len() as f64
}
 
fn main() {
    let week = [7.5, 8.0, 6.5, 9.0, 7.0];
    let history = vec![6.0, 6.5, 8.5];
 
    println!("{:.2}", average(&week));
    println!("{:.2}", average(&week[3..]));
    println!("{:.2}", average(&history));
}
7.60
8.00
7.00

&week is a reference to an array and &history a reference to a vector, but both are converted to &[f64] automatically, the same way a &String becomes a &str. Prefer &[T] over &Vec<T> or &[T; N] for parameters, for the same reason you prefer &str over &String: the function accepts more kinds of input and gives up nothing.

(This average divides by zero on an empty slice, which gives NaN for floats. A real one would check values.is_empty() first.)

Looking inside a slice

Slices come with plenty of methods. Some of the most useful:

fn main() {
    let queue = ["ana", "ben", "chloe", "dev"];
    let people = &queue[..];
 
    println!("{}", people.len());
    println!("{}", people.is_empty());
    println!("{:?} {:?}", people.first(), people.last());
    println!("{:?} {:?}", people.get(2), people.get(10));
    println!("{}", people.contains(&"ben"));
    println!("{}", people.starts_with(&["ana"]));
 
    let (front, back) = people.split_at(1);
    println!("{front:?} {back:?}");
}
4
false
Some("ana") Some("dev")
Some("chloe") None
true
true
["ana"] ["ben", "chloe", "dev"]

first, last and get return Some(value) or None instead of panicking when the element doesn't exist, which makes them safe on an empty slice or with an index you haven't checked. Some and None belong to the Option type, which has its own lesson. split_at divides a slice into two slices that sit side by side.

To copy a slice's elements into something you own, use .to_vec(), which returns a new Vec.

Mutable slices

A mutable slice, &mut [T], lets you change the elements it borrows. Take it with &mut on a mut array or vector:

fn cap_at(values: &mut [u32], limit: u32) {
    for v in values {
        if *v > limit {
            *v = limit;
        }
    }
}
 
fn main() {
    let mut volumes = [40, 95, 120, 70, 101];
    cap_at(&mut volumes[1..], 100);
    println!("{volumes:?}");
}
[40, 95, 100, 70, 100]

Looping over a &mut [T] gives you a &mut T for each element, so you change it through *, just like the &mut u32 in the previous lesson. Only the part of volumes that was sliced could change; index 0 was outside the slice.

You can also assign by index, and slices have methods that rearrange elements in place:

fn main() {
    let mut cards = [7, 3, 9, 1, 5];
    let hand = &mut cards[..];
 
    hand[0] = 8;
    hand.swap(1, 2);
    println!("{hand:?}");
 
    hand.sort();
    println!("{hand:?}");
 
    hand.reverse();
    println!("{hand:?}");
 
    hand[3..].fill(0);
    println!("{cards:?}");
}
[8, 9, 3, 1, 5]
[1, 3, 5, 8, 9]
[9, 8, 5, 3, 1]
[9, 8, 5, 0, 0]

Every change lands in cards itself, because hand never had its own copy. Note the last println! reads cards, which is only allowed because hand is no longer used; the borrow rules from the previous lesson apply to slices as well.

What a slice can't do is change its length. There is no push or remove on &mut [T], since the memory belongs to someone else. To grow or shrink a list, you need the owner, like the Vec.

Two mutable parts at once

The borrow rules forbid two &mut borrows of the same array, even for parts that don't overlap, because the compiler doesn't check ranges. When you need both halves at once, split_at_mut splits one mutable slice into two that are guaranteed not to overlap:

fn main() {
    let mut scores = [10, 20, 30, 40];
    let (left, right) = scores.split_at_mut(2);
    left[0] += right[1];
    right[0] = 0;
    println!("{scores:?}");
}
[50, 20, 0, 40]

String slices

&str is a slice too: a view into UTF-8 text owned by a String or stored in the program. Slicing a string uses the same ranges, measured in bytes:

fn main() {
    let log_line = String::from("2026-09-26 ERROR disk full");
    let date = &log_line[..10];
    let level = &log_line[11..16];
    let message = &log_line[17..];
    println!("[{date}] [{level}] [{message}]");
}
[2026-09-26] [ERROR] [disk full]

date, level and message all point into log_line; no text was copied. While they are in use, log_line can't be changed, so a slice can never show text that has since been modified or freed.

Fixed byte positions are fine for fixed-format text like this. For everything else, methods on &str find the pieces for you and hand them back as slices:

fn main() {
    let sentence = "  slices borrow  without copying ";
    let trimmed = sentence.trim();
    println!("[{trimmed}]");
    for word in trimmed.split_whitespace() {
        println!("{word} ({} bytes)", word.len());
    }
    println!("{}", trimmed.starts_with("slices"));
}
[slices borrow  without copying]
slices (6 bytes)
borrow (6 bytes)
without (7 bytes)
copying (7 bytes)
true

trim returns a slice of the same text without the whitespace at either end, and split_whitespace yields one slice per word.

Because string ranges count bytes, a range must start and end on a character boundary. With plain ASCII text every byte is a character, but a character like é takes two bytes, and slicing through the middle of it panics.

Common mistakes

A range past the end

fn main() {
    let rolls = [4, 2, 6, 1, 3];
    let last_three = &rolls[3..6];
    println!("{last_three:?}");
}

Slicing checks its bounds at runtime, like indexing does, and panics instead of reading memory it doesn't own. Use &rolls[2..] to get the last three elements, or rolls.len() to compute the range.

Slicing through a character

fn main() {
    let city = String::from("Zürich");
    let first_two = &city[..2];
    println!("{first_two}");
}

ü takes bytes 1 and 2, so ..2 would cut it in half. The panic message says exactly which character was in the way. Only slice strings at positions you know are boundaries, like those found by searching the string or by split_whitespace, and work character by character with .chars() when you need to count characters.

Forgetting the &

fn main() {
    let numbers = [1, 2, 3, 4];
    let middle = numbers[1..3];
    println!("{middle:?}");
}

numbers[1..3] on its own names the elements themselves, whose count the compiler can't know, so it can't store them in a variable. A slice always lives behind a reference: write &numbers[1..3].

Passing & where &mut is needed

fn zero_out(values: &mut [i32]) {
    values.fill(0);
}
 
fn main() {
    let mut readings = [3, 1, 4];
    zero_out(&readings);
    println!("{readings:?}");
}

The function asks for a mutable slice, so the call has to lend one: zero_out(&mut readings).

Summary

  • A slice borrows a run of elements without copying them. It is a pointer and a length, and its type doesn't include the length: &[T], &mut [T], &str.
  • Make one with a range: &a[1..4], &a[1..=4], &a[..2], &a[2..], &a[..]. Ranges past the end panic at runtime.
  • Take &[T] in functions that read a list; arrays, parts of arrays and vectors all fit.
  • first, last and get return Some/None instead of panicking; len, contains, split_at and to_vec are other everyday methods.
  • &mut [T] changes elements in place (*v = ..., swap, sort, reverse, fill) but can't add or remove them.
  • &str is a string slice. String ranges count bytes and must land on character boundaries.

Practise what you read

Challenges that use the ideas from this lesson.