| title | slug |
|---|---|
Borrowing |
borrowing |
💭 When we discussed Ownership we mentioned that, "each piece of data has a single owner, and data is only scoped to its owner (💯 if it is not borrowed)."
In a general sense, borrowing is taking something with the promise of returning it. In Rust, borrowing is the act of creating a reference (via & or &mut) to a value without copying the data or taking ownership of it. This is quite similar to the concept of "passing by reference" in other languages.
fn main() {
let a = 3;
let b = &a; // 💡 Referencing
let c = *b; // 💡 Dereferencing
println!("{a} {b} {c}"); // 3 3 3
}
// `a` has the ownership of 3. `b` is a reference to the data of `a`.
// `c` is a separate variable/ separate ownership, created by copying the data of `a`.
// ⭐️ `let c = *b;` is similar to `let c = a;`, we just use dereferenced reference to get the data.
// 💯 `let c = *b;` is only possible when `a` is a Copy type. If `a` is not Copy type, we will get a compiler error.In Rust, a reference cannot outlive the value it borrows. This kind of reference is called a "dangling reference" which occurs when a program tries to access memory that has already been deallocated.
fn main() {
let a: &f64;
{
let b = 3.14159; // 💡b owns the data and b is scoped inside the { } block
a = &b;
} // 💡b is dropped here along with its allocated data; Rust doesn't allow us to refer to data after it is dropped
println!("{a}"); // ❌ Compile-time error: "b does not live long enough"
}The Rust Borrow Checker identifies these errors at compile time.
5 | let b = 3.14159;
| -- binding `b` declared here
6 | a = &b;
| ^^ borrowed value does not live long enough
7 | }
| - `b` dropped here while still borrowed
8 |
9 | println!("{a}");
| - borrow later used hereReferences are pointers that are either a thin pointer that points to a Sized type or a fat pointer which points to an Unsized type.
use std::mem::size_of_val;
fn main() {
let a = 3; // 💡 3 store on the stack
let b: [i32; 3] = [1, 2, 3]; // 💡 [1, 2, 3] store inline on the stack
let c = &a; // 💡 c is a thin pointer (&i32) - [ptr]
let d = &b; // 💡 d is also a thin pointer (&[i32; 3]) - [ptr]
let e = &b[1..]; // 💡 [2, 3]; e is a fat pointer (&[i32]) - [ptr, len]
println!("\nSize of actual data: a: {} bytes, b: {} bytes",
size_of_val(&a), size_of_val(&b) // 4 and 12
);
println!("Size of the references: c: {} bytes, d: {} bytes, e: {} bytes",
size_of_val(&c), size_of_val(&d), size_of_val(&e) // 8 , 8, 16
);
}💡 An array of Copy types stores its data inline (store elements contiguously) wherever it is declared. It lives on the stack as a local variable, in the binary if it is
static, or on the heap if it is wrapped in aBoxorVec.STACK [ 1, 2, 3 ] // let b: [i32; 3] = [1, 2, 3]; [ ptr ] // let d = &b; 💡 pointer points to 0th index element of the array + len(3) is baked into the Type (Thin Ptr) [ ptr, len ] // let e = &b[1..]; 💡 pointer points to 1st index element of the array (Fat Ptr)💡 An array of non-Copy types also stores its elements inline and contiguously wherever it is declared. However, each element acts as a "header" that contains a pointer to the actual data stored on the heap. Example:
[String; 3]STACK HEAP [ String, String, String ] -> "A", "B", "C" // if `let b = [String::from("A"), String::from("B"), String::from("C")];` 3x ( ptr, len, cap) (3 allocations) [ ptr ] // let d = &b; 💡 pointer points to 0th String Header (Thin Ptr) [ ptr, len ] // let e = &b[1..]; 💡 pointer points to 1st String Header (Fat Ptr)
References are immutable by default (called shared references) but can be made mutable with the mut keyword.
- Shared references/
&T: Copy types and only copy the pointer (thin or fat), not actual data. - Mutable references:
&mut T: Non-copy types but reborrowing is allowed (A temporary reference to the same data with a shorter lifetime)
Because Rust is built for memory safety, we are not allowed to create a mutable reference or mutate data while having any active shared reference. Additionally, we are not allowed to have multiple active mutable references to the same piece of data at once. Rust borrow checker identifies these errors at compile time to prevent data races and undefined behavior.
-
The
println!()calls with{}or{:?}create new shared references to the data.fn main() { let a = 128; let b = String::from("ABC"); println!("{a}"); // 💡 A shared reference println!("{b}"); // 💡 A shared reference }
-
The
&operator creates a new shared reference to the data.fn main() { let a = String::from("ABCDE"); println!("{a}"); // 💡 A shared reference borrow_and_print(&a); // 💡 A shared reference let b = &a; // 💡 A shared reference borrow_and_print(b); } fn borrow_and_print(a: &String) { println!("{a}"); }
A borrow doesn't necessarily last until the end of the curly braces { }. Instead, it lasts only until its last use.
-
The
&mutoperator creates a new mutable reference to the data. When passing a&mutto a function, the reference is scoped to that function call.fn main() { let mut a = String::from("AAA"); println!("{a}"); mutate_and_print(&mut a); // 💡 A mutable reference; AAA-BBB mutate_and_print(&mut a); // 💡 A mutable reference; AAA-BBB-BBB let b = &mut a; // 💡 A mutable reference; AAA-BBB-BBB-CCC while printing b.push_str("-CCC"); println!("{a}"); mutate_and_print(&mut a); // 💡 A mutable reference; AAA-BBB-BBB-CCC-BBB } fn mutate_and_print(a: &mut String) { a.push_str("-BBB"); println!("{a}"); } // Eventhough `let b = &mut a;` creates a new mutable reference, its last use only till the next line. So, the sahred reference creation via `println!()` is still valid and trigger no error.
-
Shared references are Copy types while mutable references are Move types. However, when we pass a mutable reference to a function or assign it to a new variable with an explicit type, Rust doesn't move the original reference. Instead, Rust creates a new temporary reference to the same data with a shorter lifetime. This is called "reborrowing" as it's a "borrow from a borrow". While this new reborrow is active, the original reference cannot be used.
fn main() { let mut a = 128; let b = &mut a; // 💡 First mutable reference { let c: &mut i32 = b; // 💡 Reborrowing; A temporary mutable reference *c += 1; // println!("{b}"); // cannot borrow `b` as immutable because it is also borrowed as mutable for `c` println!("{c}"); } *b += 1; println!("{a}"); }
- 💯 Check
Rc<T>,Arc<T>,Cell<T>,RefCell<T>types and interior mutability.