Slices
What Is a Slice?
Section titled “What Is a Slice?”A slice is a reference to a contiguous sequence of elements in a collection. It does not own the data — it borrows a view into existing memory.
There are two common slice types:
&[T]— a slice of any array or vector of typeT&str— a string slice; a view into UTF-8 string data
Because a slice is always a reference, its lifetime is tied to the collection it borrows from.
Array and Vector Slices
Section titled “Array and Vector Slices”Given an array or vector, you create a slice using range syntax inside square brackets:
fn main() { let numbers = [10, 20, 30, 40, 50];
let all: &[i32] = &numbers[..]; // entire array let first_three: &[i32] = &numbers[..3]; // indices 0, 1, 2 let middle: &[i32] = &numbers[1..4]; // indices 1, 2, 3 let last_two: &[i32] = &numbers[3..];// indices 3, 4
println!("all: {:?}", all); println!("first three: {:?}", first_three); println!("middle: {:?}", middle); println!("last two: {:?}", last_two);}Range syntax summary:
| Syntax | Meaning |
|---|---|
[..] | entire collection |
[..n] | first n elements (indices 0 to n-1) |
[m..n] | indices m to n-1 |
[m..] | from index m to the end |
flowchart LR
subgraph Owner
ARR["numbers: 10 20 30 40 50 - owns this buffer"]
end
subgraph Slice
FP["middle: pointer plus length 3 - a fat pointer, owns nothing"]
end
FP -->|points into index 1| ARR String Slices (&str)
Section titled “String Slices (&str)”A string slice (&str) is a reference to a portion of a String or a string literal. String literals themselves are &str — they are slices of read-only data baked into the binary.
fn main() { let s = String::from("hello world");
let hello: &str = &s[..5]; // "hello" let world: &str = &s[6..]; // "world"
println!("{} {}", hello, world);
// A string literal is already &str let literal: &str = "Rust is fast"; println!("{}", literal);}Byte Indices, Not Character Indices
Section titled “Byte Indices, Not Character Indices”&s[..n] slices by byte offset, not by character. That is invisible with ASCII, where every character is one byte — but Rust strings are UTF-8, and a non-ASCII character like a Thai vowel or an emoji can take two, three, or four bytes. Slicing at a byte offset that lands in the middle of one of those multi-byte characters does not truncate or approximate anything — it panics.
// @expect-panicfn main() { let s = "สวัสดี"; let oops = &s[1..]; // byte 1 is inside the first character, not a boundary println!("{oops}");}thread 'main' (<id>) panicked at src/main.rs:4:18:start byte index 1 is not a char boundary; it is inside 'ส' (bytes 0..3 of string)note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceส (the first character of “สวัสดี”) is 3 bytes wide, so byte index 1 falls inside it — there is no valid char boundary there. The fix is to stop thinking in byte offsets and use one of the character-aware APIs instead:
fn main() { let s = "สวัสดี";
// chars() iterates by Unicode scalar value, never by byte let first_char: Option<char> = s.chars().next(); println!("first char: {:?}", first_char);
// char_indices() pairs each char with its byte offset — always a valid boundary if let Some((idx, ch)) = s.char_indices().nth(1) { println!("second char {:?} starts at byte {}", ch, idx); }
// is_char_boundary() lets you check before slicing by byte offset let idx = 1; if s.is_char_boundary(idx) { println!("{}", &s[idx..]); } else { println!("byte {idx} is not a char boundary — cannot slice there"); }}Slices Prevent Dangling Indices
Section titled “Slices Prevent Dangling Indices”Without slices, you might store an index into a string and then modify the string — leaving the index pointing to meaningless data. With a string slice, the borrow checker ensures the slice cannot outlive the data it references.
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &byte) in bytes.iter().enumerate() { if byte == b' ' { return &s[..i]; // slice up to the space } } s // whole string is one word}
fn main() { let sentence = String::from("hello world"); let word = first_word(&sentence); // sentence cannot be mutated while word (a slice of it) is alive println!("first word: {}", word);}Slices as Function Parameters
Section titled “Slices as Function Parameters”Functions that operate on sequences should accept slices rather than owned types. This lets them work with arrays, vectors, and any portion thereof without requiring ownership.
- Prefer
&[T]over&Vec<T>for sequence parameters - Prefer
&strover&Stringfor string parameters
Both &Vec<T> and &String coerce automatically to their slice equivalents at call sites.
Splitting a Slice in Two: split_at
Section titled “Splitting a Slice in Two: split_at”.split_at(mid) divides a slice into two at a byte/index boundary, returning both halves as a tuple — a common building block whenever an algorithm needs “everything before” and “everything from” a point without writing two separate range expressions:
fn main() { let numbers = [1, 2, 3, 4, 5]; let (left, right) = numbers.split_at(2); println!("left={:?} right={:?}", left, right);}left=[1, 2] right=[3, 4, 5]split_at works on both &[T] and &str (where it splits at a byte offset and, like indexing, panics if that offset is not a char boundary). It is exactly equivalent to writing (&numbers[..2], &numbers[2..]) yourself, just as one call that cannot accidentally disagree about where the split happens.
Getting a Safe Byte Index from find
Section titled “Getting a Safe Byte Index from find”The byte-boundary panic earlier comes from guessing an offset. Methods that search a string, like .find(), hand you back a byte offset that is guaranteed to land on a character boundary — because it found an actual character (or the space character, below) at that position:
fn main() { let s = "hello world"; if let Some(space_at) = s.find(' ') { let word = &s[..space_at]; // find() returns a byte index, always a valid boundary here println!("first word: {}", word); }}first word: helloThis is the general pattern for slicing text safely: never write a byte offset as a literal number unless you already know the data is pure ASCII; get the offset from find, char_indices, or is_char_boundary instead.
A Peek Ahead: Box<T>
Section titled “A Peek Ahead: Box<T>”Everything in this module so far — moves, references, slices — deals with values that are either on the stack or borrowed. Box<T> is Rust’s simplest way to put a value on the heap while still having exactly one, statically-checked owner: Box::new(42) allocates an i32 on the heap and gives you back an 8-byte pointer to it, regardless of how large the boxed type is. It comes up properly once you need recursive types or trait objects — mentioned here only so the name is not a surprise later in the course.
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &byte) in bytes.iter().enumerate() { if byte == b' ' { return &s[..i]; } } s}
fn sum_slice(nums: &[i32]) -> i32 { let mut total = 0; for &n in nums { total += n; } total}
fn main() { // String slice let sentence = String::from("hello world"); let word = first_word(&sentence); println!("first word: {}", word);
// Integer slice let numbers = [10, 20, 30, 40, 50]; let middle: &[i32] = &numbers[1..4]; println!("middle slice: {:?}", middle); println!("sum of middle: {}", sum_slice(middle));
// String slice literal let greeting: &str = "Rust is safe"; println!("slice: {}", &greeting[..4]);}Compiling…
Mental Model
Section titled “Mental Model”A slice is a fat pointer: a pointer to the first element plus a length, nothing more. It never owns what it points to and never allocates — &s[..5] costs exactly as much as copying two usizes. The byte-boundary panic exists because that pointer-plus-length pair has no idea where char boundaries are; it only knows byte offsets, so the moment you hand it one that lands mid-character, there is no valid &str to produce.
Trade-offs
Section titled “Trade-offs”Slices are as cheap as references get — no allocation, no copying of the underlying data — but that cheapness is exactly why they are byte-indexed rather than character-indexed: computing “the Nth character’s byte offset” for a String is an O(n) scan (there is no fixed-width encoding to jump through), so &str intentionally does not offer O(1) character indexing at all. If you need to index by character position often, the honest trade-off is to pay for a Vec<char> once, up front, rather than re-scanning a &str on every access.
Common Misconceptions
Section titled “Common Misconceptions”- “
&s[..n]truncates safely to n bytes, the way substring functions in other languages truncate to n characters.” It slices by byte, and if bytenis not on a character boundary, it panics rather than silently producing a slightly-wrong result — the earlier"สวัสดี"example is exactly this. - “Since slices are so cheap, cloning one whenever the borrow checker complains is basically free too.” A
&[T]/&strclone (technically aCopy, since slices areCopy) only duplicates the pointer and length — but the far more common mistake is reaching for.to_vec()/.to_string()on the slice’s target, which does allocate and copy the real data. Reach for that only when you actually need an owned, independent copy. - “
&Vec<T>and&[T]are basically interchangeable, so it doesn’t matter which one a function signature uses.”&[T]accepts everything&Vec<T>does (via coercion) plus arrays and any sub-slice — there is no situation where&Vec<T>accepts something&[T]rejects, so&[T]is a strict improvement as a parameter type.
Reading AI-generated code
Section titled “Reading AI-generated code”Slicing user-supplied text at a hardcoded byte count is one of the most common non-obvious bugs in AI-generated Rust — it works perfectly in every test the assistant tried (English text), and panics the first time a real user pastes in non-ASCII input.
🐛 Spot the bug
fn preview(input: &str) -> &str { &input[..10] // assumes at least 10 ASCII bytes}
fn main() { let greeting = "Hello, world!"; println!("{}", preview(greeting));}Show the answer
This runs fine for "Hello, world!" — 10 ASCII bytes is exactly 10 characters, and the string is long enough. It panics the instant a caller passes text shorter than 10 bytes ("Hi"), or text where byte 10 falls inside a multi-byte character (any non-ASCII input near that length). The fix is to compute a safe boundary instead of assuming one: input.char_indices().nth(10).map(|(i, _)| &input[..i]).unwrap_or(input) takes the first 10 characters (or the whole string if it is shorter), and never panics on any valid &str.