6.3 Borrowing and References
In Rust, borrowing grants access to a value without transferring ownership. This is done with references, which come in two forms: immutable (&T
) and mutable (&mut T
). While references in Rust resemble raw pointers in C, they are subject to strict safety guarantees preventing common memory errors. In contrast, C pointers can be arbitrarily manipulated, sometimes leading to undefined behavior. Because Rust checks references thoroughly, they are often called managed pointers.
6.3.1 References in Rust vs. Pointers in C
Rust References
- Immutable (
&T
): Read-only access. - Mutable (
&mut T
): Read-write access. - Non-nullable: Cannot be null.
- Always valid: Must point to valid data.
- Automatic dereferencing: Typically do not require explicit
*
to read values.
C Pointers
- Nullable: May be null.
- Explicit dereferencing: Must use
*ptr
to access pointed data. - No enforced mutability rules: C does not distinguish between mutable and immutable pointers.
- Can be invalid: Nothing stops a pointer from referring to freed memory.
Example
fn main() { let x = 10; let y = &x; // Immutable reference println!("y points to {}", y); }
#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’s borrowing rules are:
- You can have either one mutable reference or any number of immutable references at the same time.
- References must always be valid (no dangling pointers).
Immutable References
Multiple immutable references are permitted, whether or not the underlying variable is mut
:
fn main() { let s1 = String::from("hello"); let r1 = &s1; let r2 = &s1; println!("{}, {}", r1, r2); let mut s2 = String::from("hello"); let r3 = &s2; let r4 = &s2; println!("{}, {}", r3, r4); }
Having multiple references to the same data is sometimes called aliasing.
Single Mutable Reference
Only one mutable reference is allowed at any time:
fn main() { let mut s = String::from("hello"); let r = &mut s; // Mutable reference r.push_str(" world"); println!("{}", r); }
Why Only One?
This rule ensures no other references can read or write the same data concurrently, preventing data races even in single-threaded code.
Note that you can only create a mutable reference if the data is declared mut
. The following code will not compile:
fn main() { let s = String::from("hello"); let r = &mut s; // Error: s is not mutable }
In the same way, an immutable variable cannot be passed to a function that requires a mutable reference.
Invalid Code: Mixing a Mutable Reference and Owner Usage
fn main() { let mut s = String::from("hello"); let r = &mut s; r.push_str(" world"); s.push_str(" all"); // Error: s is still mutably borrowed by r println!("{}", r); }
Here, s
remains mutably borrowed by r
until r
goes out of scope, so direct usage of s
is forbidden during that time.
Possible Fixes:
-
Restrict the mutable reference’s scope:
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); }
-
Apply all modifications 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); }
6.3.3 Why These Rules?
They prevent data races and guarantee memory safety without a garbage collector. The compiler enforces them at compile time, ensuring there is no risk of data corruption or undefined behavior.
Though these rules may seem stringent, especially in single-threaded situations, they substantially reduce programming errors. We will delve deeper into the rationale in the following section.
Comparison with C
In C, multiple pointers can easily refer to the same data and modify it independently, often leading to unpredictable results:
#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); // "world"
return 0;
}
Rust’s borrow checker eliminates these kinds of issues at compile time.