Advanced Lifetimes and Borrowing

Borrowing Across Struct Fields

The Rust borrow checker tracks field-level disjointness (also called split borrows) inside a function body: it permits borrowing struct_var.field_a mutably (&mut) and struct_var.field_b immutably (&) or mutably (&mut) at the exact same time, provided the borrows occur through direct field paths rather than through a method that borrows the entire &mut self.

How Whole-Struct Method Signatures Block Split Borrows

Because Rust checks every function body against called method signatures (not called method implementations), calling self.get_a_mut() where fn get_a_mut(&mut self) -> &mut A borrows all of *self for the lifetime of the returned &mut A—preventing any simultaneous access to self.b:

// ❌ does not compile
struct Workspace {
    source_files: Vec<String>,
    diagnostics: Vec<String>,
}

impl Workspace {
    fn diagnostics_mut(&mut self) -> &mut Vec<String> {
        &mut self.diagnostics
    }

    fn lint_all(&mut self) {
        let diags = self.diagnostics_mut();
        // Error E0502: `self.diagnostics_mut()` borrowed ALL of `*self` as mutable,
        // so `&self.source_files` cannot be borrowed at the same time!
        for file in &self.source_files {
            if file.is_empty() {
                diags.push(String::from("empty filename"));
            }
        }
    }
}

fn main() {}
error[E0502]: cannot borrow `self.source_files` as immutable because `*self` is also borrowed as mutable
  --> src/main.rs:16:21
   |
13 |         let diags = self.diagnostics_mut();
   |                     ---- mutable borrow occurs here
...
16 |         for file in &self.source_files {
   |                     ^^^^^^^^^^^^^^^^^^ immutable borrow occurs here
17 |             if file.is_empty() {
18 |                 diags.push(String::from("empty filename"));
   |                 ----- mutable borrow later used here
Whole-Struct Borrow via `self.diagnostics_mut(&mut self)`:
  +------------------------ &mut self (Entire Struct Locked) ------------------------+
  |  source_files: Vec<String> (Blocked!)   |   diagnostics: Vec<String> (Returned)  |
  +----------------------------------------------------------------------------------+

Disjoint Field Borrow (`&self.source_files` + `&mut self.diagnostics`):
  +-----------------------------------------+----------------------------------------+
  |  &self.source_files (Shared Read OK)    |   &mut self.diagnostics (Exclusive OK) |
  +-----------------------------------------+----------------------------------------+

Three Idiomatic Solutions for Disjoint Struct Borrows

1. Borrow Disjoint Fields Directly in the Method Body

Access &self.source_files and &mut self.diagnostics directly as field projections so the borrow checker sees their paths are disjoint.

2. Factor Sub-Structs or Free/Associated Functions

Group related fields into focused sub-structs (struct Diagnostics(Vec<String>)) or pass only the needed fields to an associated function (Self::check_file(&file, &mut self.diagnostics)).

3. Provide a View / Split-Borrow Method (split_mut)

For public APIs where fields are private, expose a method fn split_mut(&mut self) -> (&[String], &mut Vec<String>) that returns references to disjoint fields in a tuple or view struct:

pub struct Workspace {
    source_files: Vec<String>,
    diagnostics: Vec<String>,
}

impl Workspace {
    pub fn split_mut(&mut self) -> (&[String], &mut Vec<String>) {
        (&self.source_files, &mut self.diagnostics)
    }

    pub fn lint_all(&mut self) {
        let (files, diags) = self.split_mut();
        for (idx, file) in files.iter().enumerate() {
            if !file.ends_with(".rs") {
                diags.push(format!("file #{idx} ({file:?}) is not a .rs file"));
            }
        }
    }
}

fn main() {
    let mut ws = Workspace {
        source_files: vec![String::from("main.rs"), String::from("notes.txt")],
        diagnostics: Vec::new(),
    };

    ws.lint_all();
    println!("diagnostics: {:?}", ws.diagnostics);
}
diagnostics: ["file #1 (\"notes.txt\") is not a .rs file"]

Closure Disjoint Field Capture (Rust 2021 and 2024 Editions)

Closures automatically capture only the specific struct fields they use (|| println!("{}", w.source_files.len()) captures &w.source_files, leaving w.diagnostics free to be mutably borrowed outside the closure).