Ownership (part 2)
In this chapter, we'll explore a number of common ownership issues. I'll demonstrate the naive way that is causing the borrow issue and then a proposed solution.
Loops
Issue
fn main() { let people = vec!["Marcel", "Tom", "Dick", "Harry"]; for person in people { println!("Hi {person}") } for person in people { println!("Hi {person}") } }
The issue is that while iterating the first time through the people
vector, the value held in people
is moved to
person.
This means that at the end of the first loop, people
is consumed by the loop, and can no longer be used.
Solution
To fix this case, you can simply borrow from people
on the first loop.
fn main() { let people = vec!["Marcel", "Tom", "Dick", "Harry"]; for person in &people { println!("Hi {person}") } for person in people { println!("Hi {person}") } }
Optionals
Issue
struct Person { first_name: Option<String>, } fn main() { let person = Person { first_name: Some("Marcel".to_string()), }; if let Some(first_name) = person.first_name { println!("Hi {}", first_name) } if let Some(first_name) = person.first_name { println!("Hi {}", first_name) } }
The issue here is that person
is partially moved into first_name
by the first Some(first_name)
statement.
Solution
struct Person { first_name: Option<String>, } fn main() { let person = Person { first_name: Some("Marcel".to_string()), }; if let Some(first_name) = &person.first_name { println!("Hi {first_name}") } if let Some(first_name) = person.first_name { println!("Hi {first_name}") } }
You can solve this quite easily by borrowing person
in the initial if let
statement. This will actually transform
first_name
from a String
to a &String
. Allowing you to use person
in the second if let
statement.
Async tasks
Problem
use tokio::task::JoinHandle; use tokio::time::Duration; struct AnswerToLife { answer: i32, } impl AnswerToLife { fn compute(&mut self) -> JoinHandle<()> { let task = tokio::spawn(async move { tokio::time::sleep(Duration::from_secs(3)).await; self.answer = 42; }); println!("We are computing the answer to life. Please be patient..."); task } fn print(&self) { println!("The answer to life is {}", self.answer); } } #[tokio::main] async fn main() { let mut big_question = AnswerToLife { answer: 0 }; let task = big_question.compute(); task.await.unwrap(); big_question.print(); }
If this looks like abracadabra to you, have a look at the chapter on Asynchronous programming.
The problem here is that self
is moving into the new tokio::task
. This is an issue, because the lifetime of the
spawned
task can not be determined at compile time. The compiler will hint at introducing a 'static
lifetime for self
,
but that will likely set you off on a wild goose chase. Focus on what you're trying to accomplish. In
this case we want to set answer
after doing some intense computation. The problem is that in the current data-model
we need to have a mutable reference to self
to do this. Why don't we solve that, and see if we can change answer,
without a reference to self
.
Solution
use std::sync::Arc; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tokio::time::Duration; struct AnswerToLife { answer: Arc<Mutex<i32>>, } impl AnswerToLife { fn compute(&self) -> JoinHandle<()> { let lockable_reference_to_answer = self.answer.clone(); let task = tokio::spawn(async move { tokio::time::sleep(Duration::from_secs(3)).await; let mut locked_answer = lockable_reference_to_answer.lock().await; *locked_answer = 42; }); println!("We are computing the answer to life. Please be patient..."); task } async fn print(&self) { let locked_answer = self.answer.lock().await; println!("The answer to life is {locked_answer}"); } } #[tokio::main] async fn main() { let big_question = AnswerToLife { answer: Arc::new(Mutex::from(0)), }; let task = big_question.compute(); task.await.unwrap(); big_question.print().await; }
By wrapping the answer
in an Arc
we solve two issues at once. compute
no longer need a mutuable reference, because
the Arc
is read-only. And two, we can clone the Arc
which is a cheap operation and move the clone into
the tokio::task
.
Remember that we are cloning the reference to the
AnswerToLife
structure, not the structure itself.
To allow changes to the answer
after computing it, we need to wrap it in a Mutex
.
Make sure to use the
tokio::sync::Mutex
not the one from the standard library.