mirror of
https://github.com/leonardomso/rust-skills.git
synced 2026-09-14 19:33:21 +08:00
3.5 KiB
3.5 KiB
closure-disjoint-capture
Capture only what you use; lean on edition-2021 disjoint closure captures
Why It Matters
Before the 2021 edition, a closure captured entire variables — using config.threshold pulled in the whole config struct, preventing other code from using any other field of config concurrently. Since Rust 2021, closures capture individual fields (config.threshold only), so sibling fields remain independently accessible. Take advantage of this: write closures that reference only the specific fields or values they need, and add move only when ownership is genuinely required. When you do need to move a single field, bind it to a local first so the rest of the struct stays usable.
Bad
struct Config {
threshold: i32,
label: String,
}
fn demo_bad() {
let config = Config { threshold: 10, label: String::from("demo") };
// In pre-2021 editions the whole `config` is captured, blocking access
// to `config.label` below. In 2021 this compiles, but the pattern of
// capturing the whole struct via `move` is the real footgun:
let threshold = config.threshold; // copy out the field first
let check = move || threshold > 0; // now `config` is NOT fully moved
// If instead you wrote: let check = move || config.threshold > 0;
// `config` would be moved in, making `config.label` inaccessible afterwards.
// Demonstrate the problematic pattern (commented out to allow compilation):
// let check2 = move || config.threshold > 0;
// println!("{}", config.label); // error: use of moved value
println!("label still accessible: {}", config.label);
assert!(check());
}
Good
struct Config {
threshold: i32,
label: String,
}
fn demo_good() {
let config = Config { threshold: 10, label: String::from("active") };
// Edition 2021: the closure captures only `config.threshold` (a Copy field).
// `config.label` is NOT captured, so it remains accessible.
let check = || config.threshold > 0;
// Both are usable simultaneously.
println!("label: {}", config.label); // fine — not captured by `check`
assert!(check());
}
// When you need `move` for one field, bind it first.
fn make_checker(config: Config) -> (impl Fn() -> bool, String) {
// Bind the field to a local, then move only that local into the closure.
let threshold = config.threshold;
let checker = move || threshold > 0; // moves `threshold` (i32, Copy), not `config`
// `config.label` is still available here.
(checker, config.label)
}
fn demo_bind_first() {
let cfg = Config { threshold: 5, label: String::from("info") };
let (check, label) = make_checker(cfg);
println!("label returned: {label}");
assert!(check());
}
Key Points
- Edition 2021 rule: closures capture the minimal path used —
foo.barrather thanfoo. This reduces spurious borrow conflicts. movecaptures the whole named place. Writingmove || self.fieldinside a method moves*self, not justself.field. Bind to a local to narrow the capture.- Copy types (integers, booleans) are copied into the closure rather than moved, so the original remains valid even with
move. - Borrow by reference first: only escalate to
movewhen the closure must outlive the scope (see closure-move-capture).
See Also
- own-borrow-over-clone - prefer borrowing over cloning
- closure-move-capture - when to use
moveand how to clone selectively