Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Borrowing Contiguous Portions with Slices

Hello! You have already used references to borrow whole values without taking ownership. A slice applies the same idea to a contiguous portion of an array or string. In this lesson, you’ll create slices, pass them to functions, and see how borrowing keeps a slice connected to its source data. That connection will matter when you build the text analyser at the end of this module.


A slice is more than a pair of indices

Suppose a function finds the first word in a string and returns the position of the first space. The position is useful only while the string still has the same contents. If the caller changes the string, the saved number remains unchanged—and may no longer describe a word at all.

A string slice instead borrows the text itself. Its type is &str; it identifies a starting position and a length, without owning or copying the characters. Because it is a borrow, Rust can prevent changes to the source that would invalidate it while the slice is still in use.

The Slice Type - The Rust Programming Language

Read the opening of “The Slice Type” in The Rust Programming Language for the problem that motivates slices, then its “String Slices” subsection for the range syntax.

Under “The Slice Type,” read from the motivating example. Follow why a returned index can become detached from the string it describes. Then, under “String Slices,” read from the range explanation, stopping after the note about UTF-8 boundaries. Focus on the exclusive end position, the slice’s starting position and length, and the abbreviated range forms.

The general pattern is &source[start..end]. The start is included; the end is excluded. Omitting start means zero, omitting end means the end of the source, and &source[..] borrows the whole source. Those conventions are the same for arrays and strings, but what the positions count differs.


Borrowing part of an array

An array has a fixed length that is part of its type: [i32; 5] is a five-element array of i32 values. A slice such as &[i32] borrows a contiguous run of those values; its length is available at runtime.

fn sum(values: &[i32]) -> i32 {
    let mut total = 0;
    for value in values {
        total += *value;
    }
    total
}

fn main() {
    let amounts: [i32; 5] = [125, -20, 45, -10, 70];
    let middle: &[i32] = &amounts[1..4];

    println!("{middle:?}");             // [-20, 45, -10]
    println!("{}", sum(middle));        // 15
    println!("{}", sum(&amounts));     // 210
}

Index 1 is included and index 4 is excluded, so middle contains three elements. sum needs neither ownership nor a particular array length: it can work with the middle slice or borrow the entire array. Inside the loop, value is a reference to an element, and *value reads that element.

Watch this short demonstration of the range notation before moving to strings. Where the video shows a slice, keep the leading & in mind: it is what makes the result a borrow.

Slice in Rust - Full Crash Rust Tutorial for Beginners

Watch “Slice in Rust - Full Crash Rust Tutorial for Beginners” by Francesco Ciulla for worked examples of array and string ranges.

Watch array ranges to see why the end index is excluded. Continue with string ranges for the ..end, start.., and .. shortcuts. We’ll add an important qualification to those string examples next: string positions count bytes, not characters.

A slice does not select arbitrary, separate elements: &amounts[1..4] describes one uninterrupted span. It also does not make a new array. Think of it as a bounded view of the existing elements, with a starting location and a length. The range must be valid—its start cannot exceed its end, and its end cannot exceed the source length.


String slices count bytes

A String owns its text; a &str borrows text. You can form a &str from a String with the same range notation:

fn main() {
    let message = String::from("ready now");

    let first: &str = &message[..5];
    let second: &str = &message[6..];
    let whole: &str = &message[..];

    println!("{first}, {second}"); // ready, now
    println!("{whole}");           // ready now
}

The indices work as expected here because the text is ASCII: each character occupies one byte. Rust strings, however, use UTF-8. Their range positions are byte offsets, and both ends of a string slice must fall on character boundaries.

For example, "éx" occupies three bytes: é takes two and x takes one. A slice ending at byte position 2 can contain "é", but one ending at position 1 would split its encoding. &text[..1] would therefore panic at runtime; Rust will not produce an invalid &str. Similarly, text.len() reports bytes, not the number of visible characters. This is why string indexing deserves more care than array indexing.

A string literal such as "ready now" already has type &str. It borrows text stored with the program; you do not need to create a String merely to pass that literal to a function expecting &str.


Returning a borrowed word

Here is a small function that returns the first word, using an ASCII space as its separator. It scans bytes to locate the space, then returns a slice rather than a detached index:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            return &s[..i];
        }
    }

    s
}

fn main() {
    let message = String::from("ready now");

    println!("{}", first_word(&message));  // ready
    println!("{}", first_word("later today")); // later
}

b' ' denotes the byte for an ASCII space. If the scan finds one, its position is a valid boundary at which to end the slice. If it finds none, the function returns the entire borrowed input. The function accepts both a borrow of a String and a string literal because its parameter is &str.

Returning a slice also preserves a crucial relationship: the returned word still borrows from the input. Consider what happens if you try to use it after clearing the source:

let mut message = String::from("ready now");
let word = first_word(&message);

message.clear();       // Does not compile: `word` is still in use.
println!("{word}");

clear needs to mutably borrow message, but word still immutably borrows its text for the later println!. Rust rejects the conflicting operation. If you finish using word before calling clear, that conflict no longer exists.

The Slice Type - The Rust Programming Language

Return to “The Slice Type” in The Rust Programming Language to connect the slice syntax to borrow checking and function design.

Under “String Slices,” begin at the borrowing example. Read through the compiler-error discussion and the “String Literals as Slices” subsection. Then, in “String Slices as Parameters,” read from the parameter discussion. Compare the functions accepting &String and &str: the latter accepts an existing slice as well as borrowed owned text.

For now, the useful interface rule is narrow: if a function only needs to read string content, &str lets it accept more kinds of input than &String. The next lesson will examine that choice alongside owning a String.


A brief extension: mutable array slices

Slices can also borrow array elements mutably. As with the mutable references you have seen already, changes through the borrow affect the original data, and you must respect the borrowing rules.

fn main() {
    let mut amounts = [125, -20, 45, -10];

    {
        let selected: &mut [i32] = &mut amounts[1..3];
        selected[0] = -25;
    }

    println!("{amounts:?}"); // [125, -25, 45, -10]
}

selected[0] refers to amounts[1]: slice indices start at zero within the slice. The inner scope makes it clear that the mutable borrow has ended before the program prints the whole array. Ordinary &str slices, in contrast, provide read-only access to their text.


Key takeaways

A slice borrows a contiguous portion of existing data without owning or copying it. Use &[T] for an immutable slice of array elements and &str for borrowed text. Ranges include their start and exclude their end; string ranges use byte offsets that must be UTF-8 boundaries. Because a slice remains a borrow of its source, Rust can catch attempts to change that source while the slice is still needed.

Next, you’ll compare String, &String, and &str more systematically when deciding what a function should own or borrow.

Can't find a good explanation? Sign up and we'll make it for you

Sign up