Slices
Intermediate · Ownership
What & why
A slice is a borrowed view into part of a collection — a piece of a string or an array — without
copying it and without owning it. It’s how you say “just this middle chunk” and pass it around
cheaply. Slices are everywhere in Rust, and they’re the thing the mysterious &str type actually is.
The idea, slowly
A window onto a row of boxes
Picture a String as a row of labelled boxes, one per byte of text:
h e l l o
0 1 2 3 4
A slice is a window you slide over that row to see just some of the boxes — say boxes 0, 1, 2.
The window doesn’t copy the boxes and doesn’t own them; it just says “look here, from this box up to
that box.” You make one with &thing[start..end]:
fn main() {
let s = String::from("hello");
let hi = &s[0..2]; // a window over boxes 0 and 1: "he"
let lo = &s[2..5]; // boxes 2,3,4: "llo"
println!("{} + {}", hi, lo); // he + llo
}
The range 0..2 means start at 0, stop before 2 — so it includes 0 and 1, not 2. That
“stop before the end number” rule is the one to burn into memory; it’s the same everywhere ranges
appear in Rust.
The & matters: a slice is a borrow
Notice the & in &s[0..2]. A slice is a kind of reference — a borrow — so all the borrowing rules
from two lessons ago still apply. The slice points into s’s memory; it does not own it. That’s why
it’s cheap (nothing is copied) and why it can’t outlive s (more on that in the mistakes section).
Handy shortcuts for the ends
You can leave off a number to mean “the very start” or “the very end”:
fn main() {
let s = String::from("hello");
println!("{}", &s[..2]); // from the start up to 2 -> "he"
println!("{}", &s[2..]); // from 2 to the end -> "llo"
println!("{}", &s[..]); // the whole thing -> "hello"
}
&s[..] (the whole string as a slice) is common when a function wants a slice and you have the
whole string.
Meet &str: the string slice
Here’s the payoff. That &str type you keep seeing? A &str is a string slice — a borrowed
window into string text. When you slice a String, the result is a &str. And a plain text
literal like "hello" in your code is also a &str (it’s a window into text baked into your
program). So these are the same type:
fn main() {
let owned = String::from("hello world");
let piece: &str = &owned[0..5]; // "hello", a slice of the String
let literal: &str = "hello"; // also a &str, baked into the program
println!("{} == {} is {}", piece, literal, piece == literal); // true
}
This is why the last lesson said to write read-only text parameters as &str: it accepts both
literals and slices of Strings (and, thanks to deref coercion, whole &Strings too). One
parameter type, lots of callers.
A real reason to slice: return part of a string
Say you want the first word of a sentence. Instead of copying characters into a new String, you
return a slice — a window onto the original text. No allocation, no copy:
fn main() {
let sentence = String::from("learning rust today");
let word = first_word(&sentence);
println!("first word: {}", word); // learning
}
fn first_word(s: &str) -> &str {
for (i, ch) in s.char_indices() {
if ch == ' ' {
return &s[..i]; // window from start up to the first space
}
}
s // no space found: the whole thing is one word
}
first_word returns a &str that borrows from sentence — a view, not a copy. This is the classic
example of why slices exist.
Slices work on arrays too
Slices aren’t just for strings. Any array or Vec can be sliced the same way, giving a &[T]
(a borrowed window over a sequence of T):
fn main() {
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // a window over 20, 30, 40
println!("{:?}", middle); // [20, 30, 40]
println!("sum = {}", middle.iter().sum::<i32>()); // 90
}
Same idea, same start..end rule, same “it’s a borrow” behavior. Whenever a function only needs to
read a run of items, taking a slice (&[T] or &str) is the idiomatic choice — it works whether
the caller has an array, a Vec, or another slice.
Common mistakes
- Off-by-one from the exclusive end.
&s[0..2]gives you indices 0 and 1, not 2. Forgetting that the end is exclusive is the #1 slice bug.[..2]= “the first two.” - Slicing a string in the middle of a character. Rust strings are UTF-8 bytes, and some
characters (like
éor emoji) take more than one byte. Slicing between the bytes of one character panics at runtime withbyte index ... is not a char boundary. For plain ASCII (a-z, 0-9) every character is one byte, so you’re safe; just be careful with accented or non-Latin text. - Index out of range.
&s[0..99]on a 5-byte string panics withbyte index 99 is out of range. A slice can’t point past the end of what it borrows. - Letting the owner die first. A slice borrows from a value, so it can’t outlive that value.
If you drop or move the original
Stringwhile a slice of it is still in use, the compiler stops you — the window would be pointing at boxes that no longer exist.
More examples
Grabbing the middle innings of a game log
A sports app stores scores for every inning but only wants to show innings 3 through 5 without copying the whole game.
fn main() {
let innings = vec![1, 0, 2, 3, 0, 1, 4];
let middle = &innings[2..5];
println!("{:?}", middle);
}
A function that works with arrays and Vecs alike
A stats helper shouldn’t care whether the caller has a fixed-size array or a growable Vec — accepting a slice lets it work with both.
fn average(nums: &[f64]) -> f64 {
nums.iter().sum::<f64>() / nums.len() as f64
}
fn main() {
let fixed = [1.0, 2.0, 3.0];
let growable = vec![4.0, 5.0, 6.0, 7.0];
println!("{}", average(&fixed));
println!("{}", average(&growable));
}
Splitting a sentence into words
A search box wants each word a user typed on its own, so it can match them individually against an index.
fn main() {
let query = String::from("best rust books for beginners");
let words: Vec<&str> = query.split_whitespace().collect();
println!("{:?}", words);
}
Splitting a buffer into two mutable halves
A packet buffer needs its header and payload processed separately, in place, without copying — split_at_mut hands back two non-overlapping mutable slices.
fn main() {
let mut buffer = [1, 2, 3, 4, 5, 6];
let (header, payload) = buffer.split_at_mut(2);
header[0] = 99;
payload[0] = 42;
println!("header: {:?}, payload: {:?}", header, payload);
}
Dealing a deck into hands
A card game needs to split a shuffled deck into equal-sized hands without allocating a new Vec for each one.
fn main() {
let deck: Vec<i32> = (1..=10).collect();
let hands: Vec<&[i32]> = deck.chunks(5).collect();
for hand in &hands {
println!("{:?}", hand);
}
}
Your turn
This program wants to print the first three letters, rus. It doesn’t compile. Fix it.
fn main() {
let word = String::from("rust");
let start = word[0..3];
println!("{}", start);
}
Show solution
A slice is a borrow, so it needs the &. Without it, you’re asking to move a chunk of the string
out by value, which isn’t allowed. Add &:
fn main() {
let word = String::from("rust");
let start = &word[0..3]; // a borrowed window: "rus"
println!("{}", start);
}
&word[0..3] makes a &str viewing bytes 0, 1, 2 — the letters r, u, s — without copying or
taking ownership.
Quick check
Remember this
- A slice is a borrowed view into part of a collection — no copy, no ownership.
- Make one with
&thing[start..end]; the end index is exclusive (stops before it). &stris a string slice — that’s why text literals and slices of aStringshare the type.- Slices work on arrays and
Vecs too, giving&[T]; prefer slice parameters for read-only access. - A slice can’t outlive the value it borrows from.
Go deeper
- Rust Book - Slices — Borrowing part of a value.
Next: