Skip to content
academia.sh

Lesson 03 / 14

Borrowing

A value can be read and written through a reference without ever giving up ownership: while the borrow lasts no record forms in the ledger, and once the borrow's scope closes the value is still accessible in its owner. A shared and a mutable reference exclude each other; a reference's scope always has to stay inside its source's scope.

Published Updated

In this lesson

  • Shows by running it that a borrowed value never ends anywhere while the borrow lasts, and is still accessible in its owner once the borrow's scope closes
  • Distinguishes the difference between a shared and a mutable reference with examples
  • Proves with nested scopes that a reference's scope has to stay inside its source's scope
Contents

The previous lesson measured how the ending point moves when ownership is transferred — the moved value’s old name lost its validity. This lesson measures how a value is reached without ownership ever being transferred: borrowing. The question is this: does the name lending a value out lose the value, and does a borrowed value end anywhere while the borrow lasts? This is not to be confused with the Concurrency course’s question of how a goroutine stops: there, a running unit stops; here, a value ends.

The previous lesson’s last section showed that a value can be moved into a function one-way and irreversibly — or that even as the owner changed across a chain, the value always ended up ending somewhere in the end. This lesson opens a different path for values passed to the same kind of function: the same job can be done without ever moving a value, just by giving it temporary access. Both paths are valid, but their results can be read separately in the source.

The Programming Fundamentals course established as a concept how a value passes to a function as a reference. What this lesson measures is that Rust splits this kind of passing into two separate, mutually exclusive forms — a shared and a mutable reference — and tests this distinction at compile time.

Borrowing Is Not Giving Ownership

A reference (&T or &mut T) is not the owner of the value it points to; it is only a separate, small value that provides access to that value. A reference has its own scope too, and the reference ends when that scope closes, but the reference ending does not affect the value it points to — because the reference was never the owner of that value to begin with, so it has nothing to trigger Drop when its scope closes.

Rust distinguishes two kinds of references. A shared reference (&T) can be taken multiple times on the same value at the same time, and allows only reading. A mutable reference (&mut T) can only be one at a time, and allows changing the value alongside reading it. The two cannot exist together, at the same time, for the same value.

These two kinds are not a choice, they are a requirement. Every piece of code that wants to access a value — a function parameter, a local binding, a struct method — has to state up front which kind it wants; even though the only difference in the syntax is a single mut keyword, the authority it produces is entirely different. Because this distinction is visible in the source, a reader can tell whether a reference can change the value just by looking at the line where that reference was taken — without looking at the value’s own definition or how it is used.

OR7. A reference is a value in its own right and has its own scope, but a reference to a type that implements Drop, like Trace, does not itself implement Drop. A value of type &Trace or &mut Trace disappears silently at the end of its scope; it writes nothing to the ledger, because the code that does the writing lives only in Trace’s own drop method, and that method is only called when an owned Trace ends.

This distinction sets up a direct contrast with the previous lesson. In the Ownership and Moves lesson, a Trace value passed to a function was moved into the function’s parameter — once the function ended, the value ended too, and passing the same value to a second call was not possible, because the old name stayed invalid after the first call. If a function borrows a value instead, this constraint disappears: the function uses the value and lets it go, ownership never changes hands, and the caller side can reuse the same value as many times as it wants.

Every measurement in this lesson uses the same excerpt from the previous lesson’s core — Trace, the Drop implementation, and the observation ledger; the helper part is placed in a separate file so later examples can pull it in again and again with mod.

// trace.rs — a measurement helper that records a value's ending
use std::cell::RefCell;
use std::rc::Rc;

pub type Ledger = Rc<RefCell<Vec<String>>>;

pub struct Trace {
    pub name: String,
    ledger: Ledger,
}

impl Trace {
    pub fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace { name: name.to_string(), ledger: Rc::clone(ledger) }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

pub fn new_ledger() -> Ledger {
    Rc::new(RefCell::new(Vec::new()))
}

pub fn read_log(ledger: &Ledger) -> Vec<String> {
    ledger.borrow().clone()
}
// borrow_fn.rs — a function borrowing and returning a value, in contrast with a move
mod trace;
use trace::{Trace, new_ledger, read_log};

fn read(reference: &Trace) -> usize {
    reference.name.len()
}

fn main() {
    let ledger = new_ledger();
    let value = Trace::new("borrowable", &ledger);

    let length1 = read(&value);
    let length2 = read(&value);
    println!("first read: {}, second read: {}", length1, length2);
    println!("ledger after two calls: [{}]", read_log(&ledger).join(", "));
}
first read: 10, second read: 10
ledger after two calls: []

The read function borrows value twice, one after another. The Ownership and Moves lesson’s consume function could not have compiled with a call taking the same value a second time, because the first call had already consumed ownership. Here both calls are valid, because read never asks for ownership — its parameter type is &Trace, not Trace. Even after two calls the ledger stays empty: value keeps living in its owner all the way to the end of main.

The & sign in the read(&value) call is required here — the function asks for a reference, and the source has to give it explicitly. The same requirement does not show up elsewhere in the code (a.name, value.name): reaching a field with . or calling a method is implicitly borrowed by the compiler when needed. The reference.name.len() line works the same way — reference is already a &Trace, but the .name access and the .len() call do not require writing a separate &. Implicit borrowing only makes the source easier to read; its limits (shared or mutable, how long it lasts) are subject to the same rules.

Measurement: The Ledger Stays Empty While the Borrow Lasts

The measurement below nests three levels of scope: main on the outside, value’s own ownership scope inside it, and inside that, the shared and mutable borrow scopes in turn. The helper file (trace.rs) is already in the working directory; this block pulls it in again with mod trace;.

// borrow.rs — a borrowed value never ends anywhere while the borrow lasts
mod trace;
use trace::{Trace, new_ledger, read_log};

fn main() {
    let ledger = new_ledger();

    {
        let mut value = Trace::new("borrow-1", &ledger);

        {
            let a = &value;
            let b = &value;
            println!("shared read: {} and {}", a.name, b.name);
            println!("ledger while the borrow lasts: [{}]", read_log(&ledger).join(", "));
        }
        println!("ledger after the shared borrow ends: [{}]", read_log(&ledger).join(", "));

        {
            let m = &mut value;
            m.name.push_str("-mod");
            println!("name inside the mutable borrow: {}", m.name);
        }
        println!("ledger after the mutable borrow ends: [{}]", read_log(&ledger).join(", "));
        println!("value is still accessible, its own name: {}", value.name);
    }
    println!("ledger after the outer scope ends: [{}]", read_log(&ledger).join(", "));
}
shared read: borrow-1 and borrow-1
ledger while the borrow lasts: []
ledger after the shared borrow ends: []
name inside the mutable borrow: borrow-1-mod
ledger after the mutable borrow ends: []
value is still accessible, its own name: borrow-1-mod
ledger after the outer scope ends: [borrow-1-mod]

Four of the five separate measurement points show that the ledger is empty — while the shared borrow lasts, after the shared borrow ends, while the mutable borrow lasts (this does not show up separately since the line is printed inside it, but the next line confirms it), and after the mutable borrow ends. value only ends when the outermost scope (main’s own body) closes, and only then does the ledger get the entry borrow-1-mod. That a and b can both exist at the same time — both only read value, without interfering with each other — shows the plurality of a shared reference.

The output’s last line (borrow-1-mod) proves something beyond this: value itself was never re-created. The change made through m was written to value’s own name field; the name that drops into the ledger when value ends is not borrow-1, the name at the moment it was created, but its form after being changed during the borrow. This shows that borrowing is not a read-only view but a real access path — a shared reference only reads, while a mutable reference can permanently change the source itself.

Shared and Mutable References

The line m.name.push_str("-mod") really changes value’s own field through a mutable reference — after this line, both m.name and, once the scope closes, value.name carry the new value (borrow-1-mod). This was not possible with the shared references (a, b): a line trying to change a.name would be rejected by the compiler, because &T only grants permission to read.

OR8. Shared references being able to exist in multiple numbers at the same time comes from the fact that none of them can change the value — reads do not interfere with each other. A mutable reference being able to exist only one at a time, in contrast, comes from the opposite reason: a change can lead to an unexpected result in the middle of another read or another change. The rule looks not at the number of references, but at whether a change exists among them.

The concrete counterpart of this distinction is that borrowing itself also carries two separate authorities. The read function cannot change the value because it only asks for &Trace — the parameter type itself prevents this, there is no need to even look at the function’s body. If a function wants to change a value, it has to write its parameter as &mut Trace, and in that case that function cannot be called alongside any other reference already borrowed on the same value at the same time. This means a function’s signature decides up front not only which type it takes, but also what it can do with that type — just as the Rust’s Design Goals lesson showed the class being read from the signature, here the authority is read from the signature too.

Rejection: One Writer or Many Readers, Never Both

The code above used the shared borrow and the mutable borrow in separate scopes, one after another — the two were never alive at the same time. This is not a coincidence, it is required. If a line like let m = &mut value; had been added while a and b were still in scope, the compiler would reject it: a shared and a mutable reference to the same value cannot exist together at the same time. The same rule applies to two mutable references — let m1 = &mut value; and let m2 = &mut value; cannot live together in the same scope, because both carry write permission on the same field, and it would become unreadable from the source which write is supposed to apply first and which second. This is not a problem for the two shared references (a and b), because neither writes — the order of reading does not change the result.

This rejection is not a runtime check; the source, the moment the two references’ scopes overlap, does not compile at all. The accepted code (above) already excludes this overlap from the start: the shared scope closes with its own curly braces, the mutable scope opens afterward with separate curly braces — the two scopes never overlap at any point.

The same constraint applies to function calls too. read(&value) could be called twice in a row because neither call’s reference was still alive when the next call began — each call opens its own borrow scope and closes it within itself. Code trying to pass the same value to both read (&Trace) and a function that changes it at the same time (&mut Trace) would not compile, because the two scopes overlap. The rule is not limited to a single function’s body; every reference scope in a source is subject to the same check.

Boundary Measurement: Borrowing Does Not Extend a Life

The three levels of scope (main, value‘s own scope, the borrow scopes) nest inside each other, and this nesting is one-directional: a borrow scope always stays inside its source’s scope, never spilling outside it. a and b close their own scopes before value’s scope closes; so does m. No reference lives longer than the value it points to.

OR9. The interval during which a reference is valid can never exceed its source’s scope — this is the concrete counterpart of borrowing not extending a life. value itself lives until main’s body ends; none of the references inside it can claim a longer interval than that, because if it could, that reference would end up pointing to a source value that had already ended.

This constraint was met here through nested scopes, with natural code: every borrow opened and closed within its own curly braces, and value’s own scope enclosed all of them. Code that tries to move a reference outside this nesting — to somewhere it would still be valid after value‘s scope, for example — is rejected by the compiler; this interval’s own name and rules will be taken up further ahead.

Why this constraint is required becomes clear when thinking through the opposite. If the mutable reference named m could live longer than value‘s scope, a read or a write could be attempted through m even after value ended and was written to the ledger — but by then there would no longer be a valid Trace left to point to. The compiler prevents this scenario from the start by limiting the interval during which a reference is valid to its source’s scope. The &Trace parameter in the read function is subject to the same constraint: no matter how much the function uses this reference inside its own body, the reference’s interval can never exceed the duration of the read call — value on the caller’s side is still in its owner, and the reference is even re-taken between the two calls.

The three things measured in this lesson are connected to each other. Because borrowing does not give ownership, a borrowed value never ends anywhere while the borrow lasts; because a reference is itself a small value separate from the source, with its own scope, it does not trigger the source’s ending; and because a reference’s scope can never exceed the source’s scope, the guarantee of “pointing to a value that is still alive” is built in from the start. The shared/mutable distinction is a fourth constraint layered on top of these three measurements: multiple reads at the same time are safe, but a change can never happen at the same time as any other access. The “single owner” principle from the Ownership and Moves lesson widens here into “one writer, many readers” — ownership is still in a single name, but with that name’s permission, multiple temporary accesses can exist at the same time. This widening also means the owner itself is never sidelined: no matter how many references value hands out, it keeps being the single real owner within its own scope.

Summary

  • A reference is not the owner of the value it points to; when its own scope closes, only it ends, without affecting the source.
  • No record forms in the ledger while a borrow lasts; a borrowed value stays accessible in its owner even after the borrow’s scope closes. The life of a value being lent out ends at scope end, not when the borrow ends.
  • A shared reference (&T) can exist in multiple numbers at the same time and only reads; a mutable reference (&mut T) can exist only one at a time and can change the value.
  • A shared and a mutable reference, or two mutable references, cannot exist together on the same value at the same time; this rule is tested at compile time.
  • A reference’s scope always has to stay inside its source’s scope — borrowing does not extend a life.
  • The same value can be passed to a borrowing function several times in a row; this is a direct contrast with a function that asks for ownership (like consume), which can only be called once. Reaching a field with . or calling a method also implicitly borrows when needed.

Next Step

This lesson measured references lent out over a value’s entirety — &Trace or &mut Trace, always the whole value. The next lesson applies the same access-without-ownership principle to only a part of a value: how can we view a specific range of a string or a growable array without ever ending the source? All the rules of borrowing measured here (the source does not end, the life is not extended, the shared/mutable distinction) will stay exactly the same; only the size of the viewed part changes.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close