How to borrow part of a struct
You're building a dashboard. The Metrics struct holds cpu_usage, memory_mb, and disk_io. You want to pass cpu_usage to a gauge widget and memory_mb to a bar chart. You try to hand the widgets references to the fields. The compiler complains about lifetimes. You try to clone the fields, but that allocates memory you don't need. You feel like Rust is forcing you to pass the entire Metrics struct to every widget, even though each widget only cares about one number.
Rust doesn't let you rip a field out of a struct and hold it independently. Every reference to a field is anchored to the struct. If you have a reference to player.score, you implicitly have a borrow of player. The compiler tracks this anchor. This design prevents dangling pointers and keeps memory safe. You cannot have a reference to a field floating around after the struct has been dropped. The borrow checker enforces this by tying the lifetime of the field reference to the lifetime of the struct reference.
The atomic unit rule
Rust treats a struct as a single unit of ownership. You cannot borrow field A and field B as two independent, floating references. To get a reference to a field, you must first borrow the struct. The field reference is just a window into the struct. If the struct goes away, the window breaks.
Think of a struct like a house. You can give someone a key to the house, and they can walk to the kitchen. You can't give someone a key to just the kitchen that works if the house is demolished. The kitchen exists only because the house exists. Rust enforces this by saying: to get a reference to a field, you must have a reference to the struct. The field reference is a view into the struct. If the struct drops, the view vanishes.
This rule applies to both immutable and mutable borrows. You can borrow the struct immutably and read any field. You can borrow the struct mutably and write to any field. You cannot borrow the struct immutably and try to mutate a field. You cannot borrow two fields mutably at the same time. The struct is the boundary.
Minimal example
The standard pattern is to borrow the whole struct and access the field through that reference. This is safe and idiomatic. The compiler tracks the lifetime of the struct reference and ensures any field references derived from it remain valid.
struct Player {
name: String,
score: i32,
}
/// Prints the score from a player reference.
fn print_score(player: &Player) {
// Access the field through the struct reference.
// This creates a reference to the field, derived from the struct borrow.
// The lifetime of score_ref is tied to the lifetime of player.
let score_ref: &i32 = &player.score;
println!("Score: {}", score_ref);
}
fn main() {
let player = Player {
name: "Alice".to_string(),
score: 100,
};
// Pass the whole struct reference.
// The function extracts the field inside.
print_score(&player);
}
The code works because print_score takes &Player. Inside the function, &player.score creates a reference to the score field. This reference lives as long as player is borrowed. When print_score returns, the borrow ends, and the field reference is dropped. No memory is leaked. No dangling pointers exist.
What happens under the hood
At compile time, the borrow checker sees &player.score and records a borrow of player. It notes that the borrow is immutable. It checks that no other mutable borrows of player exist. If the checks pass, the code compiles. The lifetime of &player.score is unified with the lifetime of &player. You cannot extend the field reference beyond the struct reference.
At runtime, there is no overhead. A reference to a struct is a pointer to the start of the struct in memory. A reference to a field is a pointer to the start of the struct plus the offset of the field. The compiler calculates the offset once. Accessing the field is just pointer arithmetic. No allocation happens. No cloning happens. It is zero-cost.
This efficiency is why Rust encourages borrowing the struct and projecting to fields. You get safety without paying for it. The compiler does the work upfront. The runtime just follows the pointer.
Realistic example
In real code, you often want to expose parts of a struct without exposing the whole struct. This is common in public APIs where you want to hide implementation details. You define methods on the struct that return references to specific fields. This encapsulates the borrowing logic and makes the API clearer.
struct Config {
host: String,
port: u16,
timeout: u32,
}
impl Config {
/// Returns a reference to the host string.
fn host(&self) -> &str {
&self.host
}
/// Returns a reference to the port number.
fn port(&self) -> &u16 {
&self.port
}
}
/// Connects to a server using host and port references.
fn connect(host: &str, port: &u16) {
println!("Connecting to {} on {}", host, port);
}
fn main() {
let config = Config {
host: "db.example.com".to_string(),
port: 5432,
timeout: 30,
};
// Use accessor methods to get field references.
// This keeps the struct encapsulated.
connect(config.host(), config.port());
}
Rust convention: if the struct and the code are in the same module, reach for direct field access. Use accessor methods when the struct is public and you want to hide the fields, or when you need to compute a value on the fly. Accessor methods give you flexibility. You can change the internal representation later without breaking the API. If you store host as a String today but switch to a Cow<str> tomorrow, the accessor method signature stays the same. Direct field access would force callers to update their code.
Why can't I split a struct like a slice?
You might notice that slices have a split_at_mut method. You can split a slice into two mutable references. Structs don't have this. You cannot split a struct into two mutable references to different fields.
The language design treats structs as atomic units for borrowing. Slices are sequences with a known layout. The compiler can prove that two parts of a slice don't overlap. Structs also have a known layout, but the borrow checker doesn't expose a general split for structs. This keeps the rules simpler. If you need to mutate parts of a struct independently, you have options. You can use interior mutability with Cell or RefCell. You can restructure your data into separate structs. You can borrow the whole struct mutably and update fields in place.
Don't try to force a split where the language doesn't support it. The borrow checker will stop you. Work with the atomic model. Borrow the struct. Update the fields. Or use interior mutability if you truly need independent access.
Pitfalls and compiler errors
The most common mistake is trying to hold an immutable reference to a field while mutating another field. This violates Rust's aliasing rules. You cannot have a mutable borrow and an immutable borrow at the same time.
struct Data {
a: i32,
b: i32,
}
fn bad_usage(data: &mut Data) {
// Borrow field a immutably.
let a_ref = &data.a;
// Try to mutate field b.
// This fails because data is already borrowed immutably via a_ref.
data.b += 1;
println!("{}", a_ref);
}
The compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable). The error tells you that data is borrowed as immutable because of a_ref. You cannot mutate data.b while a_ref is alive. The fix is to drop the immutable borrow before mutating. Or restructure the code to avoid holding the reference across the mutation.
Another pitfall is trying to return two mutable references to fields from a function. You cannot return (&mut i32, &mut i32) from a function that takes &mut self. This would create two mutable borrows of the same struct. The compiler rejects this with an error about multiple mutable borrows. If you need to mutate multiple fields, mutate them through the single mutable struct reference. Or use a helper method that takes &mut self and performs the updates internally.
Don't try to split mutable borrows. The compiler will stop you, and for good reason. Splitting mutable borrows can lead to data races and undefined behavior. Trust the borrow checker. It protects you from subtle bugs.
Decision: when to use this vs alternatives
Use direct field access when the code is in the same module as the struct and you need to read or write multiple fields. This is the standard Rust pattern for internal logic. It is concise and efficient. Use accessor methods when the struct is public and you want to hide the fields, or when you need to compute a value on the fly. Accessor methods provide a stable API and allow internal changes without breaking callers. Use a mutable struct reference when you need to update multiple fields at once. You cannot split a mutable borrow into multiple independent mutable references. Use a helper method that takes &mut self to perform the update internally. Use interior mutability with Cell or RefCell when you need to mutate parts of a struct independently while holding an immutable reference to the struct. This is rare and usually indicates a design choice for shared state.
Trust the borrow checker. If it won't let you split the borrow, there's a reason. The atomic model keeps your code safe and predictable.