Rust - Borrowing
Rust - Borrowing and References
Section titled “Rust - Borrowing and References”In the chapter on Ownership, we learned that when a value is assigned to another variable or passed to a function, its ownership is moved. After the move, the original owner can no longer use the value. This ensures memory safety but can be inconvenient. Constantly passing ownership back and forth would be cumbersome.
This is where borrowing comes in. Borrowing allows you to create a reference to a value, giving you access to it without taking ownership.
The Problem: Ownership Transfer
Section titled “The Problem: Ownership Transfer”fn calculate_length(s: String) -> (String, usize) { let length = s.len(); (s, length) // Return ownership of the String and the length}
fn main() { let s1 = String::from("hello");
// We pass s1, and also get it back. let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);}This code works, but it’s clunky. We had to return the String from the function just so we could continue using it. This is a perfect use case for borrowing.
The Solution: Borrowing with References
Section titled “The Solution: Borrowing with References”Instead of passing the value itself (String), we can pass a reference to it (&String). A reference is like a pointer that is guaranteed to point to a valid value of a particular type. The ampersand & is the reference operator.
// The function now takes a reference to a String.fn calculate_length(s: &String) -> usize { s.len()} // `s` goes out of scope here, but because it does not own what it refers to, // the String is not dropped.
fn main() { let s1 = String::from("hello");
// We pass a reference to s1. Ownership is not moved. let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len); // s1 is still valid here!}Output
Section titled “Output”The length of 'hello' is 5.The Two Types of References
Section titled “The Two Types of References”Just as variables are immutable by default, so are references. There are two kinds of references:
- Immutable Reference (
&T): Allows you to read the data but not modify it. - Mutable Reference (
&mut T): Allows you to both read and modify the data.
Example: Mutable References
Section titled “Example: Mutable References”To create a mutable reference, you use &mut. The original variable must also be declared as mut. The * operator is used to dereference the reference, allowing you to access the value it points to.
fn add_suffix(s: &mut String) { s.push_str(" World"); // Methods can be called directly on the reference}
fn increment(num: &mut i32) { *num += 1; // For primitive types, we must dereference with *}
fn main() { let mut my_string = String::from("Hello"); println!("Before: {}", my_string); add_suffix(&mut my_string); println!("After: {}", my_string);
let mut count = 5; println!("\nBefore: {}", count); increment(&mut count); println!("After: {}", count);}Output
Section titled “Output”Before: HelloAfter: Hello World
Before: 5After: 6The Rules of Borrowing
Section titled “The Rules of Borrowing”The Rust compiler enforces a crucial set of rules to prevent data races and other concurrency bugs at compile time. These rules apply to references within a given scope:
- Rule 1: You can have either one mutable reference OR any number of immutable references.
- Rule 2: References must always be valid (i.e., they cannot be dangling references that point to freed memory).
This means you cannot have a mutable reference while immutable ones exist. All the immutable readers must be gone before you can get a mutable writer.
Example: Violating the Rules
Section titled “Example: Violating the Rules”The following code will not compile because it violates Rule 1.
// This code will fail to compile!fn main() { let mut s = String::from("hello");
let r1 = &s; // an immutable borrow starts here let r2 = &s; // another immutable borrow is fine
// COMPILE ERROR: cannot borrow `s` as mutable because it is also borrowed as immutable let r3 = &mut s;
// The compiler knows that r1 and r2 could be used after the mutable borrow, which is unsafe. println!("Immutable borrows: {}, {}", r1, r2);}The compiler’s error message is very helpful here. It prevents a situation where data could change unexpectedly while something else is reading it.
Best Practices
Section titled “Best Practices”- Prefer immutable borrows: Use
&Twhenever you only need to read data. This is safer and allows for more flexible code. - Keep mutable borrows short: The scope of a mutable borrow should be as small as possible to allow other parts of the code to access the data.
- Use slices for collections: When writing functions that operate on arrays or vectors, accept a slice (
&[T]or&mut [T]) instead of a direct reference to the collection (&Vec<T>). This makes your function more generic and reusable.