Langsung ke konten
Semua insights

Academy

Borrowing: How Rust Lets You Look Without Touching

Discover how Rust's borrowing model ensures memory safety and data-race freedom at compile time without garbage collection. Read the full article!

7 menit baca

Borrowing: How Rust Lets You Look Without Touching
Di halaman ini

Ownership solves memory management beautifully, but it creates a practical problem: if every function that needs to read data has to take ownership of it, you'd spend your entire program moving data back and forth. "Here, function, take this String. Now give it back. Now you take it. Now give it back." It would be like a library where checking out a book required buying it, and returning it required selling it back. Technically functional. Practically insane.

Borrowing is Rust's solution: let functions access data without owning it. A borrow is a reference—a pointer with compile-time guarantees—that lets you read or modify data while the original owner retains ownership. When the borrow ends (the reference goes out of scope), the owner still has the data, untouched and valid.

But borrowing isn't just "pointers with extra steps." It's governed by two rules that, together, prevent every data race that has ever existed or ever will exist in safe Rust:

  1. You can have any number of immutable references (&T) to a value, OR

  2. You can have exactly one mutable reference (&mut T), but not both simultaneously.

These two rules—multiple readers XOR one writer—are the formal definition of data-race freedom, encoded into the type system and verified at compile time. No runtime checks. No race condition detectors. No "we'll catch it in testing." If your code compiles, it's data-race free. Period.

At RantAI, borrowing is what makes our concurrent data pipelines both efficient (no unnecessary copying) and safe (no data races). This article, drawn from Chapter 9, Section 9.1.2 of our guide "The Rust Programming Language," covers borrowing, references, and the lifetime system that ensures references are always valid.

Immutable Borrowing: Multiple Readers#

Rust
fn calculate_stats(data: &Vec<f64>) -> (f64, f64) {
    let sum: f64 = data.iter().sum();
    let mean = sum / data.len() as f64;
    let variance = data.iter()
        .map(|x| (x - mean).powi(2))
        .sum::<f64>() / data.len() as f64;
    (mean, variance)
}

fn main() {
    let measurements = vec![23.5, 24.1, 22.8, 25.0, 23.9];
    
    // Borrow immutably — measurements retains ownership
    let (mean, var) = calculate_stats(&measurements);
    
    // measurements is still valid — it was only borrowed, not moved
    println!("Data: {:?}", measurements);
    println!("Mean: {:.2}, Variance: {:.4}", mean, var);
    
    // Multiple immutable borrows are fine
    let r1 = &measurements;
    let r2 = &measurements;
    let r3 = &measurements;
    println!("{:?} {:?} {:?}", r1, r2, r3);  // All valid simultaneously
}

&measurements creates an immutable reference—a borrow that can read but not modify. Multiple immutable borrows can coexist because concurrent reads are safe. The function calculate_stats reads the vector without taking ownership, and measurements remains valid after the call.

Mutable Borrowing: One Exclusive Writer#

Rust
fn add_measurement(data: &mut Vec<f64>, value: f64) {
    data.push(value);
}

fn main() {
    let mut measurements = vec![23.5, 24.1, 22.8];
    
    add_measurement(&mut measurements, 25.0);  // Mutable borrow
    
    println!("Updated: {:?}", measurements);  // [23.5, 24.1, 22.8, 25.0]
}

&mut measurements creates a mutable reference—exclusive access to modify the data. While the mutable borrow is active, no other references (mutable or immutable) to measurements can exist. This exclusivity prevents data races.

The Rule in Action: No Mixing#

Rust
fn main() {
    let mut data = vec![1, 2, 3];
    
    let r1 = &data;      // Immutable borrow #1
    let r2 = &data;      // Immutable borrow #2 — fine, multiple readers OK
    
    // let r3 = &mut data;  // COMPILE ERROR: can't borrow mutably while immutably borrowed
    
    println!("{:?} {:?}", r1, r2);  // Last use of r1 and r2
    
    // Now we can borrow mutably — the immutable borrows are no longer active
    let r3 = &mut data;
    r3.push(4);
    println!("{:?}", r3);
}

Rust uses non-lexical lifetimes (NLL): a borrow is active from when it's created until its last use, not until the end of its scope. In the example above, r1 and r2 are last used in the println!, so the mutable borrow r3 is allowed afterward. The compiler is smart about when borrows actually end.

Lifetimes: Proving References Are Valid#

Borrowing creates a question: how long is the reference valid? If the data is freed while a reference still exists, you have a dangling reference. Rust prevents this with lifetimes—compile-time annotations that prove references don't outlive the data they point to.

Rust
// Lifetime annotation: the returned reference lives as long as the input
fn first_word<'a>(s: &'a str) -> &'a str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);  // word borrows from sentence
    println!("First word: {}", word);  // Valid — sentence is still alive
    
    // This would NOT compile:
    // let word;
    // {
    //     let sentence = String::from("hello world");
    //     word = first_word(&sentence);  // sentence dies at the closing brace
    // }
    // println!("{}", word);  // COMPILE ERROR: sentence was dropped
}

The lifetime 'a says: "the returned reference lives at least as long as the input reference." The compiler verifies this at every call site. If the data might be dropped while the reference is still alive, the code doesn't compile. No dangling references. Ever.

Most of the time, you don't write lifetime annotations—the compiler infers them through lifetime elision rules. But when the compiler can't determine lifetimes automatically, you add annotations to make the relationships explicit.

Lifetimes in Structs#

Rust
struct Excerpt<'a> {
    text: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first = novel.split('.').next().unwrap();
    let excerpt = Excerpt { text: first };
    
    println!("Excerpt: {}", excerpt.text);  // Valid — novel outlives excerpt
}

The lifetime 'a on Excerpt guarantees that the struct cannot outlive the string it references. The compiler enforces this at every usage site.

Borrowing and Concurrency: Thread-Safe by Design#

Rust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        }));
    }
    
    for h in handles { h.join().unwrap(); }
    println!("Result: {}", *counter.lock().unwrap());  // Always 10
}

Arc<Mutex<T>> provides shared ownership (Arc) with exclusive mutable access (Mutex). The borrowing rules are enforced at runtime through the mutex lock—only one thread can hold the lock at a time. The compiler ensures you use the mutex (you can't access the data without locking), and the mutex ensures exclusive access at runtime.

Broader Implications: Borrowing as API Design#

At RantAI, function signatures that use borrowing tell the entire ownership story: &T means "I read your data." &mut T means "I modify your data exclusively." T means "I take your data." These signatures are contracts that the compiler enforces. Code reviewers don't need to verify memory safety—the type system does it.

Practical Applications & Strategic Takeaways#

For newcomers: Default to &T (immutable borrow) in function parameters. Use &mut T only when the function needs to modify the data. Use T (ownership) only when the function needs to store the data permanently or consume it.

For C++ veterans: Rust's &T is what const& would be if C++ actually enforced constness (no const_cast, no mutable members). Rust's &mut T is what & would be if the compiler ensured exclusive access.

For concurrent programmers: The borrowing rules—multiple readers XOR one writer—are the formal definition of data-race freedom. If your code compiles, it's data-race free in safe Rust. No sanitizers needed.

Our Commitment to Open Knowledge#

RantAI is committed to open education. Borrowing and lifetimes are covered in Chapter 9, Section 9.1.2 of our guide, "The Rust Programming Language," freely available online.

Explore these concepts further: https://trpl.rantai.dev

Support Our Mission & Get Your Handbook#

When did the borrowing model click for you? Was it the "multiple readers XOR one writer" insight? A lifetime error that finally made sense? Share below!

#RustLang #Borrowing #Lifetimes #References #RantAI #LearnRust #DataRaceFreedom #MemorySafety #Concurrency #CompileTimeSafety

Artikel berikutnya

The Ownership Model: Three Rules, Zero Ambiguity