Note: I’ll be adding to this in the future
This is not a general introduction to Rust’s borrow system or the use of lifetimes, but notes on scenarios I’ve encountered and how I’ve understood them to work. People far more capable than me have covered this topic, such as here and here, for example. These are written as much for myself as they are to help people who may be experiencing particular pain points within Rust and who want to understand why that’s the case, and how to fix them.
Let’s say you have an array People objects set up like this:
struct Person {
age: i32,
is_oldest: bool,
}
fn main() {
let people = [
Person {
age: 10,
is_oldest: false,
},
Person {
age: 8,
is_oldest: false,
},
Person {
age: 14,
is_oldest: false,
},
];
}
You want to loop through and find the oldest person and modify their record. In C you can do something like this, where you loop through an save a reference to the object you want to modify, but in Rust that’s not possible.
For example, this code will not compile:
struct Person {
age: i32,
is_oldest: bool,
}
fn main() {
let people = [
...
];
let mut oldest = &people[0];
for person in people.iter() {
if person.age > oldest.age {
oldest = person;
}
}
oldest.is_oldest = true;
println!("{}", oldest.age)
}
The guilty line is as shown below, and the error given will be something along the lines of: … oldest is a & reference, so the data it refers to cannot be written.
oldest.is_oldest = true;
The issue is that oldest is a mutable reference, but what it references is not mutable. Think of oldest as a name tag, which can easily be scratched out and rewritten, but the person it’s attached to can’t be. Remember that Rust defaults to immutable, and that we declared our array as let people, without using the keyword mut. There is another reason it’s not mutable, and that’s our use of iter(), which when iterating over our array only gives us read access (immutable) to the elements.
So we have have a few reasons we can’t set is_oldest:
Let’s see if we can change these and get a C-like result. First, let’s set our array to be mutable:
fn main() {
let mut people = [
...
];
let mut oldest = &people[0];
for person in people.iter() {
if person.age > oldest.age {
oldest = person;
}
}
oldest.is_oldest = true;
println!("{}", oldest.age)
}
Unfortunately, the error still persists, so let’s move on to the second error and change the way we declare oldest.
fn main() {
let mut people = [
...
];
let mut oldest = &mut people[0];
for person in people.iter() {
if person.age > oldest.age {
oldest = person;
}
}
oldest.is_oldest = true;
println!("{}", oldest.age)
}
Doing this has moved the error inside our loop, producing and error that types differ in mutability for our assignment of person to oldest. This is what we expected based on the third bullet point, so let’s now fix that by changing iter() to iter_mut(), which allows mutability.
fn main() {
let mut people = [
...
];
let mut oldest = &mut people[0];
for person in people.iter() {
if person.age > oldest.age {
oldest = person;
}
}
oldest.is_oldest = true;
println!("{}", oldest.age)
}
This is where things get interesting, as we get an error on the people.iter_mut() portion of our loop:
let mut oldest = &mut people[0];
-------------- first mutable borrow occurs here
for person in people.iter_mut() {
^^^^^^^^^^^^^^^^^ second mutable borrow occurs here
if person.age > oldest.age {
---------- first borrow later used here
When we create oldest and assign it a reference to people[0] we’re saying that only it can access AND modify that element of the array. This is Rust’s ownership system at work, making sure we don’t have multiple mutable references to a single object. But why can’t you have multiple mutable references to a single object or place in memory?
The reasons come down to the guarantees that Rust makes, such as that it will clean up your memory allocation automatically, without a garbage collector. In C we have to allocate and deallocate memory manually, while in many other languages garbage collection is done automatically by a process that runs in the background.
Both of these can be problematic, as programmers may forget to handle the memory properly, causing memory leaks, while the garbage collection process takes up valuable CPU cycles. Rust handles both these by causing you headaches as your wet circuits rewire themselves into understanding the borrow system.
There are innumerable ways to fix this, the easiest of which is to use indices. Instead of tracking a reference, we instead track the index of the older person and update that after we’re done with the loop. That code, in its completed form, looks like this:
struct Person {
age: i32,
is_oldest: bool,
}
fn main() {
let mut people = [
Person {
age: 10,
is_oldest: false,
},
Person {
age: 8,
is_oldest: false,
},
Person {
age: 14,
is_oldest: false,
},
];
let mut oldest_idx = 0;
for (idx, person) in people.iter().enumerate() {
if person.age > people[oldest_idx].age {
oldest_idx = idx;
}
}
people[oldest_idx].is_oldest = true;
println!("{}", people[oldest_idx].age)
}
The primary changes are setting the array to be mutable and no longer taking a reference, but instead using an index. You’ll also note the use of enumerate, which is a handy way to get an index for any iter.
There are other ways to solve this problem, such as switching over to using vectors, but the above is a straightforward and easy to understand method that you’ll probably end up using a lot.