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).