In JavaScript, a Closure refers to a function that can remember and access variables from its outer scope, even after that outer scope has finished executing. In short, when a function is defined inside another function and accesses variables from the parent function, a closure is created.
Example:
function makeCounter() {
let count = 0; // variable in the scope of makeCounter
return function() {
count++; // closure accesses the variable count
return count;
};
}
const counter1 = makeCounter(); // Create closure
console.log(counter1()); // 1
console.log(counter1()); // 2
count
only exists in makeCounter
, but the returned function is still able to access it. The proof is that each time counter1()
is called, count
increments.
In Rust, closures are also similar to those in JavaScript; they can remember and use variables from the outer scope where they are defined. However, closures in Rust are much more complex than in JavaScript.
Closures in Rust are anonymous functions that you can store in a variable or pass as arguments to other functions. You can create a closure in one place and call it in another.
Unlike functions, closures can capture values from the scope they are defined in. Let’s compare a function with a closure.
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
println!("{}", add(1, 2));
}
To call add
, you must pass two parameters a
and b
, and the function returns the sum of those two numbers.
Meanwhile, the closure is written as follows.
fn main() {
let a = 1;
let add = |b| a + b;
println!("{}", add(2)); // 3
}
add
is a closure; when calling add
, it can access the variable a
from the outer environment.
A closure can be declared by assigning it to a variable or calling it directly from the execution point. |b| a + b
is a shorthand notation for the closure. The full notation looks like this:
fn main() {
let a = 1;
let add = |b: u32| -> u32 {
a + b
};
println!("{}", add(2)); // 3
}
Closures can capture values from their environment in three ways:
Immutable borrowing.
Mutable borrowing.
Ownership transfer.
The closure will decide which method to use based on the processing in the body of the closure.
Example.
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let only_borrows = || println!("From closure: {list:?}");
println!("Before calling closure: {list:?}");
only_borrows();
println!("After calling closure: {list:?}");
}
only_borrows
simply prints out the list in list
, so this closure only borrows immutably.
Consider the following example.
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
borrows_mutably
calls a push
function on list
, changing the original list
, so this closure borrows mutably.
A closure takes ownership as shown in the example below.
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}
After a closure has captured a reference or taken ownership of values from the environment where the closure is defined, the code in the body of the closure will determine what happens to the references or values when the closure is executed. A closure can perform any of the following actions: move the captured value out of the closure, change the value, or not affect it at all.
Thus, there are a total of three types of closures implementing the traits Fn
, FnMut
, and FnOnce
, corresponding to the three ways that closures "treat" the ownership of the values from the environment they capture: immutable, mutable, and ownership transfer.
Why implement these three types? Because it determines how closures can be used in certain situations. For example, the sort_by_key
method used for sorting lists can only use closures implementing Fn
.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
If you deliberately use a closure with a different trait implementation like FnMut
, the program will not run. For example, the following program will report an error cannot move out of value, a captured variable in an FnMut closure
.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
Closures in Rust, although conceptually similar to those in JavaScript, are much more complex. They allow capturing values from the outer scope in three ways: immutable borrowing, mutable borrowing, and ownership transfer, depending on how they are used in the closure body. This is controlled through three main traits: Fn
, FnMut
, and FnOnce
, each suited for different situations. This difference not only makes closures in Rust more flexible but also ensures memory safety and strict ownership management, a core factor in Rust's design.
The secret stack of Blog
As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!
Subscribe to receive new article notifications
Comments (0)