5/5
## Understanding Borrowing and References in Rust This lesson delves into Rust's powerful concepts of borrowing and references. Building upon the foundation of Rust's ownership system, we'll explore how borrowing allows for flexible data access without transferring ownership, thereby preventing common programming pitfalls. ## The Ownership Challenge: A Recap Previously, we encountered how Rust's ownership system works. Consider the following example: ```rust fn take(s: String) { println!("take {}", s); } fn main() { // Take ownership let s = String::from("rust"); take(s); // Ownership of 's' moves into the 'take' function // s is dropped after take(s) // This will not compile because 's' is no longer valid here: // println!("{}", s); } ``` In this scenario, when the `String` variable `s` (or any type that doesn't implement the `Copy` trait) is passed to the `take` function, its ownership is moved. The `take` function now owns the string data. Consequently, after the `take(s)` call, the variable `s` in the `main` function is no longer valid. If we try to use `s` again in `main` (e.g., by uncommenting `println!("{}", s);`), the Rust compiler will issue an error. This behavior, while ensuring memory safety, can be impractical if we need to use the data in the original scope after calling a function with it. ## Introducing Borrowing: A Solution to Ownership Transfer To address the impracticality of complete ownership transfer in every situation, Rust introduces the concept of **borrowing**. Borrowing allows you to temporarily use a value without taking ownership of it. The primary goal is to enable a function to access and use data, such as a string, while allowing the original owner to retain ownership and continue using that data after the function call. This principle applies to all data types that do not implement the `Copy` trait, such as `String`, `Vec<T>`, and other complex data structures. ## The Rules of Borrowing and References Borrowing is achieved by creating **references** to a value. * **What is Borrowing?** At its core, borrowing means temporarily using a value without taking ownership. * **How to Borrow?** You borrow a value by creating a reference to it. * **Effect of a Reference:** When a reference to data is created and passed to a function, the ownership of the original data *does not* move. The original owner retains control. Rust defines two main types of references, each with specific rules to ensure memory safety: ### 1. Immutable References (`&T`) Immutable references allow you to read data but not modify it. The key rule for immutable references is: * You can have **any number of immutable references** to a particular piece of data simultaneously. Consider this example: ```rust let s = String::from("rust"); let s1 = &s; // s1 is an immutable reference to s let s2 = &s; // s2 is another immutable reference to s let s3 = s2; // s3 is also an immutable reference to s (points to the same data as s2) // s1, s2, and s3 all provide read-only access to the original 's'. // 's' itself remains valid and owned by the current scope. println!("s: {}, s1: {}, s2: {}, s3: {}", s, s1, s2, s3); ``` Here, `s1`, `s2`, and `s3` are all immutable references pointing to the data owned by `s`. `s` remains the owner and is still valid. ### 2. Mutable References (`&mut T`) Mutable references allow you to both read *and* write (modify) the data they point to. For a mutable reference to be created, the original data must also be declared as mutable using the `mut` keyword. The crucial rule for mutable references is: * You can only have **one mutable reference** to a particular piece of data in a particular scope *at any given time*. This rule prevents data races at compile time. Let's look at an example: ```rust let mut s = String::from("rust"); // 's' must be declared as mutable let s1 = &mut s; // s1 is a mutable reference to s s1.push_str(" 🦀"); // s1 can be used to modify 's' // At this point, s1's borrow is active. // The following would cause a compile error if s1 is still considered "live" // before its last use: // let s2 = &mut s; // ERROR: cannot borrow `s` as mutable more than once at a time // println!("{}", s1); // If s1 were used here, s2 couldn't be created before this. println!("{}", s); // s has been modified ``` **Non-Lexical Lifetimes (NLL):** It's important to understand that a borrow's scope doesn't necessarily last for the entire lexical block it's defined in. Instead, a borrow lasts until its *last use*. This feature, known as Non-Lexical Lifetimes (NLL), allows for more flexible code. For instance, after a mutable reference is last used, you can create another mutable reference to the same data within the same lexical scope: ```rust let mut s = String::from("rust"); let s1 = &mut s; s1.push_str(" 🦀"); // Last use of s1's borrow // s1's borrow has ended because it's no longer used. // Therefore, we can create a new mutable reference s2. let s2 = &mut s; s2.push_str("🦀"); println!("{}", s); // s now contains "rust 🦀🦀" ``` ### 3. Mixing Immutable and Mutable References Rust enforces strict rules about combining immutable and mutable references to the same data: * You **cannot** have a mutable reference if any immutable references to the same data exist and are currently active. * Conversely, you **cannot** have any immutable references if a mutable reference to the same data exists and is active. Essentially, for a given piece of data in a particular scope, you can have: * Any number of immutable references (`&T`), OR * Exactly one mutable reference (`&mut T`). You cannot have both types simultaneously active. This prevents situations where data could be changed via a mutable reference while other parts of the code expect it to remain constant via immutable references. Consider this code, which will fail to compile: ```rust // This code will NOT compile // let mut s = String::from("rust"); // let s1 = &s; // Immutable borrow 1 // let s2 = &s; // Immutable borrow 2 // let s3 = &mut s; // ERROR: Cannot borrow 's' as mutable because it's already borrowed as immutable // println!("s1: {}", s1); // The use of s1 here makes its immutable borrow "live" // // when s3 is attempted. // s3.push_str("🦀"); ``` The error occurs because the immutable borrows (`s1` and `s2`) are considered active (especially if used later, like `println!("s1: {}", s1);`) when the attempt to create a mutable borrow (`s3`) is made. The compiler ensures that data cannot be mutated while immutable references to it might still be in use. ### 4. Reference Lifetimes and Preventing Dangling References A fundamental safety rule in Rust is: * A reference must **never outlive** the data it refers to. The data being referenced must live at least as long as any of its references. If data were to be dropped (deallocated) while references to it still existed, those references would become "dangling references"—pointers to invalid memory. This is a common source of bugs and security vulnerabilities in other languages. Rust's compiler, through its borrow checker, prevents this situation entirely. One way this could happen is if a reference points to data whose ownership is moved and then dropped in an inner scope: ```rust // This code will NOT compile // let s_outer = String::from("rust"); // let s1_ref = &s_outer; // s1_ref references s_outer // { // Inner scope // let s2_inner_owner = s_outer; // s_outer's ownership MOVES to s2_inner_owner. // // s_outer is now invalid in the outer scope. // } // s2_inner_owner goes out of scope here, and the String data it owns is dropped. // // ERROR: s1_ref now references dropped data. // // Compiler error might say: "borrowed value does not live long enough" // // or "s_outer does not live long enough" // println!("s1_ref: {}", s1_ref); ``` Here, `s_outer`'s data is dropped when `s2_inner_owner` goes out of scope. If `s1_ref` were allowed to be used after this, it would be a dangling reference. Another common scenario where dangling references could occur is when a function tries to return a reference to data that it owns, because that data will be dropped when the function ends: ```rust // This function will NOT compile // fn dangle(s: String) -> &String { // s is owned by this function // &s // Attempting to return a reference to s // } // s is dropped here as the function ends. The returned reference would be dangling. // fn main() { // let my_string = String::from("hello"); // // let reference_to_nothing = dangle(my_string); // This call would be problematic // } ``` The compiler will issue an error like "returns a reference to data owned by the current function," preventing the creation of a dangling reference. ## Applying Borrowing: Revisiting Our Initial Problem Let's return to the original problem where the `take` function consumed ownership of the string, making it unusable in `main` afterwards. We can solve this using borrowing. The original `take` function signature was: `fn take(s: String)` We can modify this function (or create a new one) to accept a reference instead: ```rust // Renamed for clarity, could also modify the original `take` fn borrow_string(s_ref: &String) { // Takes an immutable reference to a String println!("borrow {}", s_ref); // s_ref cannot be modified here because it's an immutable reference } fn main() { let original_s = String::from("rust"); // original_s owns the String data // Pass an immutable reference to original_s. // Ownership of original_s is NOT moved. borrow_string(&original_s); // This is now valid! original_s still owns the String and can be used. println!("{}", original_s); } ``` When this code is run, the output will be: ``` borrow rust rust ``` By changing `borrow_string` to accept `&String` (an immutable reference to a `String`) and calling it with `&original_s` (creating and passing an immutable reference), the ownership of `original_s` remains with the `main` function. Therefore, `original_s` is still valid and can be printed after the call to `borrow_string`. If we needed the function to modify the string, we would pass a mutable reference: ```rust fn modify_string(s_ref: &mut String) { // Takes a mutable reference s_ref.push_str(" is awesome!"); println!("modified in function: {}", s_ref); } fn main() { let mut modifiable_s = String::from("Rust"); // Must be mutable modify_string(&mut modifiable_s); // Pass a mutable reference println!("after function: {}", modifiable_s); // modifiable_s reflects the changes } ``` Output: ``` modified in function: Rust is awesome! after function: Rust is awesome! ``` ## Key Principles of Borrowing: A Summary To recap the core rules and benefits of borrowing in Rust: * Borrowing allows temporary access to a value via **references** without taking ownership. * Creating a reference **does not move ownership** of the data. * References can be **immutable (`&T`)**, allowing read-only access, or **mutable (`&mut T`)**, allowing read-write access. * For any given piece of data in a particular scope, you can have: * Any number of immutable references, OR * Exactly one mutable reference. You cannot have both types simultaneously active for the same data. * A reference must **never outlive** the data it points to. Rust's compiler enforces this rule to prevent dangling references. This system of ownership and borrowing, enforced at compile time, allows Rust to provide memory safety without needing a garbage collector, leading to efficient and reliable programs.
This lesson delves into Rust's powerful concepts of borrowing and references. Building upon the foundation of Rust's ownership system, we'll explore how borrowing allows for flexible data access without transferring ownership, thereby preventing common programming pitfalls.
Previously, we encountered how Rust's ownership system works. Consider the following example:
In this scenario, when the String
variable s
(or any type that doesn't implement the Copy
trait) is passed to the take
function, its ownership is moved. The take
function now owns the string data. Consequently, after the take(s)
call, the variable s
in the main
function is no longer valid. If we try to use s
again in main
(e.g., by uncommenting println!("{}", s);
), the Rust compiler will issue an error. This behavior, while ensuring memory safety, can be impractical if we need to use the data in the original scope after calling a function with it.
To address the impracticality of complete ownership transfer in every situation, Rust introduces the concept of borrowing. Borrowing allows you to temporarily use a value without taking ownership of it. The primary goal is to enable a function to access and use data, such as a string, while allowing the original owner to retain ownership and continue using that data after the function call. This principle applies to all data types that do not implement the Copy
trait, such as String
, Vec<T>
, and other complex data structures.
Borrowing is achieved by creating references to a value.
What is Borrowing? At its core, borrowing means temporarily using a value without taking ownership.
How to Borrow? You borrow a value by creating a reference to it.
Effect of a Reference: When a reference to data is created and passed to a function, the ownership of the original data does not move. The original owner retains control.
Rust defines two main types of references, each with specific rules to ensure memory safety:
&T
)Immutable references allow you to read data but not modify it. The key rule for immutable references is:
You can have any number of immutable references to a particular piece of data simultaneously.
Consider this example:
Here, s1
, s2
, and s3
are all immutable references pointing to the data owned by s
. s
remains the owner and is still valid.
&mut T
)Mutable references allow you to both read and write (modify) the data they point to. For a mutable reference to be created, the original data must also be declared as mutable using the mut
keyword.
The crucial rule for mutable references is:
You can only have one mutable reference to a particular piece of data in a particular scope at any given time. This rule prevents data races at compile time.
Let's look at an example:
Non-Lexical Lifetimes (NLL): It's important to understand that a borrow's scope doesn't necessarily last for the entire lexical block it's defined in. Instead, a borrow lasts until its last use. This feature, known as Non-Lexical Lifetimes (NLL), allows for more flexible code. For instance, after a mutable reference is last used, you can create another mutable reference to the same data within the same lexical scope:
Rust enforces strict rules about combining immutable and mutable references to the same data:
You cannot have a mutable reference if any immutable references to the same data exist and are currently active.
Conversely, you cannot have any immutable references if a mutable reference to the same data exists and is active.
Essentially, for a given piece of data in a particular scope, you can have:
Any number of immutable references (&T
), OR
Exactly one mutable reference (&mut T
).
You cannot have both types simultaneously active. This prevents situations where data could be changed via a mutable reference while other parts of the code expect it to remain constant via immutable references.
Consider this code, which will fail to compile:
The error occurs because the immutable borrows (s1
and s2
) are considered active (especially if used later, like println!("s1: {}", s1);
) when the attempt to create a mutable borrow (s3
) is made. The compiler ensures that data cannot be mutated while immutable references to it might still be in use.
A fundamental safety rule in Rust is:
A reference must never outlive the data it refers to. The data being referenced must live at least as long as any of its references.
If data were to be dropped (deallocated) while references to it still existed, those references would become "dangling references"—pointers to invalid memory. This is a common source of bugs and security vulnerabilities in other languages. Rust's compiler, through its borrow checker, prevents this situation entirely.
One way this could happen is if a reference points to data whose ownership is moved and then dropped in an inner scope:
Here, s_outer
's data is dropped when s2_inner_owner
goes out of scope. If s1_ref
were allowed to be used after this, it would be a dangling reference.
Another common scenario where dangling references could occur is when a function tries to return a reference to data that it owns, because that data will be dropped when the function ends:
The compiler will issue an error like "returns a reference to data owned by the current function," preventing the creation of a dangling reference.
Let's return to the original problem where the take
function consumed ownership of the string, making it unusable in main
afterwards. We can solve this using borrowing.
The original take
function signature was:
fn take(s: String)
We can modify this function (or create a new one) to accept a reference instead:
When this code is run, the output will be:
By changing borrow_string
to accept &String
(an immutable reference to a String
) and calling it with &original_s
(creating and passing an immutable reference), the ownership of original_s
remains with the main
function. Therefore, original_s
is still valid and can be printed after the call to borrow_string
.
If we needed the function to modify the string, we would pass a mutable reference:
Output:
To recap the core rules and benefits of borrowing in Rust:
Borrowing allows temporary access to a value via references without taking ownership.
Creating a reference does not move ownership of the data.
References can be immutable (&T
), allowing read-only access, or mutable (&mut T
), allowing read-write access.
For any given piece of data in a particular scope, you can have:
Any number of immutable references, OR
Exactly one mutable reference.
You cannot have both types simultaneously active for the same data.
A reference must never outlive the data it points to. Rust's compiler enforces this rule to prevent dangling references.
This system of ownership and borrowing, enforced at compile time, allows Rust to provide memory safety without needing a garbage collector, leading to efficient and reliable programs.
A clear pathway to Understanding Borrowing and References in Rust - Uncover how Rust's borrowing allows functions to use data via references without taking ownership, enhancing flexibility. Master the rules for immutable and mutable borrows, and how Rust's compile-time checks prevent dangling references for robust, memory-safe programs.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Introduction to the Rust programming language
Rust variables and functions
Scalar types, arrays, strings, enum, structs, vectors, and hash maps in Rust
Rust control flows: If / else, if let and let else, loop, match
Rust ownership, including borrow and references
Rust error handling
Rust Modules
Rust Traits
Last updated on August 15, 2025
Rust Developer
Rust Programming BasicsDuration: 6min
Duration: 18min
Duration: 47min
Duration: 15min
Duration: 19min
Duration: 8min
Duration: 12min
Duration: 46min
Duration: 14min
Course Overview
About the course
Introduction to the Rust programming language
Rust variables and functions
Scalar types, arrays, strings, enum, structs, vectors, and hash maps in Rust
Rust control flows: If / else, if let and let else, loop, match
Rust ownership, including borrow and references
Rust error handling
Rust Modules
Rust Traits
Last updated on August 15, 2025