Closures
In Chapter 4.1 we had a first look at a closure. In this chapter, we'll dive deeper into closures, especially how they can capture their environment, and the effect this has on ownership and borrowing.
What does "capturing their environment" mean? It means that a closure can use variables from the scope in which it was defined. An example will make this clearer.
fn main() { let name = "Marcel".to_string(); let greeting = "Hello".to_string(); let print_greeting = || { println!("{greeting}, {name}"); }; print_greeting(); println!("{name}"); }
In this case the closure borrows name
and greeting
from the scope in which it was defined. This is called a
"borrowing closure". The closure borrows the variables, and does not take ownership of them.
If you want to take ownership of a variable, you can use the move
keyword. This will move the variables into the
closure.
fn main() { let name = "Marcel".to_string(); let greeting = "Hello".to_string(); let print_greeting = move || { println!("{greeting}, {name}"); }; print_greeting(); }
At first glance, this looks like the previous example, but there is a subtle difference. The closure now takes ownership
of name
and greeting
. This means that the variables are moved into the closure, and are no longer available in the
outer scope.
Let's confirm this by trying to use name
after the closure has been called.
fn main() {
let name = "Marcel".to_string();
let greeting = "Hello".to_string();
let print_greeting = move || {
println!("{greeting}, {name}");
};
print_greeting();
println!("{name}");
}
This will result in a compilation error:
error[E0382]: borrow of moved value: `name`
--> src/main.rs:10:15
|
2 | let name = "Marcel".to_string();
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
...
5 | let print_greeting = move || {
| ------- value moved into closure here
6 | println!("{greeting}, {name}");
| ---- variable moved due to use in closure
...
10 | println!("{name}");
| ^^^^^^ value borrowed here after move
|
If you have heard the sentence: "I'm fighting the borrow checker", this is what it means. The Rust compiler is preventing you from making a mistake that could lead to undefined behavior at runtime.
There are different ways to solve this issue. You could clone the variables before moving them into the closure, or you could use a reference to the variables. Let's look at both options.
fn main() { let name = "Marcel".to_string(); let greeting = "Hello".to_string(); let use_name = name.clone(); let use_greeting = greeting.clone(); let print_greeting = move || { println!("{use_greeting}, {use_name}"); }; print_greeting(); println!("{name}"); }
This will work, but it's not very elegant. You have to clone the variables, and then use the clones in the closure. This is not very efficient, especially if the variables are large.
A better way is to use references. This way you don't have to clone the variables, and you can still use them in the closure.
fn main() { let name = "Marcel".to_string(); let greeting = "Hello".to_string(); let print_greeting = move |name: &str, greeting: &str| { println!("{greeting}, {name}"); }; print_greeting(&name, &greeting); println!("{name}"); }
This will work in many cases, but it's not always possible to use references. An almost-always foolproof way to solve
this issue is to use the Rc
and RefCell
types. We'll look at these in the next chapter.
An Rc
is a reference-counted pointer to immutable data. This means that you can have multiple references to the same
data, and the data will only be dropped when the last reference is dropped. An Rc
is used when you want to have
multiple owners of the same data.
A RefCell
is a mutable memory location with dynamically checked borrow rules. This means that you can have multiple
mutable references to the same data, and the borrow checker will ensure that the references are used correctly.
Let's look at the above example using an Rc
use std::rc::Rc; fn main() { let name = Rc::new("Marcel".to_string()); let greeting = Rc::new("Hello".to_string()); let name2 = name.clone(); let greeting2 = greeting.clone(); let print_greeting = move || { println!("{greeting2}, {name2}"); }; print_greeting(); println!("{name}"); }
It looks a bit like the earlier example where we cloned the variables, but there is a subtle difference. We are not
cloning the variables, we are cloning the Rc
pointers. This means that we are not cloning the data, we are cloning the
reference to the data. This is much more efficient, especially if the data is large.
To complete the example, we'll look at the RefCell
type. We'll use the RefCell
type to make the name
and greeting
variables mutable.
use std::cell::RefCell; use std::rc::Rc; fn main() { let name = Rc::new(RefCell::new("Marcel".to_string())); let greeting = Rc::new(RefCell::new("Hello".to_string())); let name2 = name.clone(); let greeting2 = greeting.clone(); let print_greeting = move || { println!("{}, {}", greeting2.borrow(), name2.borrow()); }; print_greeting(); println!("{}", name.borrow()); // Change the name and greeting *name.borrow_mut() = "Alice".to_string(); *greeting.borrow_mut() = "Goodbye".to_string(); print_greeting(); println!("{}", name.borrow()); }
This will print:
Hello, Marcel
Marcel
Goodbye, Alice
Alice
In this example we use the borrow
and borrow_mut
methods to get a reference to the data. The borrow
method returns
an immutable reference, and the borrow_mut
method returns a mutable reference. The borrow checker will ensure that
the references are used correctly.