Borrowing and Slices · about 40 min

Slices: Borrowing Part of a Collection

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

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

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.

Exercises

Exercise 1 · write code

Sum a slice

Write fn sum(values: &[i64]) -> i64 returning the sum of a slice. The given main reads integers (one per line) into a Vec, then prints the sum of all of them, of the first half and of the second half (for an odd count, the middle element belongs to the second half):

all: 21
first half: 6
second half: 15

for the input 1 2 3 4 5 6. Hidden tests call sum on arrays and empty slices too.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut v: Vec<i64> = Vec::new();
    for line in io::stdin().lines() {
        v.push(line.unwrap().trim().parse().unwrap());
    }
    let mid = v.len() / 2;
    println!("all: {}", sum(&v));
    println!("first half: {}", sum(&v[..mid]));
    println!("second half: {}", sum(&v[mid..]));
}

Example

input

1
2
3
4
5
6

expected output

all: 21
first half: 6
second half: 15

Example

input

10
20
30

expected output

all: 60
first half: 10
second half: 50
Hint 1

for v in values { total += v; } works: adding a &i64 to an i64 is defined.

Reference solution
use std::io;

fn sum(values: &[i64]) -> i64 {
    let mut total = 0;
    for v in values {
        total += v;
    }
    total
}

fn main() {
    let mut v: Vec<i64> = Vec::new();
    for line in io::stdin().lines() {
        v.push(line.unwrap().trim().parse().unwrap());
    }
    let mid = v.len() / 2;
    println!("all: {}", sum(&v));
    println!("first half: {}", sum(&v[..mid]));
    println!("second half: {}", sum(&v[mid..]));
}

Exercise 2 · write code

Reverse a slice in place

Write fn reverse(s: &mut [i32]) that reverses the slice in place by swapping elements from both ends towards the middle, using s.swap(i, j) (do not use the built-in s.reverse()). The given main reverses a whole array and then only the middle part of another:

[5, 4, 3, 2, 1]
[1, 4, 3, 2, 5]

File: main.rs

Starting point

fn main() {
    let mut a = [1, 2, 3, 4, 5];
    reverse(&mut a);
    println!("{:?}", a);
    let mut b = [1, 2, 3, 4, 5];
    reverse(&mut b[1..4]);
    println!("{:?}", b);
}

Example

expected output

[5, 4, 3, 2, 1]
[1, 4, 3, 2, 5]
Hint 1

Swap element i with element n - 1 - i for i in 0..n / 2.

Hint 2

An empty slice has n = 0, and the loop simply does not run.

Reference solution
fn reverse(s: &mut [i32]) {
    let n = s.len();
    for i in 0..n / 2 {
        s.swap(i, n - 1 - i);
    }
}

fn main() {
    let mut a = [1, 2, 3, 4, 5];
    reverse(&mut a);
    println!("{:?}", a);
    let mut b = [1, 2, 3, 4, 5];
    reverse(&mut b[1..4]);
    println!("{:?}", b);
}

Exercise 3 · quiz

How big is a slice reference?

On a 64-bit machine, what is std::mem::size_of::<&[u8]>(), and why?

Answer
  • 16: a pointer to the first element plus the number of elements

A slice's length is not known at compile time, so a reference to a slice carries it along: a "fat pointer" of two words, address and length. That is what makes bounds checking possible on a borrowed part of an array. It has no capacity because a slice cannot grow: it only views memory someone else owns.

Exercise 4 · write code

Best window

Read integers from stdin (one per line) and then find the run of 3 consecutive values with the largest sum. Print the window as a slice with {:?} and its sum. For input 1 9 2 8 3 7 4 (one per line):

[9, 2, 8] = 19

If two windows tie, report the first. If fewer than 3 numbers are given, print not enough data. Use slices &v[i..i + 3] (or v.windows(3)) rather than adding three indexed values by hand.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut v: Vec<i64> = Vec::new();
    for line in io::stdin().lines() {
        v.push(line.unwrap().trim().parse().unwrap());
    }
}

Example

input

1
9
2
8
3
7
4

expected output

[9, 2, 8] = 19

Example

input

-1
-2
-3

expected output

[-1, -2, -3] = -6
Hint 1

Check the length first; v.len() - 3 on a usize would underflow and panic for short input.

Hint 2

Remember the start index of the best window (an index, not a reference), compare with > to keep the first on ties.

Reference solution
use std::io;

fn sum(s: &[i64]) -> i64 {
    let mut t = 0;
    for x in s {
        t += x;
    }
    t
}

fn main() {
    let mut v: Vec<i64> = Vec::new();
    for line in io::stdin().lines() {
        v.push(line.unwrap().trim().parse().unwrap());
    }
    if v.len() < 3 {
        println!("not enough data");
        return;
    }
    let mut best = 0;
    for i in 1..=v.len() - 3 {
        if sum(&v[i..i + 3]) > sum(&v[best..best + 3]) {
            best = i;
        }
    }
    let w = &v[best..best + 3];
    println!("{:?} = {}", w, sum(w));
}