Guide contents

Lesson 16 of 94

Slices: &str and &[T]

A slice is a reference to a contiguous part of a collection: a few elements of an array or Vec, or a range of characters of a String. It borrows the data, so it never copies anything, and it can't outlive the data it points to.

A slice is checked at compile time: the data it points to cannot change or disappear while the slice exists.

Task Rust
Take part of a string &s[0..5] (a view, no copy)
Take part of a list &v[1..3] (the list cannot change while the view exists)
Accept a sequence of numbers &[i32] parameter, which accepts arrays and Vecs
Accept text &str parameter, which accepts literals and Strings

1. The problem slices solve

Suppose a function returns the index where the first word ends. The index is a plain number, unconnected to the string, so it silently becomes wrong when the string changes:

Rust
fn first_word_end(s: &String) -> usize {
    let bytes = s.as_bytes();
    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            return i;
        }
    }
    s.len()
}

fn main() {
    let mut s = String::from("hello world");
    let end = first_word_end(&s);
    s.clear(); // the string is now empty...
    println!("first word ends at {end}"); // ...but `end` is still 5
}
Output
first word ends at 5

It compiles and runs, but end now describes a string that no longer exists. Returning a slice instead ties the result to the string, so the compiler can catch this.

2. String slices with ranges

&s[start..end] is a slice of the bytes from start up to, but not including, end.

Rust
fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..11];
    println!("[{hello}] [{world}]");
}
Output
[hello] [world]
Text
 s (String)                    heap
 ┌──────────┬────┐          ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
 │ ptr      │ ───┼────────► │ h │ e │ l │ l │ o │   │ w │ o │ r │ l │ d │
 │ len      │ 11 │          └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
 │ capacity │ 11 │            ▲                       ▲
 └──────────┴────┘            │                       │
 hello (&str): ptr ───────────┘  len 5                │
 world (&str): ptr ───────────────────────────────────┘  len 5

3. Range shortcuts

Leave out the start to begin at 0, leave out the end to go to the last element, and leave out both to take everything.

Rust
fn main() {
    let path = String::from("/usr/local/bin");
    println!("{}", &path[..4]);   // same as [0..4]
    println!("{}", &path[5..]);   // to the end
    println!("{}", &path[..]);    // the whole string
    println!("{}", &path[1..=3]); // inclusive end
}
Output
/usr
local/bin
/usr/local/bin
usr

4. Returning a slice from a function

Now first_word returns a &str that borrows from s. The result is always consistent with the string it came from.

Rust
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            return &s[..i];
        }
    }
    s
}

fn main() {
    let sentence = String::from("borrow checker rules");
    let word = first_word(&sentence);
    println!("first word: {word}");
    println!("of a literal: {}", first_word("single"));
}
Output
first word: borrow
of a literal: single

5. The compiler now catches the stale result

Because word borrows s, clearing s while word is in use needs a mutable borrow during a shared one, and that does not compile.

Rust✗ does not compile · E0502
fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    s.clear();
    println!("first word: {word}");
}
Output
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:11:5
   |
10 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
11 |     s.clear();
   |     ^^^^^^^^^ mutable borrow occurs here
12 |     println!("first word: {word}");
   |                            ---- immutable borrow later used here

The bug from example 1 is now a compile error. (match on find's result is explained in Option.)

6. String literals are slices

A literal like "hello" is a &str pointing to text stored inside the compiled program. That is why literals are &str, not String, and why &str parameters accept them directly.

Rust
fn shout(s: &str) -> String {
    s.to_uppercase()
}

fn main() {
    let literal: &str = "stored in the binary";
    let owned = String::from("stored on the heap");
    println!("{}", shout(literal));
    println!("{}", shout(&owned));      // &String converts to &str automatically
    println!("{}", shout(&owned[..6])); // a slice of a String
}
Output
STORED IN THE BINARY
STORED ON THE HEAP
STORED

The automatic conversion from &String to &str is called deref coercion. It is covered in Drop, Deref & Cow.

7. A slice is a "fat pointer"

A slice reference stores two numbers: a pointer to the first element and the length. It is 16 bytes on a 64-bit machine, twice the size of a normal reference.

Rust
fn main() {
    println!("&String: {} bytes", std::mem::size_of::<&String>());
    println!("&str:    {} bytes", std::mem::size_of::<&str>());
    println!("&[i32]:  {} bytes", std::mem::size_of::<&[i32]>());
    println!("String:  {} bytes", std::mem::size_of::<String>());
}
Output
&String: 8 bytes
&str:    16 bytes
&[i32]:  16 bytes
String:  24 bytes

8. Slicing in the middle of a character panics

String slice positions are byte positions. Text is stored as UTF-8, where characters outside English take 2 to 4 bytes. Slicing through the middle of one panics.

Rustpanics at runtime
fn main() {
    let city = String::from("Zürich");
    println!("{}", &city[0..2]); // "Zü" would need 3 bytes: Z (1) + ü (2)
}
Output
thread 'main' panicked at src/main.rs:3:25:
byte index 2 is not a char boundary; it is inside 'ü' (bytes 1..3) of `Zürich`

Work with characters instead of bytes when text may not be plain ASCII. Strings covers this properly.

9. Array and vector slices: &[T]

The same syntax works on arrays and vectors. The type of a slice of i32 values is &[i32].

Rust
fn main() {
    let array = [10, 20, 30, 40, 50];
    let vector = vec![1.5, 2.5, 3.5];

    let middle: &[i32] = &array[1..4];
    let tail: &[f64] = &vector[1..];
    println!("{:?} {:?}", middle, tail);
    println!("{} elements, first is {}", middle.len(), middle[0]);
}
Output
[20, 30, 40] [2.5, 3.5]
3 elements, first is 20

10. &[T] parameters accept arrays, vectors, and slices

This fixes the problem from Tuples & Arrays, where [i32; 3] only accepted arrays of exactly three elements.

Rust
fn average(values: &[f64]) -> f64 {
    let mut sum = 0.0;
    for v in values {
        sum += v;
    }
    sum / values.len() as f64
}

fn main() {
    let array = [1.0, 2.0, 3.0];
    let vector = vec![10.0, 20.0, 30.0, 40.0];
    println!("{}", average(&array));
    println!("{}", average(&vector));
    println!("{}", average(&vector[2..]));
}
Output
2
25
35

Prefer &[T] over &Vec<T> for parameters, just as you prefer &str over &String. It accepts more kinds of input and says exactly what the function needs: read access to a sequence.

11. Useful slice methods

Every method here works on arrays, vectors, and slices alike. first and last return Option, because the slice might be empty.

Rust
fn main() {
    let queue = vec!["ana", "ben", "cy", "dee"];
    let s = &queue[..];
    println!("len={} empty={}", s.len(), s.is_empty());
    println!("first={:?} last={:?}", s.first(), s.last());
    println!("contains ben: {}", s.contains(&"ben"));
    println!("starts with ana, ben: {}", s.starts_with(&["ana", "ben"]));
    println!("position of cy: {:?}", s.iter().position(|&name| name == "cy"));
    let empty: &[i32] = &[];
    println!("first of empty: {:?}", empty.first());
}
Output
len=4 empty=false
first=Some("ana") last=Some("dee")
contains ben: true
starts with ana, ben: true
position of cy: Some(2)
first of empty: None

12. split_at divides a slice in two

Rust
fn main() {
    let steps = ["checkout", "build", "test", "deploy", "notify"];
    let (ci, cd) = steps.split_at(3);
    println!("CI: {:?}", ci);
    println!("CD: {:?}", cd);
}
Output
CI: ["checkout", "build", "test"]
CD: ["deploy", "notify"]

13. Processing in batches with chunks

chunks(n) splits a slice into pieces of n elements (the last piece may be shorter). Each piece is itself a slice, so nothing is copied.

Rust
fn main() {
    let user_ids: Vec<u32> = (1..=11).collect();
    for (i, batch) in user_ids.chunks(4).enumerate() {
        println!("batch {i}: {:?}", batch);
    }
}
Output
batch 0: [1, 2, 3, 4]
batch 1: [5, 6, 7, 8]
batch 2: [9, 10, 11]

(1..=11).collect() builds a Vec from a range; see Iterators.

14. Sliding windows with windows

windows(n) yields every run of n consecutive elements. It is perfect for comparing neighbors or computing moving averages.

Rust
fn main() {
    let prices = [100, 102, 101, 105, 110, 108];
    for w in prices.windows(2) {
        let change = w[1] - w[0];
        println!("{} -> {}: {:+}", w[0], w[1], change);
    }
}
Output
100 -> 102: +2
102 -> 101: -1
101 -> 105: +4
105 -> 110: +5
110 -> 108: -2

15. Mutable slices: &mut [T]

A mutable slice lets you change part of a collection in place. Here only the tail of the vector is sorted.

Rust
fn main() {
    let mut leaderboard = vec![99, 95, 12, 70, 45, 88];
    // the first two places are fixed; sort the rest in place
    leaderboard[2..].sort();
    println!("{:?}", leaderboard);

    let tail: &mut [i32] = &mut leaderboard[4..];
    tail[0] = 0;
    tail.reverse();
    println!("{:?}", leaderboard);
}
Output
[99, 95, 12, 45, 70, 88]
[99, 95, 12, 45, 88, 0]

16. Functions that change a slice in place

Rust
fn clamp_all(values: &mut [i32], min: i32, max: i32) {
    for v in values {
        *v = (*v).clamp(min, max);
    }
}

fn main() {
    let mut volumes = vec![-5, 30, 120, 75, 200];
    clamp_all(&mut volumes, 0, 100);
    println!("{:?}", volumes);

    let mut pixels = [300, -20, 128];
    clamp_all(&mut pixels, 0, 255);
    println!("{:?}", pixels);
}
Output
[0, 30, 100, 75, 100]
[255, 0, 128]

17. Two mutable halves with split_at_mut

Two &mut borrows of one vector are normally rejected. split_at_mut returns two mutable slices that the standard library guarantees don't overlap, so both can be used at once.

Rust
fn main() {
    let mut balances = vec![500, 120];
    let (left, right) = balances.split_at_mut(1);
    let from = &mut left[0];
    let to = &mut right[0];
    *from -= 200;
    *to += 200;
    println!("{:?}", balances);
}
Output
[300, 320]

18. Out-of-range slices panic; get does not

&v[a..b] panics if the range is invalid. v.get(a..b) returns None instead, which is the safe choice for ranges that come from user input.

Rust
fn main() {
    let items = vec![1, 2, 3, 4, 5];
    println!("{:?}", items.get(1..3));
    println!("{:?}", items.get(3..10));
    println!("{:?}", items.get(4));
    println!("{:?}", items.get(9));
}
Output
Some([2, 3])
None
Some(5)
None

19. Converting a slice to an owned Vec

A slice only borrows. When you need an owned copy, for example to keep it after the original is dropped, use to_vec().

Rust
fn top_three(scores: &[u32]) -> Vec<u32> {
    let mut sorted = scores.to_vec(); // owned copy we can sort
    sorted.sort();
    sorted.reverse();
    sorted[..3].to_vec()
}

fn main() {
    let scores = [72, 95, 88, 61, 99, 84];
    let best = top_three(&scores);
    println!("top three: {:?}", best);
    println!("original untouched: {:?}", scores);
}
Output
top three: [99, 95, 88]
original untouched: [72, 95, 88, 61, 99, 84]

20. Comparing and joining slices

Rust
fn main() {
    let a = [1, 2, 3];
    let v = vec![1, 2, 3];
    println!("array == vec slice: {}", a[..] == v[..]);

    let parts = ["2026", "09", "27"];
    println!("{}", parts.join("-"));
    let lines = [vec![1, 2], vec![3], vec![4, 5]];
    println!("{:?}", lines.concat());
}
Output
array == vec slice: true
2026-09-27
[1, 2, 3, 4, 5]

21. Matching on the shape of a slice

match can check a slice's length and take it apart at the same time. .. stands for "any number of elements". Pattern Matching covers this in depth.

Rust
fn describe(args: &[&str]) -> String {
    match args {
        [] => String::from("no arguments"),
        [only] => format!("one argument: {only}"),
        [first, .., last] => format!("{} arguments, from {first} to {last}", args.len()),
    }
}

fn main() {
    println!("{}", describe(&[]));
    println!("{}", describe(&["--help"]));
    println!("{}", describe(&["build", "--release", "--verbose"]));
}
Output
no arguments
one argument: --help
3 arguments, from build to --verbose

22. Byte slices: &[u8]

as_bytes() views a string as its raw bytes. Byte slices are what files, network sockets, and hash functions work with.

Rust
fn checksum(data: &[u8]) -> u32 {
    let mut sum: u32 = 0;
    for &byte in data {
        sum = sum.wrapping_mul(31).wrapping_add(byte as u32);
    }
    sum
}

fn main() {
    let message = "transfer 100 to bob";
    let bytes: &[u8] = message.as_bytes();
    println!("first bytes: {:?}", &bytes[..8]);
    println!("checksum: {}", checksum(bytes));
    println!("checksum after tampering: {}", checksum(b"transfer 900 to bob"));
}
Output
first bytes: [116, 114, 97, 110, 115, 102, 101, 114]
checksum: 680114932
checksum after tampering: 3402974188

b"..." is a byte string literal, of type &[u8; N], which converts to &[u8].

23. Realistic example: pagination

A page of results is just a slice of the full list. No copying happens, however large the list is.

Rust
fn page<'a>(items: &'a [String], page_number: usize, page_size: usize) -> &'a [String] {
    let start = (page_number * page_size).min(items.len());
    let end = (start + page_size).min(items.len());
    &items[start..end]
}

fn main() {
    let mut articles = Vec::new();
    for i in 1..=7 {
        articles.push(format!("article-{i}"));
    }
    for p in 0..4 {
        println!("page {p}: {:?}", page(&articles, p, 3));
    }
}
Output
page 0: ["article-1", "article-2", "article-3"]
page 1: ["article-4", "article-5", "article-6"]
page 2: ["article-7"]
page 3: []

The <'a> and &'a in the signature are lifetime annotations. This function takes one reference with data (items), and the compiler would work that out on its own, but they are shown here to preview the Lifetimes section: they say "the returned slice borrows from items".

24. Realistic example: a moving average

Rust
fn moving_average(values: &[f64], window: usize) -> Vec<f64> {
    let mut result = Vec::new();
    for w in values.windows(window) {
        let mut sum = 0.0;
        for v in w {
            sum += v;
        }
        result.push((sum / window as f64 * 100.0).round() / 100.0);
    }
    result
}

fn main() {
    let daily_visitors = [120.0, 135.0, 128.0, 160.0, 172.0, 150.0, 190.0];
    println!("3-day average: {:?}", moving_average(&daily_visitors, 3));
}
Output
3-day average: [127.67, 141.0, 153.33, 160.67, 170.67]

25. Realistic example: batching database inserts

Sending rows one by one is slow, and sending all at once may exceed limits. chunks gives batches of a fixed size, each a slice borrowed from the original rows.

Rust
fn insert_batch(table: &str, rows: &[(u32, &str)]) -> String {
    let mut values = Vec::new();
    for (id, name) in rows {
        values.push(format!("({id}, '{name}')"));
    }
    format!("INSERT INTO {table} VALUES {};", values.join(", "))
}

fn main() {
    let rows = [(1, "ana"), (2, "ben"), (3, "cy"), (4, "dee"), (5, "eve")];
    for batch in rows.chunks(2) {
        println!("{}", insert_batch("users", batch));
    }
}
Output
INSERT INTO users VALUES (1, 'ana'), (2, 'ben');
INSERT INTO users VALUES (3, 'cy'), (4, 'dee');
INSERT INTO users VALUES (5, 'eve');

26. Realistic example: parsing key=value lines

Every part of the result is a slice of the input line: nothing is allocated. split_once returns Some((before, after)) when it finds the separator; if let runs its block only in that case. Both are covered in Option.

Rust
fn parse_line(line: &str) -> (&str, &str) {
    if let Some((key, value)) = line.split_once('=') {
        (key.trim(), value.trim())
    } else {
        (line.trim(), "")
    }
}

fn main() {
    let config = "host = db.internal\nport=5432\n debug \nname = orders";
    for line in config.lines() {
        let (key, value) = parse_line(line);
        println!("{key:>6} => {value:?}");
    }
}
Output
  host => "db.internal"
  port => "5432"
 debug => ""
  name => "orders"

27. Realistic example: finding the longest run of increasing prices

A single pass with windows(2) compares neighbors, and the answer is returned as a slice of the input.

Rust
fn longest_rising_run(prices: &[i32]) -> &[i32] {
    let mut best_start = 0;
    let mut best_len = 1;
    let mut start = 0;
    for (i, w) in prices.windows(2).enumerate() {
        if w[1] <= w[0] {
            start = i + 1;
        }
        let len = i + 2 - start;
        if len > best_len {
            best_len = len;
            best_start = start;
        }
    }
    &prices[best_start..best_start + best_len]
}

fn main() {
    let prices = [5, 3, 4, 6, 8, 7, 9, 10, 11, 12, 2];
    println!("longest rise: {:?}", longest_rising_run(&prices));
}
Output
longest rise: [7, 9, 10, 11, 12]

Summary

  • A slice borrows a contiguous part of a collection: &s[a..b], &v[..n], &v[k..], &v[..].
  • &str is a string slice. String literals are &str. &[T] is a slice of any array or Vec.
  • A slice reference is a fat pointer: a pointer plus a length.
  • Take &str and &[T] as parameters instead of &String and &Vec<T>.
  • String slice positions are byte positions: slicing inside a multi-byte character panics.
  • Slices come with chunks, windows, split_at, split_at_mut, first, last, get, contains, to_vec, and more.

Next: Strings in Depth.