🎓 A1Learner
🔍

Rust Disjoint Captures

Disjoint captures let a closure capture only the places it touches. A place is one field or one tuple item of a value.

One field is not the whole struct

Form is a struct with two String fields, name and city. This closure touches only form.city, so while it lives, form.name stays free — the program reads it between defining the closure and calling it:

let mut form = Form { name: String::from("Sam"), city: String::from("Seattle") };
let mut fix_city = || form.city.push_str(", WA");
println!("name = {}", form.name);
fix_city();
println!("city = {}", form.city);

Tuples split the same way

A closure holding pair.0 hostage leaves pair.1 completely alone:

let mut pair = (String::from("a"), String::from("b"));
let mut fix0 = || pair.0.push('!');
println!("other = {}", pair.1);
fix0();
println!("fixed = {}", pair.0);

Arrays refuse to split

An index is a run-time value, so the capture cannot be precise. This closure takes the whole array as its &mut borrow. Reading arr[1] while it lives is refused:

let mut arr = [String::from("a"), String::from("b")];
let mut fix0 = || arr[0].push('!');
println!("{}", arr[1]); // error E0502: arr is borrowed as mutable, whole

When a split is refused, borrow the field first

Inside a method this is the everyday pattern. Iterating self.set while a closure pushes into self.vec looks like one borrow too many. The fix is to borrow the field into a local first. Then the closure captures the local instead of self:

let vec = &mut self.vec;
self.set.iter().for_each(|&n| vec.push(n));

Example

struct Page {
    heading: String,
    text: String,
}

fn main() {
    let mut page = Page {
        heading: String::from("Intro"),
        text: String::from("draft"),
    };

    // Two closures alive at once, each holding a different field.
    let mut fix_heading = || page.heading.push('!');
    let show_text = || println!("text = {}", page.text);
    fix_heading();
    show_text();
    println!("heading = {}", page.heading);

    // A field can move out while a closure keeps a sibling field.
    let pair = (String::from("left"), String::from("right"));
    let show_right = || println!("right = {}", pair.1);
    let left = pair.0;
    show_right();
    println!("left = {left}");
}

Key points

  • A capture names a place: a variable, a field of it, or a field inside a field.
  • The compiler follows the exact path your body uses
  • Two closures may hold disjoint places of one value at once: one may read form.name while another mutates form.city
  • A field captured by value drops when the closure drops.
  • Its sibling fields drop later, together with the original value
  • Splitting stops at a reference.
  • A closure reaching through an & or an Rc captures the reference itself, not the pieces behind it.
  • Box is the one exception, treated as part of the value
  • Arrays and slices never split.
  • Using arr[0] captures the whole array.
  • An index is only worked out at run time
  • The body may also use the whole variable somewhere — printing it, or passing it on.
  • Then the whole variable is captured.
  • The capture uses the strongest strength in the body
  • Edition 2021 introduced this finer capture.
  • Disjoint captures hold only the touched fields or items, not the whole value.

In this example, two closures are alive at the same time over one Page. fix_heading captures page.heading by &mut borrow. show_text captures page.text by shared borrow. The fields are disjoint, so the borrows never overlap: the text prints as draft, and afterwards the heading reads Intro!. Below, show_right captures only pair.1, so pair.0 is still free to move into left. The closure prints right = right, and main prints its own left = left. Before edition 2021, both halves of this program failed: a closure took the whole variable or nothing.

Note

The older fix still works in every edition.

Bind the field to a local name first (let vec = &mut self.vec).

Then let the closure capture the local.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

In a 2021 crate, one live closure mutates page.heading while another reads page.text. Why does this compile?