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:
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
}first word ends at 5It 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.
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("[{hello}] [{world}]");
}[hello] [world] 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 53. 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.
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
}/usr
local/bin
/usr/local/bin
usr4. 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.
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"));
}first word: borrow
of a literal: single5. 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.
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}");
}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 hereThe 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.
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
}STORED IN THE BINARY
STORED ON THE HEAP
STOREDThe 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.
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>());
}&String: 8 bytes
&str: 16 bytes
&[i32]: 16 bytes
String: 24 bytes8. 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.
fn main() {
let city = String::from("Zürich");
println!("{}", &city[0..2]); // "Zü" would need 3 bytes: Z (1) + ü (2)
}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].
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]);
}[20, 30, 40] [2.5, 3.5]
3 elements, first is 2010. &[T] parameters accept arrays, vectors, and slices
This fixes the problem from Tuples & Arrays, where [i32; 3] only accepted arrays of exactly three elements.
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..]));
}2
25
35Prefer &[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.
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());
}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: None12. split_at divides a slice in two
fn main() {
let steps = ["checkout", "build", "test", "deploy", "notify"];
let (ci, cd) = steps.split_at(3);
println!("CI: {:?}", ci);
println!("CD: {:?}", cd);
}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.
fn main() {
let user_ids: Vec<u32> = (1..=11).collect();
for (i, batch) in user_ids.chunks(4).enumerate() {
println!("batch {i}: {:?}", batch);
}
}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.
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);
}
}100 -> 102: +2
102 -> 101: -1
101 -> 105: +4
105 -> 110: +5
110 -> 108: -215. 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.
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);
}[99, 95, 12, 45, 70, 88]
[99, 95, 12, 45, 88, 0]16. Functions that change a slice in place
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);
}[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.
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);
}[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.
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));
}Some([2, 3])
None
Some(5)
None19. 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().
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);
}top three: [99, 95, 88]
original untouched: [72, 95, 88, 61, 99, 84]20. Comparing and joining slices
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());
}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.
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"]));
}no arguments
one argument: --help
3 arguments, from build to --verbose22. Byte slices: &[u8]
as_bytes() views a string as its raw bytes. Byte slices are what files, network sockets, and hash functions work with.
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"));
}first bytes: [116, 114, 97, 110, 115, 102, 101, 114]
checksum: 680114932
checksum after tampering: 3402974188b"..." 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.
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));
}
}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
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));
}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.
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));
}
}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.
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:?}");
}
} 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.
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));
}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[..]. &stris a string slice. String literals are&str.&[T]is a slice of any array orVec.- A slice reference is a fat pointer: a pointer plus a length.
- Take
&strand&[T]as parameters instead of&Stringand&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.
