Why this matters
Functions often need to work on part of some data: the first half of a list, the bytes after a header, one word of a sentence. A slice is a reference to a contiguous run of elements inside an array, a vector or a string. It lets a function accept "some elements, from wherever" without caring who owns them or how they are stored, and with bounds checking intact. &str, which you have used since module 1, is the slice type for text. After this lesson you will know what it actually is.
Making slices
fn main() {
let a = [10, 20, 30, 40, 50];
let middle: &[i32] = &a[1..4];
let start = &a[..2];
let end = &a[3..];
let all = &a[..];
println!("{:?} {:?} {:?} {:?}", middle, start, end, all);
println!("middle has {} elements, first is {}", middle.len(), middle[0]);
}
[20, 30, 40] [10, 20] [40, 50] [10, 20, 30, 40, 50]
middle has 3 elements, first is 20
&a[1..4]borrows elements 1, 2 and 3: the range syntax fromforloops, half-open as always...2starts at 0;3..runs to the end;..is everything.- The type is
&[i32]: "a slice of i32", with no length in the type. Compare[i32; 5], an array whose length is part of its type. - A slice is indexed from 0 relative to its own start:
middle[0]is 20.
What a slice is in memory
A slice reference is a fat pointer: two words, a pointer to the first element and a length.
a (array on the stack): [ 10 | 20 | 30 | 40 | 50 ]
^
middle: &[i32] |
+--------+-----+ |
| ptr ----------------------- +
| len 3 |
+--------+
fn main() {
use std::mem::size_of;
println!("&i32: {}", size_of::<&i32>());
println!("&[i32]: {}", size_of::<&[i32]>());
println!("&str: {}", size_of::<&str>());
println!("String: {}", size_of::<String>());
}
&i32: 8
&[i32]: 16
&str: 16
String: 24
The length is needed because it is not part of the type; it is what allows middle[5] to be caught at run time. A slice has no capacity: it cannot grow, because it only views memory that someone else owns. Slices obey all the borrowing rules: while middle is in use, a cannot be modified.
Slices as parameters
Because a slice can point into arrays and vectors alike, &[T] is the most flexible parameter type for "a sequence of T I want to read":
fn largest(values: &[i32]) -> i32 {
let mut best = values[0];
for &v in values {
if v > best {
best = v;
}
}
best
}
fn main() {
let arr = [3, 9, 2];
let v = vec![7, 1, 8, 4];
println!("{}", largest(&arr));
println!("{}", largest(&v));
println!("{}", largest(&v[..2]));
}
9
8
7
&arr (a &[i32; 3]) and &v (a &Vec<i32>) both convert automatically to &[i32]. A function taking &Vec<i32> would reject the array and the sub-slice. Rule of thumb: take &[T], not &Vec<T>; take &str, not &String. (largest panics on an empty slice because of values[0]; module 8 shows how to return "no answer" properly.)
Mutable slices
&mut a[1..4] is a mutable slice, &mut [T], through which elements can be changed (but not added or removed: the length is fixed):
fn zero_out(s: &mut [i32]) {
for x in s.iter_mut() {
*x = 0;
}
}
fn main() {
let mut a = [1, 2, 3, 4, 5];
zero_out(&mut a[1..3]);
println!("{:?}", a);
a[3..].sort_by(|x, y| y.cmp(x));
println!("{:?}", a);
}
[1, 0, 0, 4, 5]
[1, 0, 0, 5, 4]
Slices come with many methods: len, is_empty, first, last, contains, iter, sort, reverse, swap, binary_search, windows, chunks, split_at and more. They work on arrays and vectors too, because those convert to slices. (sort_by takes a small function written inline, a closure; module 11.)
Splitting a slice
split_at(mid) divides a slice into two non-overlapping slices. split_at_mut does the same for mutable slices, and it is the answer to the "two mutable references into one array" problem from lesson 3:
fn main() {
let mut arr = [1, 2, 3, 4, 5, 6];
let (left, right) = arr.split_at_mut(3);
left[0] += 100;
right[0] += 100;
println!("{:?} {:?}", left, right);
println!("{:?}", arr);
}
[101, 2, 3] [104, 5, 6]
[101, 2, 3, 104, 5, 6]
The borrow checker cannot prove that &mut arr[0] and &mut arr[3] do not overlap, but split_at_mut guarantees it, so it hands out two independent &mut slices. Inside, it uses a little unsafe code; module 15 shows how to write exactly this function.
Windows and chunks
Two slice iterators are handy for sequence problems:
fn main() {
let temps = [12, 15, 9, 20, 17, 14];
for w in temps.windows(3) {
print!("{:?} ", w);
}
println!();
for c in temps.chunks(4) {
print!("{:?} ", c);
}
println!();
}
[12, 15, 9] [15, 9, 20] [9, 20, 17] [20, 17, 14]
[12, 15, 9, 20] [17, 14]
windows(n) gives every overlapping run of n elements; chunks(n) gives non-overlapping pieces (the last may be shorter). Each item is itself a slice, borrowing from the array: no copying at all.
String slices
A &str is a slice of a string's UTF-8 bytes: a pointer and a length, exactly like &[u8] but guaranteed to be valid text. &s[0..5] takes a string slice by byte positions:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..];
println!("{hello}|{world}");
}
hello|world
Byte positions matter: with non-ASCII text, a slice boundary in the middle of a multi-byte character panics. The next module is all about strings and UTF-8.
Common mistakes
Out-of-range slices panic. &a[2..10] on a five-element array: "range end index 10 out of range for slice of length 5". Start greater than end also panics.
Taking &Vec<T> or &String parameters. It works, but only for that exact type. Prefer &[T] and &str.
Modifying the owner while a slice is in use (E0502), the same as any borrow:
fn main() {
let mut v = vec![1, 2, 3];
let head = &v[..2];
v.push(4);
println!("{:?}", head);
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> main.rs:4:5
|
3 | let head = &v[..2];
| - immutable borrow occurs here
4 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{:?}", head);
| ---- immutable borrow later used here
push may reallocate the buffer head points into.
Recap
&a[start..end]borrows a contiguous run of elements;&[T]and&mut [T]are slice types.- A slice reference is a fat pointer: address plus length (16 bytes); it has no capacity and cannot grow.
- Arrays and vectors convert to slices automatically;
&[T]and&strare the best read-only parameter types. split_at_mutgives two non-overlapping mutable slices;windowsandchunksiterate over sub-slices.&stris a slice of UTF-8 bytes; string slicing uses byte positions.
Going further: look through the method list in the docs for slice (doc.rust-lang.org/std/primitive.slice.html). Try binary_search on a sorted slice and concat on a slice of slices.