Skip to content

Commit

Permalink
Improve explanation of lifetimes (google#2584)
Browse files Browse the repository at this point in the history
This approach seems to balance formalism with understanding. That is, it
doesn't mention contravariance, but suggests that lifetime annotations
in parameters and return values mean "opposite" things. It also
leverages the understanding that types must be specified in function
signatures, and are used to check types in the function body and at call
sites.
  • Loading branch information
djmitche authored and michael-kerscher committed Feb 7, 2025
1 parent ff7dde4 commit 9c47da4
Show file tree
Hide file tree
Showing 3 changed files with 22 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@
- [Lifetimes](lifetimes.md)
- [Lifetime Annotations](lifetimes/lifetime-annotations.md)
- [Lifetime Elision](lifetimes/lifetime-elision.md)
- [Struct Lifetimes](lifetimes/struct-lifetimes.md)
- [Lifetimes in Data Structures](lifetimes/struct-lifetimes.md)
- [Exercise: Protobuf Parsing](lifetimes/exercise.md)
- [Solution](lifetimes/solution.md)

Expand Down
20 changes: 12 additions & 8 deletions src/lifetimes/lifetime-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ also be explicit: `&'a Point`, `&'document str`. Lifetimes start with `'` and
`'a` is a typical default name. Read `&'a Point` as "a borrowed `Point` which is
valid for at least the lifetime `a`".

Lifetimes are always inferred by the compiler: you cannot assign a lifetime
yourself. Explicit lifetime annotations create constraints where there is
ambiguity; the compiler verifies that there is a valid solution.
Only ownership, not lifetime annotations, control when values are destroyed and
determine the concrete lifetime of a given value. The borrow checker just
validates that borrows never extend beyond the concrete lifetime of the value.

Lifetimes become more complicated when considering passing values to and
returning values from functions.
Explicit lifetime annotations, like types, are required on function signatures
(but can be elided in common cases). These provide information for inference at
callsites and within the function body, helping the borrow checker to do its
job.

<!-- The multi-line formatting by rustfmt in left_most is apparently
intentional: https://github.com/rust-lang/rustfmt/issues/1908 -->
Expand Down Expand Up @@ -56,9 +58,11 @@ Add `'a` appropriately to `left_most`:
fn left_most<'a>(p1: &'a Point, p2: &'a Point) -> &'a Point {
```

This says, "given p1 and p2 which both outlive `'a`, the return value lives for
at least `'a`.
This says there is some lifetime `'a` which both `p1` and `p2` outlive, and
which outlives the return value. The borrow checker verifies this within the
function body, and uses this information in `main` to determine a lifetime for
`p3`.

In common cases, lifetimes can be elided, as described on the next slide.
Try dropping `p2` in `main` before printing `p3`.

</details>
13 changes: 9 additions & 4 deletions src/lifetimes/lifetime-elision.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fn cab_distance(p1: &Point, p2: &Point) -> i32 {
(p1.0 - p2.0).abs() + (p1.1 - p2.1).abs()
}
fn nearest<'a>(points: &'a [Point], query: &Point) -> Option<&'a Point> {
fn find_nearest<'a>(points: &'a [Point], query: &Point) -> Option<&'a Point> {
let mut nearest = None;
for p in points {
if let Some((_, nearest_dist)) = nearest {
Expand All @@ -40,7 +40,11 @@ fn nearest<'a>(points: &'a [Point], query: &Point) -> Option<&'a Point> {
fn main() {
let points = &[Point(1, 0), Point(1, 0), Point(-1, 0), Point(0, -1)];
println!("{:?}", nearest(points, &Point(0, 2)));
let nearest = {
let query = Point(0, 2);
find_nearest(points, &Point(0, 2))
};
println!("{:?}", nearest);
}
```

Expand All @@ -49,12 +53,13 @@ fn main() {
In this example, `cab_distance` is trivially elided.

The `nearest` function provides another example of a function with multiple
references in its arguments that requires explicit annotation.
references in its arguments that requires explicit annotation. In `main`, the
return value is allowed to outlive the query.

Try adjusting the signature to "lie" about the lifetimes returned:

```rust,ignore
fn nearest<'a, 'q>(points: &'a [Point], query: &'q Point) -> Option<&'q Point> {
fn find_nearest<'a, 'q>(points: &'a [Point], query: &'q Point) -> Option<&'q Point> {
```

This won't compile, demonstrating that the annotations are checked for validity
Expand Down

0 comments on commit 9c47da4

Please sign in to comment.