6.5 Slices: Borrowing Contiguous Data

Beyond references to entire values, Rust provides slices, which are references to a contiguous sequence of elements within a collection, rather than the whole collection. Slices provide a non-owning view (a borrow) into data owned by something else (like a String, Vec<T>, array, or even another slice). They are crucial for writing efficient code that accesses portions of data without needing to copy it or take ownership.

Internally, a slice is typically a fat pointer, storing two pieces of information:

  1. A pointer to the start of the sequence segment.
  2. The length of the sequence segment.

Because slices borrow data, they strictly adhere to Rust’s borrowing rules: you can have multiple immutable slices of the same data, or exactly one mutable slice, but not both at the same time if they could overlap.

6.5.1 Immutable and Mutable Slices

There are two primary kinds of slices, mirroring the two kinds of references:

  • Immutable Slice (&[T]): Provides read-only access to a sequence of elements of type T.
  • Mutable Slice (&mut [T]): Provides read-write access to a sequence of elements of type T.

The type T represents the element type (e.g., i32, u8).

6.5.2 Array Slices

Slices are commonly used with arrays (fixed-size lists on the stack) and vectors (growable lists on the heap).

fn main() {
    let numbers: [i32; 5] = [10, 20, 30, 40, 50]; // An array

    // Create immutable slices using range syntax
    let all: &[i32] = &numbers[..];        // Slice of the whole array
    let first_two: &[i32] = &numbers[0..2]; // Slice of elements 0 and 1 ([10, 20])
    let last_three: &[i32] = &numbers[2..];   // Slice of elements 2, 3, 4 ([30, 40, 50])

    println!("All: {:?}", all);
    println!("First two: {:?}", first_two);
    println!("Last three: {:?}", last_three);

    // Create a mutable slice (requires the owner to be mutable)
    let mut mutable_numbers = [1, 2, 3];
    let mutable_slice: &mut [i32] = &mut mutable_numbers[1..]; // Slice of elements 1 and 2
    // Index access refers to the slice itself: index 0 of the slice is index 1 of the array.
    mutable_slice[0] = 99;
    // mutable_numbers is now [1, 99, 3]
    println!("Modified numbers: {:?}", mutable_numbers);
}

Note: The .. range syntax creates slices: .. is the whole range, start..end includes start but excludes end, start.. goes from start to the end, and ..end goes from the beginning up to (excluding) end. This syntax works on arrays, vectors, and existing slices.

6.5.3 String Slices (&str)

A string slice, written &str, is a specific type of immutable slice that always refers to a sequence of valid UTF-8 encoded bytes. It’s the most primitive string type in Rust. You can create string slices by borrowing from Strings, other string slices, or string literals using range syntax with byte indices.

fn main() {
    let s_ascii: String = String::from("hello world"); // ASCII string

    // Slicing ASCII text is straightforward as byte indices match character boundaries
    let hello: &str = &s_ascii[0..5]; // Slice referencing "hello"
    let world: &str = &s_ascii[6..11]; // Slice referencing "world"
    println!("Slice 1: {}", hello);
    println!("Slice 2: {}", world);

    // With multi-byte UTF-8 characters, indices must respect character boundaries
    let s_utf8 = String::from("你好"); // "Nǐ hǎo" - 6 bytes total, each char is 3 bytes
    // let invalid_slice = &s_utf8[0..1]; // PANIC! 1 is not a character boundary.
    // let invalid_slice = &s_utf8[0..2]; // PANIC! 2 is not a character boundary.
    let first_char: &str = &s_utf8[0..3]; // OK: Slice referencing the first character "你"
    let second_char: &str = &s_utf8[3..6]; // OK: Slice referencing the second character "好"

    println!("First char: {}", first_char);
    println!("Second char: {}", second_char);
}

Because &str must always point to valid UTF-8 sequences, creating string slices using byte indices ([start..end]) has an important restriction: the start and end indices must fall on valid UTF-8 character boundaries. Attempting to create a slice where an index lies in the middle of a multi-byte character sequence is a runtime error and will cause your program to panic (a controlled crash indicating a program bug).

For the simpler examples in this chapter introducing slices, we often use ASCII text where each character is conveniently one byte long, making byte indices align with character boundaries. When working with text that may contain multi-byte characters, slicing using direct byte indices requires careful validation; often, iterating over characters or using methods designed for UTF-8 processing is a safer approach than direct byte-index slicing. Operations that could break the UTF-8 invariant (like arbitrary byte mutation within a &mut str) are also carefully controlled, as discussed later.

6.5.4 String Literals

Now we can understand string literals (e.g., "hello"). They are essentially string slices (&str) whose data is stored directly in the program’s compiled binary and is therefore valid for the entire program’s execution. Their type is &'static str, where 'static is a special lifetime indicating validity for the whole program runtime.

fn main() {
    let literal_slice: &'static str = "I am stored in the binary";
    println!("{}", literal_slice);
}

6.5.5 Slices in Functions

One of the most common uses for slices is in function arguments. Accepting a slice (&[T] or &str) instead of an owned type (like Vec<T> or String) makes a function more flexible and efficient, as it can operate on different kinds of data sources without taking ownership or requiring data copying.

// Function accepting an array/vector slice
fn sum_slice(slice: &[i32]) -> i32 {
    let mut total = 0;
    for &item in slice { // Iterate over elements in the slice
        total += item;
    }
    total
}

// Function accepting a string slice
fn first_word(text: &str) -> &str {
    // Iterate over bytes, find first space
    for (i, &byte) in text.as_bytes().iter().enumerate() {
        if byte == b' ' {
            return &text[0..i]; // Return slice up to space
        }
    }
    &text[..] // No space found, return whole slice
}

fn main() {
    // Array slice example
    let numbers = [1, 2, 3, 4, 5];
    // Can pass reference to array directly (coerces to slice)
    println!("Sum of numbers: {}", sum_slice(&numbers));
    // Or pass explicit slice
    println!("Sum of part: {}", sum_slice(&numbers[1..4]));

    // String slice example
    let sentence = String::from("hello wonderful world");
    println!("First word: {}", first_word(&sentence)); // Pass slice of String
    let literal = "goodbye";
    println!("First word: {}", first_word(literal)); // Pass a string literal directly
}

Note: Due to automatic deref coercions (discussed later), functions expecting &[T] can often directly accept references to arrays (&[T; N]) or Vec<T>s. Similarly, functions expecting &str can accept &String.

6.5.6 Mutable Slices (&mut [T] and &mut str)

Mutable slices (&mut [T]) allow modification of the elements within the borrowed sequence:

fn main() {
    let mut data = [10, 20, 30];
    let slice: &mut [i32] = &mut data[..];
    slice[0] = 15;
    slice[1] *= 2;
    println!("Modified data: {:?}", data); // Prints: [15, 40, 30]
}

Mutable string slices (&mut str) exist but are more restricted. Because a &str (and &mut str) must always contain valid UTF-8, arbitrary byte modifications are disallowed. Furthermore, the length of a string slice cannot be changed, as this would require modifying the owner (e.g., reallocating a String), which a borrow cannot do. This prevents simple appending operations directly on a &mut str.

Mutable string slices are primarily useful for in-place modifications that preserve UTF-8 validity and length, such as changing case via methods like make_ascii_uppercase(). For operations that need to change string length or might temporarily invalidate UTF-8, working directly with an owned String or a mutable byte slice (&mut [u8]) is necessary.

fn main() {
    let mut s = String::from("hello");
    { // Limit scope of mutable borrow
        let slice: &mut str = &mut s[..];
        slice.make_ascii_uppercase(); // In-place modification allowed
    } // Mutable borrow ends here
    println!("Uppercase: {}", s); // Prints: HELLO
}

Remember that all slice operations must respect the borrowing rules – particularly the exclusivity of mutable borrows for potentially overlapping data.