Slices
Borrow a contiguous portion of a string or array without copying — understanding &str, &[T], and slice ranges.
- Explain the difference between &str and String and when to use each
- Create array slices with &[T] and specify ranges
- Write functions that accept slices instead of owned collections
- Understand why string literals are &str
A slice is a reference to a contiguous sequence of elements — a borrowed window into a string or array. Slices let you pass part of a collection without copying it, and they're the idiomatic way to write functions that read (but don't need to own) sequences of data.
String vs &str
You've seen two string types in Rust. Understanding the difference is important:
Stringis an owned, heap-allocated, growable string. You own it; you can mutate it; when it goes out of scope it's dropped.&stris a string slice — a reference to some UTF-8 text stored somewhere (on the heap inside aString, or in the program's read-only data segment).
let owned: String = String::from("hello"); // heap-allocated, owned
let borrowed: &str = "hello"; // string literal — &str pointing into read-only memory
let slice: &str = &owned[1..3]; // &str pointing into the String's heap bufferString literals like "hello" are &str, not String. They live in the binary itself and are valid for the lifetime of the program.
Prefer &str over &String in function parameters. A &str can accept both string literals and references to owned Strings — it's strictly more flexible. &String only accepts &String.
Slice ranges
You create a slice by indexing with a range:
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
let all = &s[..]; // the whole stringThe range start..end is half-open: it includes start and excludes end. You can omit either end:
&s[..5] // equivalent to &s[0..5]
&s[6..] // from index 6 to the end
&s[..] // the whole sliceString slices must fall on valid UTF-8 character boundaries. If you index in the middle of a multi-byte character, Rust panics at runtime. For byte-level work, use .as_bytes() and &[u8] slices. For character-level work, use .chars().
Array slices: &[T]
The same idea applies to arrays and Vec<T>. An array slice &[T] is a borrowed view into a contiguous sequence:
fn sum(nums: &[i32]) -> i32 {
nums.iter().sum()
}
fn main() {
let arr = [1, 2, 3, 4, 5];
let v = vec![10, 20, 30];
println!("{}", sum(&arr)); // works
println!("{}", sum(&arr[1..3])); // pass just elements 1 and 2
println!("{}", sum(&v)); // Vec<i32> coerces to &[i32]
}A function that takes &[T] is more general than one that takes &Vec<T> — it works with arrays, vecs, and sub-slices. Prefer it.
Returning slices from functions
A function can return a slice, but the slice must refer to data that lives long enough. A classic mistake:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
&s[..]
}This works because the returned &str points into s, which the caller owns. The borrow checker ensures s is not modified (or dropped) while the returned slice is in use:
let mut sentence = String::from("hello world");
let word = first_word(&sentence);
// sentence.clear(); // ERROR — sentence is borrowed, can't mutate
println!("{}", word);This is the borrow checker at its most useful: catching a bug (using a slice after the string was invalidated) that would be a silent corruption in C.
Check your understanding
Knowledge check
- 1.Why is &str preferred over &String as a function parameter for reading strings?
- 2.Creating a slice copies the underlying data into a new allocation.
- 3.Given
let v = vec![10, 20, 30, 40];, what does&v[1..3]contain?
Do it yourself
Write a function longest_word(text: &str) -> &str that returns a slice of the first longest word in a string. Observe that returning &str forces you to think about what the slice points into — it must be a sub-slice of text.
fn longest_word(text: &str) -> &str {
text.split_whitespace()
.max_by_key(|w| w.len())
.unwrap_or("")
}Experiment: try storing the result, then modifying the source string — the compiler will tell you exactly why that's not allowed.
Where to go next
Slices introduce an implicit question: how long is this reference valid? The next lesson on lifetimes makes that question explicit, giving you the tools to write functions that return references with confidence.