What a slice is

A slice is a view into a sequence of elements. It does not own the data. Instead, it stores a pointer to the first element in the selected range and a length.

In Rust, slices come in two common forms:

  • &[T] for slices of array-like data
  • &str for string slices

Both are borrowed references, which means they let you inspect data without taking ownership.

Why slices matter

Slices are useful when you want to:

  • process only part of a collection
  • avoid copying large values
  • write functions that accept arrays and vectors interchangeably
  • work with string segments safely
  • express APIs that operate on borrowed data

A function that accepts a slice is often more flexible than one that accepts a Vec<T> or String.


Slice syntax for arrays and vectors

You can create a slice using range syntax inside brackets.

let numbers = [10, 20, 30, 40, 50];

let middle = &numbers[1..4];
assert_eq!(middle, &[20, 30, 40]);

The range 1..4 includes index 1 and excludes index 4. This is the standard Rust range behavior.

You can also slice from the beginning or to the end:

let first_three = &numbers[..3];
let last_two = &numbers[3..];
let all = &numbers[..];

These forms are common in real code because they make intent clear:

  • [..3] means “the first three elements”
  • [3..] means “from index 3 to the end”
  • [..] means “the whole collection as a slice”

Slices from vectors

Vectors work the same way because they store elements contiguously in memory.

let values = vec![1, 2, 3, 4, 5];
let part = &values[2..];
assert_eq!(part, &[3, 4, 5]);

A slice can borrow from a vector, array, or any contiguous sequence-like structure.


String slices: &str

String slices are one of the most important slice types in Rust. A String owns its contents, while &str borrows a portion of that text.

let text = String::from("hello world");
let word = &text[0..5];
assert_eq!(word, "hello");

Because Rust strings are UTF-8 encoded, slicing by byte index must respect character boundaries. That means you cannot safely slice in the middle of a multi-byte character.

let text = "Здравствуйте";
// This would panic if the index is not on a valid UTF-8 boundary.
// let bad = &text[0..2];

In practice, avoid manual string slicing unless you are certain the indices are valid byte boundaries. For user-facing text, prefer iterating over characters or using higher-level parsing logic.

String vs &str

TypeOwnershipTypical use
StringOwns heap-allocated textBuilding, modifying, storing text
&strBorrows textReading, parsing, passing text to functions

A good rule of thumb: accept &str in function parameters unless you specifically need ownership.


Passing slices to functions

One of the best uses of slices is designing flexible function signatures.

Instead of writing:

fn sum(values: Vec<i32>) -> i32 {
    values.iter().sum()
}

prefer:

fn sum(values: &[i32]) -> i32 {
    values.iter().sum()
}

This version accepts:

  • arrays
  • vectors
  • slices of larger collections

Example usage:

let a = [1, 2, 3];
let b = vec![4, 5, 6];

assert_eq!(sum(&a), 6);
assert_eq!(sum(&b), 15);
assert_eq!(sum(&b[1..]), 11);

This is more ergonomic because callers do not need to convert their data into a Vec<i32> just to call the function.

Slice parameters improve API design

A slice parameter communicates a clear contract:

  • the function reads data
  • the function does not own the data
  • the function works on a contiguous range

That makes your API easier to use and harder to misuse.


Mutable slices

Slices can also be mutable. A mutable slice lets you modify the borrowed elements in place.

fn double_all(values: &mut [i32]) {
    for value in values {
        *value *= 2;
    }
}

let mut data = [1, 2, 3, 4];
double_all(&mut data);
assert_eq!(data, [2, 4, 6, 8]);

Mutable slices are useful when you want to transform data without allocating a new collection.

When to use mutable slices

Use &mut [T] when:

  • you need to update elements in place
  • the caller should retain ownership
  • you want to avoid extra allocations
  • the operation is naturally local to a range of data

Avoid mutable slices when the function should produce a new collection or when mutation would make the API harder to understand.


Common slice operations

Slices support many of the same read-only operations you would expect from collections.

Iteration

let items = [10, 20, 30];
for item in &items[1..] {
    println!("{item}");
}

Length

let slice = &items[..2];
assert_eq!(slice.len(), 2);

Empty slices

An empty slice is valid and often useful.

let empty: &[i32] = &[];
assert!(empty.is_empty());

Indexing

You can index a slice like an array, but be careful: out-of-bounds indexing will panic.

let slice = &[100, 200, 300];
assert_eq!(slice[0], 100);

If the index may be invalid, use safe access methods such as get:

let slice = &[100, 200, 300];
assert_eq!(slice.get(5), None);

This is especially important in parsing code, where input may be incomplete or malformed.


Slices in real-world parsing code

Slices are a natural fit for parsers because parsing often means working with prefixes, suffixes, and subranges.

Imagine a simple protocol where the first byte indicates a message type and the rest is payload:

fn parse_message(input: &[u8]) -> Option<(u8, &[u8])> {
    if input.is_empty() {
        return None;
    }

    let kind = input[0];
    let payload = &input[1..];
    Some((kind, payload))
}

This function does not copy the payload. It returns a borrowed slice into the original buffer.

That pattern is common in:

  • network packet parsing
  • binary file formats
  • command-line token processing
  • log and text scanning

Why this pattern is efficient

By returning &[u8] or &str, you avoid allocating new buffers for every parsed segment. This can significantly reduce overhead in hot paths.


Safe slicing practices

Although slices are safe abstractions, slicing syntax can still panic if the range is invalid. Rust checks slice bounds at runtime.

Avoid unchecked assumptions

let data = [1, 2, 3];

// Panics if the range is invalid.
// let bad = &data[0..10];

Instead, validate before slicing:

fn head_two(values: &[i32]) -> Option<&[i32]> {
    if values.len() >= 2 {
        Some(&values[..2])
    } else {
        None
    }
}

This is a better choice for library code or input-driven logic.

Prefer split_at when dividing a slice

If you need to split a slice into two parts at a known index, split_at is often clearer:

let data = [1, 2, 3, 4, 5];
let (left, right) = data.split_at(2);

assert_eq!(left, &[1, 2]);
assert_eq!(right, &[3, 4, 5]);

This expresses intent directly and avoids manual range slicing.


Useful slice methods

Rust provides several methods that make slice handling easier and safer.

MethodPurposeNotes
len()Returns the number of elementsWorks on all slices
is_empty()Checks whether the slice has no elementsOften clearer than len() == 0
get(index)Returns Option<&T>Safe alternative to indexing
split_at(index)Splits into two slicesPanics if index is out of bounds
first() / last()Access boundary elementsReturns Option<&T>
iter()Iterates over elementsCommon in read-only processing

These methods help you write code that is both concise and robust.


Best practices for working with slices

Accept slices in function parameters

If your function only needs read access, prefer &[T] or &str over owned types.

fn contains_zero(values: &[i32]) -> bool {
    values.contains(&0)
}

This keeps your API flexible and avoids unnecessary ownership constraints.

Return slices when borrowing is enough

If the result is a part of the input, return a slice instead of allocating a new collection.

fn prefix(input: &str, n: usize) -> &str {
    &input[..n]
}

In real code, add validation to avoid invalid slicing.

Use &mut [T] for in-place changes

If a function transforms elements but does not need to resize the collection, a mutable slice is a strong choice.

Be careful with string boundaries

Never assume byte offsets are character offsets in UTF-8 text. If you need logical text units, use chars() or a text-processing library.

Prefer explicit range semantics

Use [..], [..n], and [n..] to communicate intent. These forms are easy to read and common in Rust codebases.


When slices are the wrong tool

Slices are excellent for borrowed contiguous data, but they are not always the right abstraction.

Avoid slices when:

  • you need ownership of the data
  • the data is not contiguous
  • you need to grow or shrink the collection
  • you are modeling a non-linear structure such as a tree or graph

In those cases, use owned collections, iterators, or domain-specific types instead.


Conclusion

Slices are a core Rust feature for writing efficient, reusable, and safe code. They let you work with parts of arrays, vectors, and strings without copying data or taking ownership. In everyday development, slices improve function design, reduce allocations, and make parsing and transformation code much cleaner.

If you remember one principle, make it this: accept slices when you only need borrowed access, and return slices when a borrowed view is sufficient. That habit leads to APIs that are both practical and idiomatic in Rust.

Learn more with useful resources