Since Rust 2021, a closure can capture one struct or tuple field while leaving other fields available to the surrounding code. This is called disjoint capture. It can remove borrowing conflicts and change when owned fields are dropped. The examples below use ordinary synchronous closures.
Borrow the field you use
Save this complete program as report.rs:
struct Report {
rows: Vec<String>,
total: i64,
}
fn main() {
let mut report = Report { rows: vec!["a".into()], total: 0 };
let mut push = |s: &str| report.rows.push(s.to_string());
assert_eq!(report.total, 0);
push("b");
assert_eq!(report.rows, ["a", "b"]);
}
Compile it with rustc --edition=2024 report.rs, then run the executable.
Edition 2021 also works. With --edition=2018, rustc reports a borrowing
conflict: the closure borrows all of report mutably, preventing the read
of report.total before the closure's last use.
In the newer editions, only report.rows is mutably borrowed. Reading
report.total does not overlap that borrow. The ordinary borrowing rules
still apply to each captured part.
To make this example work in edition 2018, insert
let rows = &mut report.rows; before creating the closure and change its
body to rows.push(s.to_string()). That explicitly separates the field
borrow before capture analysis runs.
move includes copying Copy values
Capture precision and capture mode answer different questions: which part
is captured, and whether the closure borrows or owns it. A move closure
captures required values by value. For Copy values, that means copying.
fn make_reader() -> impl Fn() -> i32 {
let value = 7;
move || value
}
fn main() {
let mut value = 1;
let read = move || value;
value = 2;
assert_eq!(read(), 1);
assert_eq!(value, 2);
assert_eq!(make_reader()(), 7);
}
This program works in editions 2018, 2021 and 2024. The closure does not
borrow the original integer. Removing move from make_reader would try
to return a closure borrowing a local that is about to disappear.
Without move, a closure can borrow a Copy value even when its body copies
that value. Do not confuse copying inside the body with copying at capture.
Likewise, move does not automatically mean FnOnce-only: the reader above
implements Fn because calling it does not consume its stored value.
Owned fields can drop at different times
struct Noisy(&'static str);
impl Noisy {
fn ping(&self) { println!("ping {}", self.0); }
}
impl Drop for Noisy {
fn drop(&mut self) { println!("drop {}", self.0); }
}
fn main() {
let pair = (Noisy("first"), Noisy("second"));
{
let call = move || pair.0.ping();
call();
}
println!("closure gone");
}
The output depends on the edition:
| Edition 2021 or 2024 | Edition 2018 |
|---|---|
ping first |
ping first |
drop first |
drop first |
closure gone |
drop second |
drop second |
closure gone |
In 2018, the closure owns the entire tuple. In newer editions, it owns only
pair.0; the second field remains in the enclosing scope. Here the closure
survives its call and drops at the inner closing brace. Other closures may
consume captured values during a call, so this is not a universal rule that
every captured value lives until closure destruction.
Destructors can release resources or perform other observable work. During
an edition migration, review those timing changes. In this example, adding
let _ = &pair; inside the move closure forces whole-tuple capture and
restores the old output. The edition guide
explains migration assistance from cargo fix --edition.
A wildcard is different from taking a reference
In edition 2021 and later, this program does not capture text:
fn main() {
let text = String::from("hello");
let call = move || { let _ = text; };
drop(text);
call();
}
The wildcard does not read or bind this place. Replacing text with &text
in the closure takes a reference to it and therefore requires capture. In
this move closure, that captures the String by value, so the later
drop(text) fails. Without move, the reference expression instead causes
a shared borrow. An expression such as text.len() still reads text even
if its result is assigned to _.
Capture precision has limits
Captures are not always the smallest field visible in the source. Reading a whole struct as well as one field can require whole-struct capture. Arrays and slices do not have the same partial-capture support as struct fields. Packed fields, raw pointers, unions, references and smart-pointer dereferencing also have special rules.
In particular, shared-reference truncation can follow nested references;
it does not simply mean "always capture the first reference variable."
A move closure using a field through &mut T captures the reference rather
than moving that field out. The closure reference
specifies these cases, including the special treatment of Box.
Thread-safety traits depend on the actual captures and their modes. For example, a shared borrow requires its referent to be Sync for the closure to be Send. Capturing an owned Send field can allow a closure to cross a thread boundary even when another, uncaptured field cannot. Whole-wrapper trait implementations can also make a change in capture granularity matter; do not assume an edition change can only improve trait compatibility.
When a closure unexpectedly blocks access or fails a trait bound, identify
the captured part and its capture mode first. If an apparently redundant
let _ = &value; is present, check whether it deliberately preserves whole
capture before removing it.