Rust: Rust References and Borrows
Last updated: 2026-08-26
A citation is like a library card—you can borrow it (read-only citation) or request exclusive permission to rewrite it (editable citation), but you can’t do both at the same time.
Rust's reference mechanism allows you to access data without transferring ownership. This is called "borrowing."
1. What You'll Learn
- Use
&Tto create an immutable reference (read-only borrow) - Create a mutable reference (read-write borrow) using
&mut T - Borrowing rules: one mutable reference or multiple immutable references (not both at the same time)
- The Scope of References and NLL (Non-Lexical Lifetimes)
- Dangling References and Compiler Safeguards
- Applications of References in Function Parameters
2. Conceptual Diagrams
flowchart LR
subgraph "Transfer of Ownership"
A1["Data String"] -->|"let s2 = s1<br>move"| B1["New Owner s2"]
B1 -->|"s1 Failure"| C1["Compilation Error<br>println!(s1)"]
end
subgraph "Citation, Borrowing"
A2["Data String"] -->|"let r = &s<br>borrow"| B2["Quote r"]
B2 -->|"r Return after use<br>s Still valid"| C2["✅ Secure Access"]
end
3. The Story of a Library
(1) Heartbreak: The book was torn to pieces
There is a popular book titled Programming in Rust in Tom's library, and it can only be checked out once:
- Alice borrowed this book, and ownership was transferred to Alice
- Bob wanted to read it, but the book was no longer at the library—it belonged to just one person.
- The library will have to buy another copy.
"If only I could let multiple people read the same book at the same time without them tearing it apart..."
(2) Solutions for Rust References
fn main() {
let book = String::from("Rust Programming");
// Multiple immutable references: Multiple people at the same time "read-only" check out
let reader1 = &book; // Reader 1 checks out (read-only)
let reader2 = &book; // Reader 2 checks out (read-only)
println!("Readers 1 See: {}", reader1);
println!("Readers 2 See: {}", reader2);
// Both references are valid at the same time -- because everyone has read-only access
let mut notebook = String::from("Notebook");
// Mutable References: write-only permission
let writer = &mut notebook; // Only one person can make changes
writer.push_str("-- I wrote some notes");
println!("Rewriter: {}", writer);
// writer ends scope here, only then can others borrow it again.
}
Citation rules are like library rules: Either multiple people can read at the same time (multiple &T), or one person can edit alone (one &mut T)—but they cannot happen simultaneously.
4. Borrowing Rules
graph TB
A[Borrowing Rules] --> B[You can only choose one of them.]
B --> C[Multiple immutable references &T]
B --> D[Or a mutable reference &mut T]
B --> E[Cannot coexist]
A --> F[References must always be valid.]
F --> G[There must be no dangling references.]
| Rule | Description | Consequences of Violation |
|---|---|---|
| Multiple &T or a single &mut T | Immutable and mutable references cannot coexist | Compilation error |
| References must be valid | A reference cannot outlive the object it refers to | Compilation error |
| When a reference is immutable, the original value cannot be modified either | When &T exists, the original value cannot be modified via &mut T | Compilation error |
| NLL Scope | The scope of &mut T ends after its last use | Compiler-driven optimization |
(2) Comparison of Reference Types
| Citation Type | Syntax | Permissions | Maximum Number Allowed | Typical Uses |
|---|---|---|---|---|
| Immutable Reference | &T |
Read-only | Multiple | Read-only access to function parameters |
| Variable reference | &mut T |
Read/write | 1 | Function parameter modifies data |
| Fat Pointer (Slice) | &[T] |
Read-Only | Multiple | Array/String Slice |
(3) Quick Reference for Borrowing Scenarios
| Scenario | Recommended Approach | Example |
|---|---|---|
| Read-Only Function Access | &T |
fn len(s: &String) -> usize |
| Functions that Modify Data | &mut T |
fn push(s: &mut String, ch: char) |
| Function Data Usage | T (pass-by-value) |
fn consume(s: String) |
| Function Return Data | T (Return Value) |
fn create() -> String |
| Multiple Functions Share Read-Only Access | &T |
Multiple Functions Receive the Same &T |
| Alternating Modifications | &mut T (Time-Division Multiplexing) |
NLL Allows Alternating Creation of &mut T |
5. Citation Examples
▶ Example 1: Immutable References—Read-Only Access (Difficulty ⭐⭐)
Output:
'Hello, Rust!' The length of is: <len>
r1: Hello, Rust!, r2: Hello, Rust!
// ============================================
// Immutable References &T: Multiple readers can borrow items at the same time
// ============================================
fn calculate_length(s: &String) -> usize {
// s is a String reference, no ownership
s.len()
} // s goes out of scope, but since it's a reference, it will not destroy the String
fn main() {
let s = String::from("Hello, Rust!");
let len = calculate_length(&s); // Borrow s, no transfer of ownership
println!("'{}' The length of is: {}", s, len); // ✅ s Still available
// Multiple immutable references can coexist
let r1 = &s;
let r2 = &s;
println!("r1: {}, r2: {}", r1, r2); // ✅ Read simultaneously
}
Output:
'Hello, Rust!' The length of is: 12
r1: Hello, Rust!, r2: Hello, Rust!
Output:
Modifying Through a Variable Reference: <r>
&screates a reference tos; when passed to a function, ownership is not transferred. After the function returns, the reference becomes invalid, butsstill exists. This is what is known as a "borrow"—you use it and then return it.
▶ Example 2: Mutable References—Exclusive Modification Permissions (Difficulty ⭐⭐)
Output:
Modifying Through a Variable Reference: <r>
Another mutable reference: <r2>
<ref1>, <ref2>
// ============================================
// Mutable References &mut T: Only one writer at a time
// ============================================
fn main() {
let mut s = String::from("Hello");
// Create a mutable reference
let r = &mut s;
r.push_str(", world!");
println!("Modifying Through a Variable Reference: {}", r);
// r is used here for the last time, NLL ends
// After the mutable reference ends, you can create a new mutable reference.
let r2 = &mut s;
r2.push_str("!!");
println!("Another mutable reference: {}", r2);
// --- Examples of Errors (Uncomment to view compilation errors) ---
// let mut s2 = String::from("test");
// let ref1 = &mut s2;
// let ref2 = &mut s2; // ❌ There cannot be two mutable references at the same time.
// println!("{}, {}", ref1, ref2);
}
Output:
r1: Key Data, r2: Key Data
r3: <r3>
Demo, <r_b>
&mutis mutable—the value of the reference can be modified. However, Rust enforces the rule that only one mutable reference can exist at a time. This prevents data races—no locks or atomic operations are needed; the issue is resolved entirely at compile time.
▶ Example 3: Immutable and mutable references cannot coexist (Difficulty ⭐⭐⭐)
Output:
r1: Key Data, r2: Key Data
r3: <r3>
Demo, <r_b>
// ============================================
// Demonstration of Conflicts Between Immutable and Mutable References
// ============================================
fn main() {
let mut data = String::from("Key Data");
let r1 = &data; // ✅ Immutable References 1
let r2 = &data; // ✅ Immutable References 2
println!("r1: {}, r2: {}", r1, r2);
// r1 and r2 are used here for the last time
let r3 = &mut data; // ✅ At this point, you can create a mutable reference.
r3.push_str("--Modified");
println!("r3: {}", r3);
// --- Examples of Errors: A mutable reference cannot be created while an immutable reference still exists. ---
// let mut s = String::from("Demo");
// let r_a = &s; // Immutable References
// let r_b = &mut s; // ❌ Compilation Error: immutable reference already exists
// println!("{}, {}", r_a, r_b);
}
Output:
Safe Return Values: hello
Referencing Local Variables: Local Data
Key point: The immutable references
r1andr2go out of scope after their last use (println!), and only then can the mutable referencer3be created. This is NLL (Non-Lexical Lifetime)—the compiler intelligently determines when a reference is no longer in use, rather than waiting for the end of the block.
▶ Example 4: Preventing Hanging References (Difficulty ⭐⭐⭐)
Output:
Safe Return Values: hello
Referencing Local Variables: Local Data
// ============================================
// How Do Compilers Prevent Dangling References?
// ============================================
// Examples of Errors: Return a reference to a local variable
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // ❌ Compilation Error: s is destroyed at the end of the function, the reference would be dangling
// }
// The Correct Approach: Return String directly (transfer of ownership)
fn no_dangle() -> String {
let s = String::from("hello");
s // Return String directly, ownership is transferred to the caller
}
fn main() {
let s = no_dangle();
println!("Safe Return Values: {}", s);
// The Correct Way to Reference Local Variables: Use within the scope
let local = String::from("Local Data");
let r = &local; // Quote
println!("Referencing Local Variables: {}", r);
// r ends here, local is still valid
} // local is destroyed here (later than r), safe
Output:
The deposit amount must be greater than 0
Deposit <amount> Yuan Chenggong
Transfer Failed: Insufficient balance or invalid amount
Transfer <amount> yuan: <from.owner> -> <to.owner>
--- Account List ---
<acc.owner>. <acc.balance> balance: <i + 1> yuan
<check_balance(&alice)>
A dangling reference occurs when the memory pointed to by a reference has been deallocated. The Rust compiler can detect this situation at compile time—if the object pointed to by a reference is destroyed after the reference is created, the compiler will report an error. This completely eliminates bugs such as "dangling pointers."
▶ Example 5: Comprehensive Exercise—Bank Account Operations (Difficulty ⭐⭐⭐)
Output:
The deposit amount must be greater than 0
Deposit <amount> Yuan Chenggong
Transfer Failed: Insufficient balance or invalid amount
Transfer <amount> yuan: <from.owner> -> <to.owner>
--- Account List ---
<acc.owner>. <acc.balance> balance: <i + 1> yuan
<check_balance(&alice)>
<check_balance(&bob)>
<check_balance(&alice)>
Total Deposits: <total> yuan
// ============================================
// Comprehensive Example: Applications of References in Real-Life Scenarios
// ============================================
struct Account {
owner: String,
balance: f64,
}
fn check_balance(account: &Account) -> String {
format!("{} balance: {:.2} yuan", account.owner, account.balance)
}
fn deposit(account: &mut Account, amount: f64) {
if amount <= 0.0 {
println!("The deposit amount must be greater than 0");
return;
}
account.balance += amount;
println!("Deposit {:.2} Yuan Chenggong", amount);
}
fn transfer(from: &mut Account, to: &mut Account, amount: f64) -> bool {
if amount <= 0.0 || from.balance < amount {
println!("Transfer Failed: Insufficient balance or invalid amount");
return false;
}
from.balance -= amount;
to.balance += amount;
println!("Transfer {:.2} yuan: {} -> {}", amount, from.owner, to.owner);
true
}
fn show_accounts(accounts: &[Account]) {
println!("--- Account List ---");
for (i, acc) in accounts.iter().enumerate() {
println!("{}. {} balance: {:.2} yuan", i + 1, acc.owner, acc.balance);
}
}
fn main() {
let mut alice = Account { owner: String::from("Alice"), balance: 1000.0 };
let mut bob = Account { owner: String::from("Bob"), balance: 500.0 };
println!("{}", check_balance(&alice));
println!("{}", check_balance(&bob));
deposit(&mut alice, 200.0);
println!("{}", check_balance(&alice));
transfer(&mut alice, &mut bob, 300.0)?;
let accounts = [&alice, &bob];
show_accounts(&accounts);
let total: f64 = accounts.iter().map(|a| a.balance).sum();
println!("Total Deposits: {:.2} yuan", total);
}
Output:
Alice balance: 1000.00 yuan
Bob balance: 500.00 yuan
Deposit 200.00 Yuan Successful
Alice balance: 1200.00 yuan
Transfer 300.00 yuan: Alice -> Bob
--- Account List ---
1. Alice balance: 900.00 yuan
2. Bob balance: 800.00 yuan
Total Deposits: 1700.00 yuan
This example demonstrates the practical application of the three reference types:
&T(read-only query),&mut T(modification operation), and&[T](read-only traversal).transferrequires two&mut Treferences simultaneously, which is safe in Rust because they point to different data.
❓ FAQ
📖 Summary
- Reference allows you to access data without transferring ownership—that is, by "borrowing" it.
- Immutable reference
&T: Multiple instances can exist simultaneously; read-only - Variable reference
&mut T: Only one at a time; read/write - Borrowing rules: Multiple
&Tor a single&mut Tcannot coexist - References must always be valid; the compiler prevents dangling references
- NLL causes a reference to end after its last use, rather than waiting until the end of the block
📝 Exercises
- Difficulty ⭐: Write a function
fn print_message(msg: &String)that prints the string passed to it. Inmain, createStringand then call this function to verify that the original variable remains accessible after the call. - Difficulty ⭐⭐: Write a program to create a
mut String. First, create two immutable references and print them; then, create a mutable reference, modify its contents, and print the modified result. - Difficulty ⭐⭐⭐: Try writing a function that returns a reference (such as
fn get_ref() -> &String), and observe the compiler’s error messages. Then modify it to return ownership ofString, and understand why Rust prohibits returning references to local variables.