DEV Community

Brian Maher
Brian Maher

Posted on • Edited on

Rust Borrow Checker Explained Part 2: Borrowing

Intro

In our last article we discussed how to avoid the borrow checker by making copies. In this article, I will introduce the concept of borrowing which allows us to avoid copying and thus increases the performance of our code.

More specifically, we will see:

  • How to perform an immutable borrow using &.
  • How to perform a mutable borrow using &mut.
  • How the borrow check "Angel" will catch errors and keep our code bug free.
  • How to use * to unwrap the borrow.

Borrowing

It would be a lot more efficient if you could just pass in a pointer instead of copying data around. This is exactly what borrowing is in Rust.

In the first part of this series, we tried to compile this code:

#[derive(Debug)]
struct Num(i32);

fn main() {
    let num = Num(2);
    let even = is_even(num);
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: Num) -> bool {
    num.0 % 2 == 0
}
Enter fullscreen mode Exit fullscreen mode

Try it!

...and we saw the borrow of moved value error, but if you read a bit further in the error, you would see this:

consider changing this parameter type in function `is_even`
to borrow instead if owning the value isn't necessary
Enter fullscreen mode Exit fullscreen mode

This suggestion is a better solution than using clone(), since we can avoid copying data.

To borrow in Rust, you use & in front of the thing you are borrowing, so we change num.clone() to &num. We also need to declare our is_even() function to take a borrowed &Num type by adding that & in front again:

#[derive(Debug, Clone)]
struct Num(i32, String);

fn main() {
    let num = Num(2, "two".into());
    let even = is_even(&num);
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: &Num) -> bool {
    num.0 % 2 == 0
}
Enter fullscreen mode Exit fullscreen mode

Try it!

Under the covers, instead of copying the entire Num(2, "two") structure onto the stack, a pointer to this structure is placed on the stack, and we avoid copying.

Mutable Borrows

Let's say we want our is_even() function to have a side-effect of modifying the String associated with the Num borrowed in. Now that we are simply pushing a pointer onto the stack, we should be able to produce a side-effect:

#[derive(Debug, Clone)]
struct Num(i32, String);

fn main() {
    let num = Num(2, "two".into());
    let even = is_even(&num);
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: &Num) -> bool {
    let even = num.0 % 2 == 0;
    num.1 = if even { "even".into() } else { "odd".into() };
    return even;
}
Enter fullscreen mode Exit fullscreen mode

Try it!

...but the compiler is angry again:

error[E0594]: cannot assign to `num.1`, which is behind a `&` reference
  --> src/main.rs:12:5
Enter fullscreen mode Exit fullscreen mode

This is because everything is immutable by default in Rust. To get over this, we need to declare our references to be mutable, and we need the num in main to also be declared mut:

#[derive(Debug, Clone)]
struct Num(i32, String);

fn main() {
    let mut num = Num(2, "two".into());
    let even = is_even(&mut num);
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: &mut Num) -> bool {
    let even = num.0 % 2 == 0;
    num.1 = if even { "even".into() } else { "odd".into() };
    return even;
}
Enter fullscreen mode Exit fullscreen mode

Try it!

Easy!

The Borrow Check Angel

Let's switch it up and try implementing a function that splits a string on :.

Copying Split

One straight forward way to implement this is to take the owned String and return an owned String:

fn main() {
    let key = split_key("left: right".into());
    print!("key={key}");
}

fn split_key(str: String) -> String {
    if let Some(colon) = str.find(':') {
        return str[0..colon].into();
    }
    return "".into();
}
Enter fullscreen mode Exit fullscreen mode

Try it!

However, this has a bunch of copies when performing conversions using .into().

Borrowing Split

It would be more efficient, if we borrowed the string we split on... and since the result is a substring of the input, we could also have the return type be borrowed too:

fn main() {
    // Avoid .into() since "string" type is &str by default:
    let key = split_key("left: right");
    print!("key={key}");
}
// param is now &str, and result is &str:
fn split_key(str: &str) -> &str {
    if let Some(colon) = str.find(':') {
        // Avoid .into(), and use `&` to borrow the result:
        return &str[0..colon];
    }
    // Avoid .into()
    return "";
}
Enter fullscreen mode Exit fullscreen mode

Try it!

This implementation completely removes all the string copies from the previous implementation.

Add Parens To Split Result

Requirements changed, and we now want square braces to always be added to the result of split_key(). The format! macro allows us to easily add the square braces. The first argument is our format string "[{}]", the {} is a place holder for the &str[0..colon] argument, and then we borrow the result:

fn main() {
    let key = split_key("left: right");
    print!("key={key}");
}

fn split_key(str: &str) -> &str {
    if let Some(colon) = str.find(':') {
        return &format!("[{}]", &str[0..colon]);
    }
    return "";
}
Enter fullscreen mode Exit fullscreen mode

Try it!

The borrow check Angel points out the bug:

error[E0515]: cannot return reference to temporary value
 --> src/main.rs:9:16
Enter fullscreen mode Exit fullscreen mode

The result of format! is an "owned" String which is a stack local variable, and so we can't just borrow it and return the result. If the compiler did not catch this, then we would have a "dangling" pointer which would be invalid after the split_key() function returns.

To fix this, we need to change the function so it returns a String type:

fn main() {
    let key = split_key("left: right");
    print!("key={key}");
}

fn split_key(str: &str) -> String {
    if let Some(colon) = str.find(':') {
        return format!("[{}]", &str[0..colon]);
    }
    return "".into();
}
Enter fullscreen mode Exit fullscreen mode

Mutable Borrow Exclusivity Rule

One more rule to be aware of is that there can only be a single mutable borrowed value. Let's say that we want to write code that modifies a vector of numbers so that all numbers are odd.

fn main() {
    let mut vec = vec![1, 2, 3];

    for (idx, num) in vec.iter().enumerate() {
        if num % 2 == 0 {
            vec[idx] = num + 1;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Try it!

The borrow checking Angel helped us out again:

error[E0502]: cannot borrow `vec` as mutable because it is
also borrowed as immutable
 --> src/main.rs:6:13
Enter fullscreen mode Exit fullscreen mode

In Java iterating over a collection and mutating at it the same time, could result in a ConcurrentModificationException at runtime.

To fix this, we just need to use iter_mut() instead which iterates over the Vec elements as a &mut element type. The * syntax is needed to unwrap the borrow:

fn main() {
    let mut vec = vec![1, 2, 3];

    for num in vec.iter_mut() {
        if *num % 2 == 0 {
            *num = *num + 1;
        }
    }
    print!("vec={vec:?}")
}
Enter fullscreen mode Exit fullscreen mode

Try it!

Borrowing Recap

To recap, we can increase our code performance by borrowing, which under the covers is just a fancy name for a pointer. There are two variations on borrowing, an immutable borrow using & syntax and a mutable borrow which uses the &mut syntax. Mutable borrows are exclusive, so no other borrowing of the same instance is allowed.

We also saw how the "borrow checker" is actually very helpful in pointing out when you attempt to use a borrowed value outside of the bounds of the "owned" objects lifetime.

So far, the lifetimes of all of our borrows have been implicit, but in Part 3 we will discuss how to explicitly declare lifetimes.

Top comments (0)