Code of the Day
IntermediateOwnership in depth

Slices

Borrow a contiguous portion of a string or array without copying — understanding &str, &[T], and slice ranges.

RustIntermediate10 min read
Recommended first
By the end of this lesson you will be able to:
  • 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 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:

  • String is an owned, heap-allocated, growable string. You own it; you can mutate it; when it goes out of scope it's dropped.
  • &str is a — a reference to some UTF-8 text stored somewhere (on the heap inside a String, 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 buffer

String 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 string

The 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 slice

String 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. 1.
    Why is &str preferred over &String as a function parameter for reading strings?
  2. 2.
    Creating a slice copies the underlying data into a new allocation.
  3. 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.

Finished reading? Mark it complete to track your progress.

On this page