Skip to content
academia.sh

Lesson 06 / 14

The Lifetime Concept

Establishes that the source range over which a reference is valid is not a runtime object, only a range the compiler checks. The range shown to be checked in the Borrow Checker lesson is given a name here, and it is measured that two independent references' ranges need not be nested.

Published Updated

In this lesson

  • Defines a lifetime as the source range over which a reference is valid
  • Runs a slice of the measurement core to show that a reference's usage range stays within its owner's life
  • Runs a measurement to show that two independent references' validity ranges need not be nested
Contents

The Borrow Checker lesson showed that it was checking something, but it never named that something: the checker was controlling a range tied to how long a reference could be used. This lesson gives that range a name and applies the course’s question (“where does this value’s life end”) to a reference: where does a reference’s own validity range start and end, and where is that range read from?

The question asked by the Go curriculum’s Concurrency course was different — there, what was measured was when a running unit would stop, a stop triggered by an outside decision (a channel closing, a context being canceled). What is measured here is not a unit of work; it is the range in the source over which a reference can be used. The Borrowing lesson had already shown this range’s consequence: when a reference’s scope ended, it did not end anything itself — the value it borrowed was still alive. This lesson goes down to that consequence’s cause — whether the reference itself has a named range of its own, and if so, how that range is determined.

A Range’s Name

In Rust, a lifetime is the name of the source range over which a reference is considered valid. This range has two ends: the point where the reference is created and the point where it is last used. A lifetime defines the distance between these two points, and the compiler makes sure a reference is never used outside that range. Without this check, a reference could still be held after the value it pointed to had ended, and a read could be attempted through it; the result would be an undefined read into memory that was no longer valid. Rust never leaves this to runtime: a range violation is caught at compile time, before the program has run even once.

There is one crucial distinction to establish here: a lifetime is not a runtime object. Nowhere during the program’s run is there a counter, a timestamp, or a record stored under the name “this reference’s lifetime.” The Trace type measured in the previous lesson implemented the Drop trait and wrote to the ledger when a value ended — but that was the end of an owned value. A reference (&Trace) does not implement Drop; the reference itself never drops into the ledger, because its “ending” is not an event that happens at runtime but a boundary verified at compile time. A lifetime is read from the block structure in the source; once the run is over, no trace of it remains.

LI1. A lifetime is not a value; it is the name of an inference the compiler carries out over the source — it is stored nowhere while the program itself is running. It fits none of the four end classes measured in the course’s earlier lessons: a lifetime is not the end of a value but the range over which a reference is considered valid; what is measured is not an event but a source range.

Once established, this boundary will be used again and again through the rest of the course: every new mechanism (explicit annotation, elision rule, struct field, heap-allocated single ownership, shared ownership) will approach the same question from a different angle, but all of them will rest on the same foundation — the fact that a reference cannot outlive its owner.

When it needs to, Rust itself writes a lifetime down as a short name starting with an apostrophe ('a, 'b); this naming is not a value, only a way of telling the compiler and the reader “this reference and that reference are tied to the same range.” When these names need to be written and what they express is the subject of the next lesson; the only thing that matters here is that a range having a name is not what makes that range exist at runtime.

The Difference Between Scope and Lifetime

The Programming Fundamentals course established the concept of scope: the block in which a name appears, and the point past which it becomes unreachable once that block ends. A lifetime is related to this concept but is not the same thing. Scope is a syntactic boundary — it is drawn by a pair of { and } in the source and covers an entire block. A lifetime, by contrast, is a usage range: the range over which a reference actually has to be valid is not the whole block it sits in, only the segment from the point it is created to the point it is last used.

The measurement below makes this distinction concrete: the references r1, r2, and r3 are created inside a scope but do not fill that scope entirely — after the line where they are last used, several more lines of code still run before the scope closes, and those lines never touch the references again. A reference’s validity range runs from where it is created to where it is last used; the rest of the scope still matters for the three values’ ownership (all three stay alive until the scope closes), but it no longer matters for the references. This is why a lifetime is named as a separate concept: scope alone is not enough to say how long a reference stays safe.

This difference cuts both ways: although a reference’s range does not have to fill the scope entirely, it also cannot spill outside it. A reference can never keep being used after it has left the block it belongs to — a validity range is a subset of scope, not a line independent of scope and fully detached from it. The explicit lifetime annotations seen in later lessons do not break this relationship either; they only make a bond between several references’ ranges visible in the source when such a bond exists.

Measurement: A Reference’s Range Stays Within Its Owner’s Life

Part A of the measurement core in the Stack and Heap lesson had shown that three values (scope-1, scope-2, scope-3) are set up in the same scope and end in the reverse order of their declaration once the scope closes. The same slice is run again here, this time with references taken: a reference is taken to each of the three values while still inside the scope, a read is made through those references, and the ledger is checked at the moment of that read.

// reference_range.rs — a slice of life.rs's part A: three values, read through references.

use std::cell::RefCell;
use std::rc::Rc;

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

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

impl Trace {
    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());
    }
}

fn read(ledger: &Ledger) -> Vec<String> {
    ledger.borrow().clone()
}

fn newly_added(ledger: &Ledger, before: usize) -> String {
    let all = read(ledger);
    all[before..].join(", ")
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));

    let a0 = read(&ledger).len();
    {
        let scope_1 = Trace::new("scope-1", &ledger);
        let scope_2 = Trace::new("scope-2", &ledger);
        let scope_3 = Trace::new("scope-3", &ledger);

        let r1 = &scope_1;
        let r2 = &scope_2;
        let r3 = &scope_3;
        println!("read through references -> {}, {}, {}", r1.name, r2.name, r3.name);
        println!("ledger at read time      -> [{}]", newly_added(&ledger, a0));

        // The references' last use was on the line above; none of them appear below, but
        // the three values keep living as owners until the scope itself closes.
        let still_empty = newly_added(&ledger, a0).is_empty();
        println!("ledger still empty while scope open -> {}", still_empty);
    }
    println!("once the scope closes   -> {}", newly_added(&ledger, a0));
}
read through references -> scope-1, scope-2, scope-3
ledger at read time      -> []
ledger still empty while scope open -> true
once the scope closes   -> scope-3, scope-2, scope-1

The references r1, r2, and r3 are created and read at the same point, and the ledger is still empty at that moment — all three values are still alive, because the scope has not yet closed. The references’ last use is on that very same line: they are never used again. The line that follows touches none of the references and confirms the ledger is still empty — all three values keep living as owners, even after the references have finished their work, right up to the scope’s own end. Once the scope closes they end in the reverse order of their declaration — the rule established in the Stack and Heap lesson holds here too. The references’ own validity range sits inside the life of these three bindings; it does not spill outside it, but it does not have to fill the entire life of the bindings either.

LI2. A reference’s validity range is a sub-range of the life of the value it refers to — a reference can never be used past the point where its owner ends. Code that crosses this boundary is rejected by the compiler; the concrete form of that rejection was shown running in the Borrow Checker lesson.

Suppose the measurement above tried to move a reference like r1 outside the inner scope: say r1 were assigned to a variable defined in the outer scope, or returned from a function. That code would not compile, because scope_1, the value r1 is tied to, ends when the inner scope closes, and r1 would then be held somewhere that kept living past that point — a reference outliving its own owner is exactly what a lifetime does not allow. The run above was accepted because all three references stayed inside their own inner scope; the difference between what is accepted and what is rejected is whether the reference’s point of use falls inside or outside its owner’s scope.

Limiting Measurement: Ranges Need Not Be Nested

Textbooks often draw lifetimes as nested boxes, and that picture can leave a false impression: as if every reference’s range had to nest inside some outer range. In reality, two independent references’ ranges need not have any relationship to each other at all — they do not even need to overlap.

// independent_ranges.rs — two references' validity ranges never intersect.

use std::cell::RefCell;
use std::rc::Rc;

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

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

impl Trace {
    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());
    }
}

fn read(ledger: &Ledger) -> Vec<String> {
    ledger.borrow().clone()
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));

    let trace_a = Trace::new("independent-a", &ledger);
    let trace_b = Trace::new("independent-b", &ledger);

    {
        let r = &trace_a;
        println!("read through a -> {}", r.name);
    }
    println!("ledger after reading a -> [{}]", read(&ledger).join(", "));

    {
        let r = &trace_b;
        println!("read through b -> {}", r.name);
    }
    println!("ledger after reading b -> [{}]", read(&ledger).join(", "));
}
read through a -> independent-a
ledger after reading a -> []
read through b -> independent-b
ledger after reading b -> []

trace_a and trace_b live together in the same outer scope and both end only at the very end of the program — the ledger stays empty after both reads, because neither has ended yet. But the picture is different for the references: the reference taken to trace_a is created in its own inner block and used there, and its validity ends once that block closes; the reference taken to trace_b is created afterward, in a completely separate block. The two references’ validity ranges never overlap in time at all — one closes fully before the other opens. The compiler never has to compare these two ranges at any point, because no relationship was ever established between them.

LI3. Two references’ validity ranges do not have to be nested or to intersect; ranges that are fully independent of each other, even ones that never overlap in time, can sit side by side in the same program. As long as a function signature does not establish a relationship between two references, those two ranges stay independent for the compiler too — when that relationship needs to be established is the subject of the next lesson.

The practical upshot of this independence is that dozens of references can be alive at once in a program, and most of them have no relationship to each other at all — each exists in its own independent range, tied to its own owner. The compiler only compares ranges that are genuinely related: when one reference depends on another reference’s data, or when a function establishes a bond between two references. Two unrelated references never enter that comparison, just as in the trace_a and trace_b example above. This also explains why the explicit syntax seen in the next lesson only appears when it is needed: if there is no relationship, there is nothing to write.

Reading the Range from the Source

The two measurements above share one feature: neither of them printed a lifetime directly. The only thing printed was when the owned values ended — a reference’s own range never appeared in any output, because there is no runtime counterpart for it to appear as. The only way to learn a reference’s range is to read the source: r1’s range runs from the line let r1 = &scope_1; to the print call where r1.name is used; there is no separate tool to run either before or after that.

This continues the pattern established in the course’s earlier lessons (the expression/statement/item classification, the four end classes): every measurement produces a conclusion drawn from the source’s own structure, not a number or an ordered record left over once the run is done. For a lifetime, this means information that never appears in the output but is always visible in the source.

The same reading method holds in the second measurement: to find the range of the reference on trace_a, there is no need to look at the ledger or at any other output — in the source, the inner block from let r = &trace_a; to the line where r.name is used is the whole of that range. The range on trace_b is bounded the same way, by its own inner block. The lines that fall between the two references’ ranges — after the first block closes, before the second block opens — belong to neither range, and that too is understood only by reading the source. No runner or extra tool contributes anything to this conclusion; the measurement core exists here only to make concrete a reading that was already correct.

Summary

  • A lifetime is the name of the source range over which a reference is considered valid, and it is not a runtime object — only an inference the compiler carries out over the source.
  • A reference does not implement Drop and never drops into the ledger; only owned values do; an owned value’s life ends at scope end.
  • A reference’s validity range is a sub-range of the life of the value it refers to, and this range cannot spill past its owner’s end.
  • Two independent references’ validity ranges need not be nested or intersect.
  • A reference’s range never appears in any output; its only source is where in the source code the reference is created and last used.

Next Step

This lesson showed that a reference’s range stays within its owner’s life, that this range leaves no trace at runtime, and that two independent ranges may enter no relationship with each other at all. But some functions have to establish exactly that kind of relationship: when a function is written that takes multiple references and returns one of them, which input the returned reference is tied to cannot be inferred from the source alone, and the compiler does not accept that ambiguity. The next lesson measures how the range is written explicitly in that case, what information this writing adds to the source, and what happens when what is written does not actually hold.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close