
Rust Slices: Borrowing Parts of Collections Without Copying
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&strfor 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
| Type | Ownership | Typical use |
|---|---|---|
String | Owns heap-allocated text | Building, modifying, storing text |
&str | Borrows text | Reading, 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.
| Method | Purpose | Notes |
|---|---|---|
len() | Returns the number of elements | Works on all slices |
is_empty() | Checks whether the slice has no elements | Often clearer than len() == 0 |
get(index) | Returns Option<&T> | Safe alternative to indexing |
split_at(index) | Splits into two slices | Panics if index is out of bounds |
first() / last() | Access boundary elements | Returns Option<&T> |
iter() | Iterates over elements | Common 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.
