Rust Non-Lexical Lifetimes
Non-lexical lifetimes are lifetimes measured by use, not by block. A borrow stays live from its first use to its last use.
The old way: a borrow filled its whole block
Before 2018, this shape was refused. The borrow of data lasted until the end of the block, even though slice is never used after the call:
let slice = &mut data[..];
capitalize(slice);
data.push('d'); // refused before 2018: the block was not overNow: a borrow ends at its last use
The same program compiles today. The borrow is measured by where the reference is used, not by where the block ends:
let mut names = vec![String::from("ana"), String::from("bo")];
let first = &names[0];
println!("first = {}", first);
names.push(String::from("new"));
println!("count = {}", names.len());A borrow can live in one match arm only
Here the borrow of the map exists only in the Some arm. In the None arm there is no borrow at all, so the map itself may be used:
match counts.get_mut("apples") {
Some(n) => *n += 1,
None => { counts.insert("apples", 1); }
}Walking a &mut down a list
This linked-list walk borrows one node's value, then moves the &mut on to the next node. Early Rust refused the assignment, with error E0506. It compiles now, because the borrowed value sits behind the reference, not inside the variable being reassigned:
result.push(&mut list.value);
if let Some(n) = list.next.as_mut() {
list = n;
}Errors point at the later use
A modern borrow error shows its working. It names the first borrow and the second borrow. It also names the line where the first one is used again. That last line is the one to fix:
let a = &mut x[0];
let b = &mut x[1]; // error E0499: borrowed as mutable more than once
println!("{} {}", a, b); // ...and the error adds: first borrow later used hereExample
use std::collections::HashMap;
fn capitalize(data: &mut [char]) {
for c in data.iter_mut() {
c.make_ascii_uppercase();
}
}
fn main() {
let mut data = vec!['a', 'b', 'c'];
let slice = &mut data[..];
capitalize(slice);
data.push('d');
println!("{:?}", data);
let mut counts: HashMap<&str, i32> = HashMap::new();
counts.insert("apples", 3);
match counts.get_mut("apples") {
Some(n) => *n += 1,
None => { counts.insert("apples", 1); }
}
match counts.get_mut("pears") {
Some(n) => *n += 1,
None => { counts.insert("pears", 1); }
}
println!("apples = {} pears = {}", counts["apples"], counts["pears"]);
}Key points
- Non-lexical: a borrow's span is worked out from the flow of the program.
- It does not come from the { } block it sits in.
- Lexical would mean "by block".
- A borrow is live from the line that makes it.
- It stays live until the last place it may still be used.
- After that line, the value is free again.
- Two borrows of one value may follow each other inside one function.
- No extra block is needed between them.
- The first must finish before the second begins.
- Code in old books and posts sometimes wraps a borrow in its own { } block.
- The block ends the borrow early.
- That was a workaround for the older checker.
- It is rarely needed now.
- Rust has measured borrows this way since the 2018 edition.
In this example, two formerly-refused shapes compile side by side. First, slice borrows the whole of data mutably and capitalize shouts each letter. The borrow's last use is that call. So the very next line may push onto data itself. It prints ['A', 'B', 'C', 'd']. Below, each match borrows the map only inside its Some arm. apples is found and raised to 4. pears is missing. So the None arm inserts it as 1. No borrow exists in that arm. Before non-lexical lifetimes, both halves of this program needed restructuring to please the checker.
Note
The borrow rules themselves never changed — many readers OR one writer still holds.
Only the measuring became precise: the checker now watches uses, not blocks.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
In `let slice = &mut data[..]; capitalize(slice); data.push('d');`, why does the push compile on modern Rust?