RustSlices

Slices in Rust

Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection. A slice is a kind of reference, so it does not take ownership of the data it points to.

What Is a Slice?

A slice is a reference to a contiguous portion of a collection — like a window into an array, Vec, or String. Instead of copying data, a slice borrows a piece of it. The type of a slice is written as &[T] for sequences, or &str for string slices.

Note
Slices are fat pointers: they store both a pointer to the first element and a length. No heap allocation occurs when you create a slice.
String Slices (`&str`)

The most common slice in Rust is the string slice. A &str is a reference to a portion of a String (or a string literal stored in the binary).

RUST
fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];  // "hello"
    let world = &s[6..11]; // "world"

    println!("{} {}", hello, world);
}
Range Syntax

Rust provides several range syntaxes for slicing. The end index is exclusive by default; use ..= to make it inclusive.

Syntax

Meaning

&s[0..5]

indices 0, 1, 2, 3, 4 (end exclusive)

&s[0..=4]

indices 0, 1, 2, 3, 4 (end inclusive)

&s[..5]

from the start up to index 4

&s[3..]

from index 3 to the end

&s[..]

the entire string or collection

RUST
fn main() {
    let s = String::from("hello world");

    let from_start = &s[..5];   // "hello"
    let to_end     = &s[6..];   // "world"
    let everything = &s[..];    // "hello world"

    println!("{}", from_start);
    println!("{}", to_end);
    println!("{}", everything);
}
Warning
String slice indices must fall on valid UTF-8 character boundaries. Slicing in the middle of a multi-byte character will cause a panic at runtime.
Why `&str` Over `&String`?

Consider a function that finds the first word in a string. An early approach might return a usize index:

RUST
// Fragile: the index becomes meaningless if the String changes
fn first_word_index(s: &String) -> usize {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    s.len()
}

fn main() {
    let mut s = String::from("hello world");
    let word = first_word_index(&s); // word = 5
    s.clear(); // s is now ""
    // word is still 5, but s is empty — the index is now invalid!
    println!("{}", word);
}

The slice version is much safer. The borrow checker ensures the slice stays valid as long as it is used:

RUST
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

fn main() {
    let s = String::from("hello world");
    let word = first_word(&s);
    println!("First word: {}", word);
    // s.clear(); // compile error! cannot mutate 's' while 'word' holds a borrow
}
Success
Returning `&str` ties the lifetime of the result to the input. The compiler rejects any attempt to invalidate the slice while it is still in use.
Array and Vec Slices (`&[T]`)

The same slice concept applies to arrays and Vec<T>. The slice type is &[T].

RUST
fn main() {
    let a = [1, 2, 3, 4, 5];

    let slice = &a[1..3]; // [2, 3]
    println!("{:?}", slice);

    let v: Vec<i32> = vec![10, 20, 30, 40, 50];
    let v_slice = &v[2..4]; // [30, 40]
    println!("{:?}", v_slice);
}
The `[T]` vs `&[T]` Distinction

[T] by itself is an unsized (dynamically sized) type — you cannot store it directly in a variable because the compiler does not know its size at compile time. You always work with it behind a reference: &[T] (immutable slice) or &mut [T] (mutable slice). Both are fat pointers carrying a data address and a length.

Slice Borrowing Rules

Slices follow the same borrowing rules as all references in Rust:

  • You can have any number of immutable slices (&[T]) at the same time.

  • You can have exactly one mutable slice (&mut [T]) at a time — and no other references.

  • A mutable slice cannot coexist with any other borrow of the same data.

RUST
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    // OK: multiple immutable borrows at once
    let s1 = &v[0..2];
    let s2 = &v[3..5];
    println!("{:?} {:?}", s1, s2);

    // OK: one mutable borrow (previous immutable borrows are no longer used)
    let ms = &mut v[0..3];
    ms[0] = 99;
    println!("{:?}", ms);
}
Mutable Slices (`&mut [T]`)

A &mut [T] slice lets you modify the elements it refers to. This is useful when you want to sort or transform part of a collection in place.

RUST
fn double_elements(slice: &mut [i32]) {
    for x in slice.iter_mut() {
        *x *= 2;
    }
}

fn main() {
    let mut data = [1, 2, 3, 4, 5];
    double_elements(&mut data[1..4]); // doubles indices 1, 2, 3
    println!("{:?}", data);
}
Prefer `&[T]` Over `&Vec<T>` in Function Parameters

Function parameters should use slice types rather than references to owned types. This makes your functions more flexible: callers can pass arrays, slices, or Vecs without changes on their end.

RUST
// Less flexible — only accepts &Vec<i32>
fn sum_vec(v: &Vec<i32>) -> i32 {
    v.iter().sum()
}

// More flexible — accepts &Vec<i32>, &[i32], arrays, and sub-slices
fn sum_slice(v: &[i32]) -> i32 {
    v.iter().sum()
}

fn main() {
    let v = vec![1, 2, 3];
    let a = [4, 5, 6];

    println!("{}", sum_slice(&v));      // works with Vec
    println!("{}", sum_slice(&a));      // works with array
    println!("{}", sum_slice(&v[1..])); // works with a slice of a Vec
}
Tip
Clippy (the Rust linter) will warn you when a function parameter is `&Vec<T>` or `&String` and suggest changing it to `&[T]` or `&str`. Follow that advice — it is almost always correct.
Common Slice Methods

RUST
fn main() {
    let mut v = vec![5, 3, 8, 1, 9, 2];
    let s = v.as_mut_slice();

    // Length and emptiness
    println!("len: {}", s.len());
    println!("empty: {}", s.is_empty());

    // First and last element — returns Option<&T>
    println!("first: {:?}", s.first());
    println!("last: {:?}", s.last());

    // Membership
    println!("contains 8: {}", s.contains(&8));

    // Split at an index — returns two sub-slices
    let (left, right) = s.split_at(3);
    println!("left: {:?}, right: {:?}", left, right);
}
Sorting and Searching

RUST
fn main() {
    let mut nums = [4, 2, 7, 1, 9, 3];

    nums.sort();
    println!("sorted: {:?}", nums);

    // binary_search requires a sorted slice
    match nums.binary_search(&7) {
        Ok(index)  => println!("found 7 at index {}", index),
        Err(index) => println!("7 not found; would insert at {}", index),
    }
}
Windows and Chunks

Two iterator adapters that are unique to slices are .windows(n) and .chunks(n).

  • .windows(n) — produces overlapping sub-slices of length n, advancing by one each step

  • .chunks(n) — produces non-overlapping sub-slices of length n (the last chunk may be shorter)

RUST
fn main() {
    let data = [1, 2, 3, 4, 5];

    println!("Windows of 3:");
    for w in data.windows(3) {
        println!("  {:?}", w);
    }

    println!("Chunks of 2:");
    for c in data.chunks(2) {
        println!("  {:?}", c);
    }
}
Iterating Over Slices

RUST
fn main() {
    let scores = [88, 92, 75, 100, 63];

    // Immutable iteration — borrows each element
    for score in scores.iter() {
        print!("{} ", score);
    }
    println!();

    // Mutable iteration — can modify each element in place
    let mut grades = [70, 80, 90];
    for g in grades.iter_mut() {
        *g += 5; // curved by 5 points
    }
    println!("{:?}", grades);

    // enumerate — get index alongside value
    for (i, &val) in scores.iter().enumerate() {
        println!("scores[{}] = {}", i, val);
    }
}
Summary
  • &str is a string slice — a reference to part (or all) of a String or string literal.

  • &[T] is a sequence slice — a reference to a contiguous run of elements in an array or Vec.

  • Slices carry a pointer and a length; they do not own data and make no heap allocations.

  • Use &str over &String and &[T] over &Vec<T> in function parameters for maximum flexibility.

  • The same borrow rules apply: one mutable OR many immutable references at any given time.

  • Rich built-in methods: .len(), .sort(), .binary_search(), .windows(), .chunks(), .split_at(), and more.