Split Borrowing

Borrow different parts of a struct simultaneously

advanced
borrowingmutabilitystructs
šŸŽ® Interactive Playground

What is Split Borrowing?

Split borrowing is an advanced Rust pattern that allows you to borrow different fields of a struct simultaneously, even when some borrows are mutable and others are immutable. This pattern leverages Rust's granular borrow checking at the field level.

The Problem

When working with structs, you might need to access multiple fields simultaneously. Without split borrowing, you'd be limited by Rust's borrowing rules at the struct level.

Example Code

struct Player {
    name: String,
    health: u32,
    position: (f32, f32),
    inventory: Vec<String>,
}

impl Player {
    // āŒ This doesn't work - can't have multiple mutable borrows
    fn update_bad(&mut self) {
        // This borrows the entire self mutably
        self.health -= 10;
        // Error: can't borrow self.position mutably here
        // self.update_position();
    }

    // āœ… This works - split borrowing at field level
    fn update_good(&mut self) {
        let health = &mut self.health;
        let position = &mut self.position;
        let name = &self.name; // immutable borrow is fine

        *health -= 10;
        position.0 += 1.0;
        println!("{} moved to {:?}", name, position);
    }
}

// Example with functions
fn process_player_data(health: &mut u32, position: &mut (f32, f32), name: &str) {
    *health -= 5;
    position.0 += 2.0;
    println!("{} is at {:?} with {} health", name, position, health);
}

fn main() {
    let mut player = Player {
        name: String::from("Hero"),
        health: 100,
        position: (0.0, 0.0),
        inventory: vec![],
    };

    // Split borrowing in action
    process_player_data(
        &mut player.health,
        &mut player.position,
        &player.name
    );

    println!("Final health: {}", player.health);
}

Why Split Borrowing Works

Rust's borrow checker operates at the field level within a function. When you explicitly borrow individual fields:

  1. No Overlap: Each field is independently borrowed
  2. Different Memory Locations: Fields occupy different memory locations
  3. Safe Aliasing: The compiler can prove there's no aliasing

When to Use

āœ… Use split borrowing when:

  • You need to modify multiple fields of a struct simultaneously
  • You want to pass different fields to helper functions
  • You're working with large structs and want to avoid moving the entire struct
  • You need immutable access to some fields while mutating others

āŒ Avoid split borrowing when:

  • A simple method call would suffice
  • You're accessing fields that have logical dependencies
  • The pattern makes code less readable

āš ļø Anti-patterns

āš ļø Mistake #1: Over-complicating Simple Operations

// āŒ Bad: Unnecessary split borrowing
fn set_health_bad(player: &mut Player, new_health: u32) {
    let health = &mut player.health;
    *health = new_health;
}

// āœ… Good: Direct access is clearer
fn set_health_good(player: &mut Player, new_health: u32) {
    player.health = new_health;
}

āš ļø Mistake #2: Fighting the Borrow Checker

// āŒ Bad: Trying to split borrow when there's logical coupling
struct BankAccount {
    balance: f64,
    transaction_log: Vec<String>,
}

// This is a code smell - these fields are logically related
fn process_transaction_bad(
    balance: &mut f64,
    log: &mut Vec<String>,
    amount: f64
) {
    *balance += amount;
    log.push(format!("Added {}", amount));
}

// āœ… Good: Keep logically coupled operations together
impl BankAccount {
    fn deposit(&mut self, amount: f64) {
        self.balance += amount;
        self.transaction_log.push(format!("Deposited {}", amount));
    }
}

Advanced Example: Game State Management

struct GameState {
    score: u32,
    time_remaining: f32,
    player_position: (f32, f32),
    enemy_positions: Vec<(f32, f32)>,
}

impl GameState {
    fn update(&mut self, delta_time: f32) {
        // Split borrowing enables parallel-looking code
        update_timer(&mut self.time_remaining, delta_time);
        update_player(&mut self.player_position, &mut self.score);
        update_enemies(&mut self.enemy_positions, &self.player_position);
    }
}

fn update_timer(time: &mut f32, delta: f32) {
    *time -= delta;
}

fn update_player(position: &mut (f32, f32), score: &mut u32) {
    position.0 += 0.1;
    *score += 1;
}

fn update_enemies(enemies: &mut Vec<(f32, f32)>, player_pos: &(f32, f32)) {
    for enemy in enemies.iter_mut() {
        // Move enemies toward player
        if enemy.0 < player_pos.0 { enemy.0 += 0.05; }
        if enemy.1 < player_pos.1 { enemy.1 += 0.05; }
    }
}

Real-World Usage

šŸ¦€ Bevy Engine

Bevy's ECS system heavily relies on split borrowing to allow systems to access different components simultaneously.

View on GitHub

šŸ¦€ Tokio

Tokio uses split borrowing for IO operations, allowing read and write halves of sockets to be used independently.

View on GitHub

Performance Considerations

Split borrowing has zero runtime cost - it's purely a compile-time feature. The borrow checker ensures safety without any runtime overhead.

// These compile to identical machine code
fn method1(player: &mut Player) {
    player.health -= 10;
    player.position.0 += 1.0;
}

fn method2(player: &mut Player) {
    let health = &mut player.health;
    let position = &mut player.position;
    *health -= 10;
    position.0 += 1.0;
}

Exercises

Exercise 1: Basic Split Borrowing

Implement a Rectangle struct with width and height fields. Write a function that can simultaneously modify both dimensions and calculate the area.

Hint: Think about which borrows need to be mutable and which can be immutable.

Exercise 2: Game Character

Create a Character struct with stats (health, mana, stamina) and equipment (weapon, armor). Implement a function that uses different equipment while updating stats.

Hint: Consider which operations need to happen simultaneously.

Exercise 3: Advanced Challenge

Implement a thread-safe data structure where different threads can access different fields simultaneously using Arc> for each field instead of the whole struct.

Hint: This combines split borrowing with concurrent programming patterns.

šŸŽ® Try it Yourself

šŸŽ®

Split Borrowing - Playground

Run this code in the official Rust Playground