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
}
...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
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
}
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;
}
...but the compiler is angry again:
error[E0594]: cannot assign to `num.1`, which is behind a `&` reference
--> src/main.rs:12:5
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;
}
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();
}
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 "";
}
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 "";
}
The borrow check Angel points out the bug:
error[E0515]: cannot return reference to temporary value
--> src/main.rs:9:16
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();
}
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;
}
}
}
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
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:?}")
}
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)