[Rust Series 6] Getting Comfortable with References and Borrowing

한국어 버전

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

  1. Immutable references (&T): read-only lenses that you can create in parallel.
  2. Mutable references (&mut T): allow mutation but only one may exist at a time.
  3. 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 = &note; // immutable reference
    let also_read = &note; // 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.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. Fork the starter and open src/main.rs
  2. Paste the lesson code and run cargo check plus cargo run in order
  3. Change types, values, or borrowing flow and compare the compiler feedback with the output

Rust practice here is mainly terminal-driven rather than browser-preview driven. Lessons that need multiple files or extra crates may require a bit more setup inside the starter.

Try It Yourself

  • Replace String with Vec<i32>, have immutable references compute a sum, and mutable references push new values.
  • Use separate scopes to borrow immutably first, drop those references, and only then create a mutable borrow.
  • Notice how far the compiler infers lifetimes automatically when you pass references into functions. (Full lifetime syntax shows up in part 14.)

Wrap-Up

The heart of references and borrowing is "split reading and writing so the language can prevent data races." If you remember that immutable references can multiply while mutable ones stay single, you’re ready for slices, strings, and later for method design. Next we’ll look at strings and slices to see how these rules keep real data segments safe.

💬 댓글

이 글에 대한 의견을 남겨주세요