Borrowing and Slices · about 40 min

References: Borrowing Without Taking

Why this matters

The last module ended on a problem: if a function needs to look at a String, passing it moves it, and the caller loses it. Returning it every time is clumsy, cloning is wasteful. What we want is to let a function use a value without owning it. That is a reference, and creating one is called borrowing. References are everywhere in Rust code: nearly every function that reads data takes a reference. This lesson covers shared (read-only) references; the next adds mutable ones.

Creating and using a reference

fn length(s: &String) -> usize {
    s.len()
}

fn main() {
    let s1 = String::from("borrowing");
    let n = length(&s1);
    println!("{s1:?} has {n} bytes");
}
"borrowing" has 9 bytes

What a reference is in memory

A reference is a pointer: the memory address of the value it refers to. It is 8 bytes on a 64-bit machine.

 length's frame                 main's frame                        heap
 +-----------+                  +----------------------+
 | s: addr --------------------> | s1: ptr  -------------------->  [b|o|r|r|o|w|i|n|g]
 +-----------+                  |     len 9            |
                                |     cap 9            |
                                +----------------------+

s points at s1, the String handle in main's frame, which in turn points at the heap buffer. The String is never copied and ownership never changes hands. Passing a reference costs the same as passing an integer.

fn main() {
    let x = 42;
    let r = &x;
    println!("x = {x}, r = {r}, *r = {}", *r);
    println!("r points at {:p}", r);
    println!("a reference is {} bytes", std::mem::size_of::<&i32>());
}
x = 42, r = 42, *r = 42
r points at 0x7ffcee654f14
a reference is 8 bytes

Dereferencing

To get at the value a reference points to, use the dereference operator *:

fn main() {
    let a = 10;
    let b = &a;
    let c = *b + 1;
    println!("{c}");
    println!("{}", b == &10);
    println!("{}", *b == 10);
}
11
true
true

In practice you write * less often than you might expect, because Rust dereferences automatically in several places:

Arithmetic and comparisons between a reference and a plain value do not auto-deref: *b + 1 needs the star; b + 1 happens to work for numbers because the standard library defines + for &i32 too, but do not rely on that in general.

Borrowing rules, part 1

While a value is borrowed, the owner must respect the borrow. In particular, a value cannot be moved while a reference to it is still in use:

fn main() {
    let s = String::from("hello");
    let r = &s;
    let t = s;
    println!("{r}");
}
error[E0505]: cannot move out of `s` because it is borrowed
 --> main.rs:4:13
  |
2 |     let s = String::from("hello");
  |         - binding `s` declared here
3 |     let r = &s;
  |             -- borrow of `s` occurs here
4 |     let t = s;
  |             ^ move out of `s` occurs here
5 |     println!("{r}");
  |               --- borrow later used here
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let r = &s.clone();
  |               ++++++++

E0505. If the move were allowed, t could later free the buffer while r still pointed at s. The compiler reasons about how long each reference is used (here, until the println!), and refuses any move, drop or modification of the owner in that window. The next two lessons explore these rules in depth. For now: the owner may not invalidate what a reference is looking at, while the reference is used.

Shared references can be freely duplicated; any number may exist at once:

fn main() {
    let s = String::from("shared");
    let r1 = &s;
    let r2 = &s;
    let r3 = r1;
    println!("{r1} {r2} {r3} {s}");
}
shared shared shared shared

&T is Copy: copying a pointer that only reads is harmless.

&str is the better parameter type

You have seen &str since the first module. It is a reference to text: a pointer plus a length (module 6 explains its layout). For a function that only reads text, &str is more flexible than &String:

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

fn main() {
    let owned = String::from("owned string");
    println!("{}", shout(&owned));
    println!("{}", shout("a literal"));
}
OWNED STRING
A LITERAL

A string literal is already a &str, and a &String is automatically converted to a &str where one is expected (a deref coercion). With a &String parameter, the literal call would not compile. The same idea applies to lists: prefer &[T] (a slice, lesson 5) over &Vec<T>.

So the parameter table from the last module grows:

parametercaller keeps the value?function may modify?
s: Stringno, it is movedyes (if mut s)
s: &str / s: &Stringyesno
s: &mut Stringyesyes (next lesson)

Iterating by reference

Loops over references are the same idea. for x in &array borrows the array and gives a reference to each element, leaving the array usable afterwards:

fn main() {
    let words = [String::from("alpha"), String::from("beta")];
    for w in &words {
        println!("{w} ({} bytes)", w.len());
    }
    println!("still have {} words", words.len());
}
alpha (5 bytes)
beta (4 bytes)
still have 2 words

Without the &, for w in words would move each String out of the array, and the array would be unusable afterwards.

Common mistakes

Forgetting the & at the call site: length(s1) where &String is expected gives E0308 "expected &String, found String" with the help "consider borrowing here: &s1".

Using * where it is not needed is harmless but noisy; using it on a non-reference is an error (E0614 "type i32 cannot be dereferenced").

Moving the owner while a reference is alive (E0505): use the reference first, or move later.

Thinking a reference keeps a value alive. It does not. A reference must never outlive its owner, and the compiler enforces that (lesson 4). There is no reference counting behind &.

Recap

Going further: print {:p} of a String's handle (&s) and of its buffer (s.as_ptr()) and compare them: one is a stack address, the other a heap address. The Rust Book section 4.2 covers references with the same diagrams.

Exercises

Exercise 1 · write code

Borrow instead of move

count_vowels takes ownership of its argument, so main cannot print the word afterwards. Change the function to borrow (parameter type &str) and change the call accordingly, so the program prints:

"education" has 5 vowels

The word comes from stdin. Do not use clone.

File: main.rs

Starting point

use std::io;

fn count_vowels(s: String) -> usize {
    let mut n = 0;
    for c in s.chars() {
        if "aeiouAEIOU".contains(c) {
            n += 1;
        }
    }
    n
}

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let word = line.trim().to_string();
    let n = count_vowels(word);
    println!("{:?} has {n} vowels", word);
}

Example

input

education

expected output

"education" has 5 vowels

Example

input

rhythm

expected output

"rhythm" has 0 vowels
Hint 1

Change the signature to fn count_vowels(s: &str) -> usize.

Hint 2

Call it as count_vowels(&word): the & lends the String without giving it away.

Reference solution
use std::io;

fn count_vowels(s: &str) -> usize {
    let mut n = 0;
    for c in s.chars() {
        if "aeiouAEIOU".contains(c) {
            n += 1;
        }
    }
    n
}

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let word = line.trim().to_string();
    let n = count_vowels(&word);
    println!("{:?} has {n} vowels", word);
}

Exercise 2 · write code

Read through a reference

Write fn total(values: &[i32; 5]) -> i32 that returns the sum of the array it borrows, and fn bigger(a: &i32, b: &i32) -> i32 that returns the larger of the two values (as a plain i32, by dereferencing). The given main prints:

total 15
bigger 9

File: main.rs

Starting point

fn main() {
    let data = [1, 2, 3, 4, 5];
    println!("total {}", total(&data));
    let x = 4;
    let y = 9;
    println!("bigger {}", bigger(&x, &y));
}

Example

expected output

total 15
bigger 9
Hint 1

for v in values over a &[i32; 5] gives &i32 items; add *v.

Hint 2

*a reads the i32 that a points to.

Reference solution
fn total(values: &[i32; 5]) -> i32 {
    let mut sum = 0;
    for v in values {
        sum += *v;
    }
    sum
}

fn bigger(a: &i32, b: &i32) -> i32 {
    if *a > *b { *a } else { *b }
}

fn main() {
    let data = [1, 2, 3, 4, 5];
    println!("total {}", total(&data));
    let x = 4;
    let y = 9;
    println!("bigger {}", bigger(&x, &y));
}

Exercise 3 · quiz

What is a reference?

Which statements about let r = &s; (where s is a String) are true? Select all that apply.

Answer
  • r holds the address of s; on a 64-bit machine it is 8 bytes

  • When r goes out of scope, nothing is freed

  • Copying r (let r2 = r;) is allowed and leaves r usable

A reference is a pointer to a value owned by someone else. It owns nothing, so dropping it frees nothing, and s stays the owner. Shared references are Copy: duplicating a pointer is harmless. Note that r points at the String handle (s on the stack), which in turn points at the heap buffer.

Exercise 4 · quiz

Which parameter type?

A function only reads some text. Which parameter type is the most flexible choice for callers who may have a String, a string literal, or part of a string?

Answer
  • s: &str

&str accepts string literals directly, any &String (Rust converts it automatically, a "deref coercion"), and slices of strings. &String only accepts references to a String, and String forces callers to give up or clone their value. For read-only text, &str is the idiomatic choice.