Slices of arrays and strings
In Rust a slice is a borrowed view of several values in a row. It is two words at run time, a pointer and a length, and it is the type a function should take when it needs to read a sequence without owning it. A string slice, &str, is the same idea for UTF-8 bytes.
Slices matter because they make one function accept many containers. A function that takes &[i32] works with an array, with part of an array and with a Vec, without a generic parameter and without a copy. This lesson covers the array cases; the lesson on Vec adds the last one.
A slice is a view
Taking &numbers from an array produces a slice automatically, and the parameter type &[i32] accepts it. The function walks the values with a for loop and never learns how many there are at compile time.
The array stays where it is, and the slice points into it. The sum is fifteen, and the caller can use the array again after the call because nothing was moved.
fn sum(values: &[i32]) -> i32 {
let mut total = 0;
for value in values {
total += value;
}
total
}
fn main() {
let numbers = [3, 5, 7];
println!("{}", sum(&numbers));
}
rustc --edition 2024 sum_slice.rs
./sum_slice
15
The same function accepts &vec[..] and a literal &[1, 2, 3][..], which is why library functions take slices rather than the concrete containers. The lesson on arrays covers the fixed-size type in full.
Ranges and sub-slices
A range in square brackets takes a part of a sequence: &values[1..4] is the values at indices one, two and three. The end is exclusive, and a range that leaves the bounds panics at run time, because the length may only be known then.
The debug format prints the slice in square brackets, and len counts its elements. The output below shows both, which is the quickest way to check a slice in a program.
fn main() {
let values = [10, 20, 30, 40, 50];
let middle = &values[1..4];
println!("{middle:?} len {}", middle.len());
}
rustc --edition 2024 subslice.rs
./subslice
[20, 30, 40] len 3
A slice of a slice works the same way, and the two share the underlying values. The lesson on &str and UTF-8 applies ranges to strings, where the indices must also fall on character boundaries.
A string slice
&str is a slice of bytes that are known to be valid UTF-8. A String borrows as &str, and a literal already is one. The function below returns the first five bytes as a slice without copying them.
The returned slice points into the caller string, which is why the signature needs a lifetime when the compiler cannot infer one; here there is a single input reference, so elision fills it in. The next lesson explains that rule.
fn head(text: &str) -> &str {
&text[..5]
}
fn main() {
let sentence = String::from("hello world");
let head = head(&sentence);
println!("{head}");
}
rustc --edition 2024 str_view.rs
./str_view
hello
The borrow checker keeps the String alive while the slice is in use, so the view cannot dangle. The lesson on lifetimes shows the annotation that makes the same guarantee explicit.
Words from a line
split_whitespace returns an iterator of slices, one per word, with the spaces removed. Each item borrows from the original line, so the loop prints a word and its length without allocating anything.
The program counts the bytes of each word, which is the same as the number of characters for ASCII. The lesson on UTF-8 shows where that equality stops holding.
fn main() {
let line = "alpha beta gamma";
for word in line.split_whitespace() {
println!("{word} {}", word.len());
}
}
rustc --edition 2024 split.rs
./split
alpha 5
beta 4
gamma 5
The iterator is lazy: nothing is split until the loop asks for the next item. The lesson on iterators explains the trait behind that behaviour.
A mutable slice
A mutable slice, &mut [i32], lets a function change the elements in place. The caller passes &mut values, and the loop uses iter_mut to get a mutable reference per element, which is why the body writes *value.
The array is printed with the debug format after the call, and every element is doubled. No new sequence was allocated: the function wrote through the borrow into the caller array.
fn double_all(values: &mut [i32]) {
for value in values.iter_mut() {
*value *= 2;
}
}
fn main() {
let mut values = [1, 2, 3];
double_all(&mut values);
println!("{values:?}");
}
rustc --edition 2024 mut_slice.rs
./mut_slice
[2, 4, 6]
A mutable slice is a mutable borrow, so the caller may not read the array while the function runs. That is the rule from mutable borrows applied to a range of elements.
Forgetting the borrow
Passing an array where a slice is expected needs the ampersand. Without it the compiler sees a value of type [{integer}; 3] and a parameter of type &[i32], and reports E0308 with a help line that adds the missing character.
The message also shows the function signature, which is the fastest way to see what the parameter really wants. The fix is one character at the call site.
fn sum(values: &[i32]) -> i32 {
values.len() as i32
}
fn main() {
let numbers = [3, 5, 7];
println!("{}", sum(numbers));
}
rustc --edition 2024 no_ref_err.rs
error[E0308]: mismatched types
--> no_ref_err.rs:7:24
|
7 | println!("{}", sum(numbers));
| --- ^^^^^^^ expected `&[i32]`, found `[{integer}; 3]`
| |
| arguments to this function are incorrect
|
note: function defined here
--> no_ref_err.rs:1:4
|
1 | fn sum(values: &[i32]) -> i32 {
| ^^^ --------------
help: consider borrowing here
|
7 | println!("{}", sum(&numbers));
| +
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
A Vec passed by value would give the same error, with the type
Vec
Exercises
- Write a function that returns the largest value of a slice. Which type does the parameter need, and does the function allocate?
- Split a line on commas with split(',') instead of whitespace and print each field. Which field has a leading space?
- Change the mutable slice to reverse the array in place. Why does the loop body need a mutable reference to each element?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a slice in Rust?
Answer
A borrowed view of several values in a row
A slice is a pointer and a length over values owned elsewhere, so it never copies them.
-
Which parameter type accepts an array, a part of it and a Vec?
Answer
&[T]
A slice is the common view of every contiguous sequence, which is why library functions take it.
-
What does the range
1..4in a slice include?Answer
Indices 1, 2 and 3
The end of a range with two dots is exclusive, and 1..=4 would include four.
-
What does a mutable slice allow a function to do?
Answer
Change the elements in place
The elements are changed through the borrow, and the sequence itself stays where it is.
-
What does this program print?
fn sum(values: &[i32]) -> i32 { let mut total = 0; for value in values { total += value; } total } fn main() { let numbers = [3, 5, 7]; println!("{}", sum(&numbers)); }Answer
15
The slice borrows the three values and the loop adds them to fifteen.
-
What does this program print?
fn main() { let values = [10, 20, 30, 40, 50]; let middle = &values[1..4]; println!("{middle:?} len {}", middle.len()); }Answer
[20, 30, 40] len 3
The range starts at one and stops before four, which is three values.
-
What does this program print?
fn head(text: &str) -> &str { &text[..5] } fn main() { let sentence = String::from("hello world"); println!("{}", head(&sentence)); }Answer
hello
The first five bytes of the string are hello, and the borrow keeps the string alive for the print.
-
What does this program print?
fn main() { let line = "alpha beta"; for word in line.split_whitespace() { println!("{} {}", word, word.len()); } }Answer
alpha 5 / beta 4
split_whitespace gives two slices, and their lengths are five and four bytes.
-
What does this program print?
fn double_all(values: &mut [i32]) { for value in values.iter_mut() { *value *= 2; } } fn main() { let mut values = [1, 2, 3]; double_all(&mut values); println!("{values:?}"); }Answer
[2, 4, 6]
Each element is doubled through a mutable reference, and the array prints with its new values.
-
What is the first line of the compiler output for this program?
fn sum(values: &[i32]) -> i32 { values.len() as i32 } fn main() { let numbers = [3, 5, 7]; println!("{}", sum(numbers)); }Answer
error[E0308]: mismatched types
The call passes the array by value where a slice is expected, and the help adds the ampersand.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev