Skip to content

Slices

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 type T
  • &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.

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:

SyntaxMeaning
[..]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
A slice is a pointer-plus-length view into someone else's buffer

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);
}

&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-panic
fn 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");
}
}

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);
}

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 &str over &String for string parameters

Both &Vec<T> and &String coerce automatically to their slice equivalents at call sites.

.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.

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: hello

This 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.

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]);
}

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.

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.

  • “&s[..n] truncates safely to n bytes, the way substring functions in other languages truncate to n characters.” It slices by byte, and if byte n is 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]/&str clone (technically a Copy, since slices are Copy) 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.

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.

What type represents a borrowed view into part of a String?
What does the range syntax [1..4] select from a slice?
Does a slice own the data it points to?
Why should a function parameter use &[T] instead of &Vec<T>?
Does this panic? What does it print if not?
fn main() {
    let s = "สวัสดี";
    let oops = &s[1..];
    println!("{oops}");
}
What does this program print?
fn main() {
    let numbers = [1, 2, 3, 4, 5];
    let (left, right) = numbers.split_at(2);
    println!("{:?} {:?}", left, right);
}