6.3 Borrowing and References

Borrowing in Rust allows you to access data without taking ownership. This is achieved through references.

6.3.1 References in Rust vs. Pointers in C

Rust References

  • Immutable References (&T): Read-only access.
  • Mutable References (&mut T): Read and write access.
  • Non-nullable: Rust references cannot be null.
  • Guaranteed Validity: References are guaranteed to point to valid data.
  • Automatically Dereferenced: Accessing the value doesn't require explicit dereferencing.

C Pointers

  • Nullable: Can be null.
  • Explicit Dereferencing: Require explicit dereferencing (*ptr).
  • No Enforced Mutability Rules: Mutability is not enforced.
  • Possible Invalid Pointers: May point to invalid or uninitialized memory.

Example

Rust Code:

fn main() {
    let x = 10;
    let y = &x; // Immutable reference
    println!("y points to {}", y);
}

C Code:

#include <stdio.h>

int main() {
    int x = 10;
    int *y = &x; // Pointer to x
    printf("y points to %d\n", *y);
    return 0;
}

6.3.2 Borrowing Rules

Rust enforces strict borrowing rules to ensure safety:

  1. At any given time, you can have either one mutable reference or any number of immutable references.
  2. References must always be valid.

Single Mutable Reference

Here's an example that demonstrates the correct use of a mutable reference:

fn main() {
    let mut s = String::from("hello");
    let r = &mut s;         // Mutable reference to s
    r.push_str(" world");
    println!("{}", r);
}

In this code:

  • We create a mutable reference r to s.
  • We mutate the data through r.
  • We do not use s directly while r is active.
  • This adheres to Rust's borrowing rules and compiles successfully.

Invalid Code: Mutable Reference and Use of Original Variable

Consider the following code:

fn main() {
    let mut s = String::from("hello");
    let r = &mut s;         // Mutable reference to s
    r.push_str(" world");

    s.push_str(" all");     // Attempt to use s while r is still in scope

    println!("{}", r);
    println!("{}", s);
}

This code does not compile because it violates Rust's borrowing rules.

Compiler Error:

error[E0503]: cannot use `s` because it was mutably borrowed
 --> src/main.rs:6:5
  |
3 |     let r = &mut s;         // Mutable reference to s
  |             ------ borrow of `s` occurs here
...
6 |     s.push_str(" all");     // Attempt to use s while r is still in scope
  |     ^^^^^^^^^^^^^^^^^^ use of borrowed `s`
7 |     
8 |     println!("{}", r);
  |                    - borrow later used here

Explanation:

  • When r is created as a mutable reference to s, it has exclusive access to s.
  • Attempting to use s directly (s.push_str(" all")) while r is still active violates the rule that you cannot have other references to a variable while a mutable reference exists.
  • The compiler prevents this to ensure memory safety and avoid data races.

How to Fix the Code:

  • Option 1: Limit the scope of the mutable reference:

    fn main() {
        let mut s = String::from("hello");
        {
            let r = &mut s;
            r.push_str(" world");
            println!("{}", r);
        } // r goes out of scope here
    
        s.push_str(" all");
        println!("{}", s);
    }
  • Option 2: Perform all mutations through the mutable reference:

    fn main() {
        let mut s = String::from("hello");
        let r = &mut s;
        r.push_str(" world");
        r.push_str(" all");
        println!("{}", r);
    }

By adjusting the code to comply with Rust's borrowing rules, we ensure that our program is both safe and functional.

6.3.3 Why These Rules?

These rules prevent data races and ensure memory safety without a garbage collector. By enforcing them at compile time, Rust eliminates entire classes of runtime errors common in C.

The borrow checker analyzes your code to track ownership and borrowing, ensuring that references are used safely according to the borrowing rules. It prevents you from having multiple mutable references to the same data, which could lead to data races, especially in concurrent contexts.

Comparison with C

In C, nothing prevents you from having multiple pointers to the same data, leading to potential undefined behavior.

#include <stdio.h>
#include <string.h>

int main() {
    char s[6] = "hello";
    char *p1 = s;
    char *p2 = s;
    strcpy(p1, "world");
    printf("%s\n", p2); // Outputs: world
    return 0;
}

In C, modifying data through one pointer affects all other pointers to that data. Rust prevents this when mutable references are involved.