Introduction
Slices let you reference a contiguous portion of a collection rather than the entire thing. They are one of Rust's most versatile tools for working with strings, arrays, and vectors without copying data. If you have worked with string views in C++ or array slices in Go, Rust slices fill the same role but with compile-time safety.
Key Concepts
- Slice: A reference to a contiguous sequence of elements within a collection. It stores a pointer to the start and a length.
- String slice (
&str): A reference to a portion of aString(or a string literal). This is the most common slice type in Rust. - Array slice (
&[T]): A reference to a portion of an array or vector. - Range syntax:
[start..end]creates a half-open range.[..end],[start..], and[..]are shorthands.
Real World Context
Slices are used everywhere in production Rust code. Parsers use string slices to refer to portions of input without allocating new strings. Network protocols use byte slices (&[u8]) to inspect packet headers without copying memory. By accepting &str instead of &String, your functions become more flexible and idiomatic.
Deep Dive
You create a string slice using range syntax on a String:
rustlet s = String::from("hello world"); let hello = &s[0..5]; // "hello" let world = &s[6..11]; // "world" let whole = &s[..]; // "hello world"
A function that returns a string slice ties the returned reference to the input, so the compiler ensures the source data lives long enough:
rustfn 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[..] }
This function works with both String references and string literals because it accepts &str.
Array slices work the same way:
rustlet numbers = [1, 2, 3, 4, 5]; let middle: &[i32] = &numbers[1..4]; // [2, 3, 4]
Slices also prevent mutation bugs. If you hold a slice to a String, the compiler will not let you mutate the underlying String:
rustlet mut s = String::from("hello world"); let word = first_word(&s); // s.clear(); // Error! Cannot mutate while slice exists println!("{}", word);
Common Pitfalls
- Slicing on non-character boundaries in UTF-8 strings — Rust strings are UTF-8, and slicing at a byte offset that falls in the middle of a multi-byte character will cause a panic at runtime. Always ensure your indices align with character boundaries or use
.char_indices(). - Confusing
&Stringand&str— Functions that accept&Stringcannot receive string literals directly. Prefer&stras the parameter type because it works with bothStringreferences and&strvalues.
Best Practices
- Use
&strin function signatures — Accept&strinstead of&Stringto make your functions compatible with literals, slices, and owned strings. - Prefer iterators over manual indexing — Instead of slicing strings by byte index, use
.chars(),.split_whitespace(), or.split()to avoid UTF-8 boundary issues.
Summary
- Slices are references to a contiguous portion of a collection, storing a pointer and a length.
- String slices (
&str) are the most common; they reference part (or all) of aStringor a string literal. - Array slices (
&[T]) let you work with subsections of arrays and vectors. - Holding a slice prevents mutation of the underlying data, enforced by the borrow checker.
- Always prefer
&strover&Stringin function parameters for maximum flexibility.
Code Examples
fn main() {
// Prefer &str over &String in function parameters
fn print_length(s: &str) {
println!("Length: {}", s.len());
}
let owned = String::from("hello");
let literal = "world";
print_length(&owned); // Works with String
print_length(literal); // Works with &str
print_length(&owned[1..4]); // Works with slices
}