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:
- At any given time, you can have either one mutable reference or any number of immutable references.
- 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
tos
. - We mutate the data through
r
. - We do not use
s
directly whiler
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 tos
, it has exclusive access tos
. - Attempting to use
s
directly (s.push_str(" all")
) whiler
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.