If ownership in part 5 was about "each value has a single owner," part 6 focuses on "what rules apply when you just want temporary access to that value." A reference lets you read or write without transferring ownership, and borrowing is the act of taking that reference. We will stay away from advanced lifetime syntax and simply build intuition for when immutable and mutable references are allowed.
These rules exist so Rust can stop "one reader and one writer colliding" before your code ever runs. Instead of detecting data races at runtime, the compiler rejects them.
What We Cover Here
- Immutable references (
&T): read-only lenses that you can create in parallel. - Mutable references (
&mut T): allow mutation but only one may exist at a time. - Core borrowing rule: "many immutable references" or "a single mutable reference" is fine, but mixing them at the same moment triggers a compile error.
Reference Rules by Example
Rust variables are immutable by default, and references follow the same philosophy.
fn main() {
let note = String::from("Rust ownership");
let read_only = ¬e; // immutable reference
let also_read = ¬e; // another immutable reference
println!("{} / {}", read_only, also_read);
println!("The original still works: {}", note);
}
Immutable references let multiple places read without copying. The key point is that "while a reference is in use, trying to mutate the original" is not allowed. Mutable references flip the trade-off: they grant write access but must stay unique.
fn main() {
let mut score = 80;
{
let change = &mut score; // only one mutable reference
*change += 5;
} // change drops here, so score becomes usable again
println!("Final score: {}", score);
}
*change dereferences the pointer so you can touch the underlying value. When you borrow mutably, remember you need both mut on the binding and &mut when creating the reference.
Combinations That Don’t Compile Together
It helps to deliberately write code that violates the borrowing rules and read the compiler message.
fn main() {
let mut label = String::from("Rust");
let read = &label;
let write = &mut label; // error: cannot make a mutable borrow while an immutable borrow lives
println!("{} {}", read, write);
}
Rust blocks this to prevent data races at compile time. What matters is not the scope keyword but "whether references overlap in time." In real projects you either shorten the scope or move code so that borrows never coexist.
Passing by Value vs by Reference
fn read_title(title: &String) {
println!("Read-only: {}", title);
}
fn main() {
let title = String::from("Rust notes");
read_title(&title); // pass a reference
println!("Original still usable: {}", title);
}
Passing a String directly moves ownership, but passing &String or &str only lends read permission. Adding &x in front of an argument makes your intent explicit: "borrow, don’t take."
Connecting to a Small Practice Project
This series keeps evolving a simple console app that tracks study progress. Let’s store the progress as a string and use references to summarize it.
fn describe(status: &String) {
println!("Progress: {}", status);
}
fn main() {
let mut status = String::from("reviewing ownership");
describe(&status); // immutable borrow for reading
update(&mut status); // mutable borrow for updating
describe(&status);
}
fn update(status: &mut String) {
status.push_str(", practicing borrowing");
}
main keeps ownership of status. describe only borrows immutably, while update borrows mutably and therefore exclusively. Once you write & or &mut in a signature, it’s obvious to the reader that "this function borrows instead of owning."
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.
💬 댓글
이 글에 대한 의견을 남겨주세요