Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ use-boolean-and = true
"generics/trait-objects.html" = "../traits/trait-objects.html"
"hello-world/basic-syntax/functions-interlude.html" = "../../control-flow-basics/functions.html"
"hello-world/hello-world.html" = "../types-and-values/hello-world.html"
"lifetimes/lifetime-annotations.html" = "../lifetimes.html"
"memory-management/manual.html" = "../memory-management/approaches.html"
"memory-management/rust.html" = "../memory-management/ownership.html"
"memory-management/scope-based.html" = "../memory-management/approaches.html"
Expand Down
6 changes: 5 additions & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,11 @@
- [Exercise: Health Statistics](borrowing/exercise.md)
- [Solution](borrowing/solution.md)
- [Lifetimes](lifetimes.md)
- [Lifetime Annotations](lifetimes/lifetime-annotations.md)
- [Borrowing and Functions](lifetimes/simple-borrows.md)
- [Returning Borrows](lifetimes/returning-borrows.md)
- [Multiple Borrows](lifetimes/multiple-borrows.md)
- [Borrow Both](lifetimes/borrow-both.md)
- [Borrow One](lifetimes/borrow-one.md)
- [Lifetime Elision](lifetimes/lifetime-elision.md)
- [Lifetimes in Data Structures](lifetimes/struct-lifetimes.md)
- [Exercise: Protobuf Parsing](lifetimes/exercise.md)
Expand Down
46 changes: 46 additions & 0 deletions src/lifetimes/borrow-both.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
minutes: 5
---

# Borrow Both

In this case, we have a function where either `a` or `b` may be returned. In
this case we use the lifetime annotations to tell the compiler that both borrows
may flow into the return value.

```rust
fn pick<'a>(c: bool, a: &'a i32, b: &'a i32) -> &'a i32 {
if c { a } else { b }
}

fn main() {
let mut a = 5;
let mut b = 10;

let r = pick(true, &a, &b);

// Which one is still borrowed?
// Should either mutation be allowed?
// a += 7;
// b += 7;

dbg!(r);
}
```

<details>

- The `pick` function will return either `a` or `b` depending on the value of
`c`, which means we can't know at compile time which one will be returned.

- To express this to the compiler, we use the same lifetime for both `a` and
`b`, along with the return type. This means that the returned reference will
borrow BOTH `a` and `b`!

- Uncomment both of the commented lines and show that `r` is borrowing both `a`
and `b`, even though at runtime it will only point to one of them.

- Change the first argument to `pick` to show that the result is the same
regardless of if `a` or `b` is returned.

</details>
77 changes: 77 additions & 0 deletions src/lifetimes/borrow-one.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
minutes: 5
---

# Borrow One

In this example `find_nearest` takes in multiple borrows but returns only one of
them. The lifetime annotations explicitly tie the returned borrow to the
corresponding argument borrow.

```rust,editable
#[derive(Debug)]
struct Point(i32, i32);

/// Searches `points` for the point closest to `query`.
/// Assumes there's at least one point in `points`.
fn find_nearest<'a>(points: &'a [Point], query: &Point) -> &'a Point {
fn cab_distance(p1: &Point, p2: &Point) -> i32 {
(p1.0 - p2.0).abs() + (p1.1 - p2.1).abs()
}

let mut nearest = None;
for p in points {
if let Some((_, nearest_dist)) = nearest {
let dist = cab_distance(p, query);
if dist < nearest_dist {
nearest = Some((p, dist));
}
} else {
nearest = Some((p, cab_distance(p, query)));
};
}

nearest.map(|(p, _)| p).unwrap()
// query // What happens if we do this instead?
}

fn main() {
let points = &[Point(1, 0), Point(1, 0), Point(-1, 0), Point(0, -1)];
let query = Point(0, 2);
let nearest = find_nearest(points, &query);

// `query` isn't borrowed at this point.
drop(query);

dbg!(nearest);
}
```

<details>

- It may be helpful to collapse the definition of `find_nearest` to put more
focus on the signature of the function. The actual logic in the function is
somewhat complex and isn't important for the purpose of borrow analysis.

- When we call `find_nearest` the returned reference doesn't borrow `query`, and
so we are free to drop it while `nearest` is still active.

- But what happens if we return the wrong borrow? Change the last line of
`find_nearest` to return `query` instead. Show the compiler error to the
students.

- The first thing we have to do is add a lifetime annotation to `query`. Show
students that we can add a second lifetime `'b` to `find_nearest`.

- Show the new error to the students. The borrow checker verifies that the logic
in the function body actually returns a reference with the correct lifetime,
enforcing that the function adheres to the contract set by the function's
signature.

- The "help" note in the error notes that we can add a lifetime bound `'b: 'a`
to say that `'b` will live at least as long as `'a`, which would then allow us
to return `query`. On the next slide we'll talk about lifetime variance, which
is the rule that allows us to return a longer lifetime when a shorter one is
expected.

</details>
64 changes: 0 additions & 64 deletions src/lifetimes/lifetime-annotations.md

This file was deleted.

59 changes: 14 additions & 45 deletions src/lifetimes/lifetime-elision.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,60 +16,29 @@ This is not inference -- it is just a syntactic shorthand.
that lifetime is given to all un-annotated return values.

```rust,editable
#[derive(Debug)]
struct Point(i32, i32);

fn cab_distance(p1: &Point, p2: &Point) -> i32 {
(p1.0 - p2.0).abs() + (p1.1 - p2.1).abs()
fn only_args(a: &i32, b: &i32) {
todo!();
}

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 {
let dist = cab_distance(p, query);
if dist < nearest_dist {
nearest = Some((p, dist));
}
} else {
nearest = Some((p, cab_distance(p, query)));
};
}
nearest.map(|(p, _)| p)
fn identity(a: &i32) -> &i32 {
a
}

fn main() {
let points = &[Point(1, 0), Point(1, 0), Point(-1, 0), Point(0, -1)];
let nearest = {
let query = Point(0, 2);
find_nearest(points, &query)
};
println!("{:?}", nearest);
struct Foo(i32);
impl Foo {
fn get(&self, other: &i32) -> &i32 {
&self.0
}
}
```

<details>

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. In `main`, the
return value is allowed to outlive the query.

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

```rust,ignore
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
by the compiler. Note that this is not the case for raw pointers (unsafe), and
this is a common source of errors with unsafe Rust.
- Walk through applying the lifetime elision rules to each of the example
functions. `only_args` is completed by the first rule, `identity` is completed
by the second, and `Foo::get` is completed by the third.

Students may ask when to use lifetimes. Rust borrows _always_ have lifetimes.
Most of the time, elision and type inference mean these don't need to be written
out. In more complicated cases, lifetime annotations can help resolve ambiguity.
Often, especially when prototyping, it's easier to just work with owned data by
cloning values where necessary.
- If all lifetimes have not been filled in by applying the three elision rules
then you will get a compiler error telling you to add annotations manually.

</details>
54 changes: 54 additions & 0 deletions src/lifetimes/multiple-borrows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
minutes: 5
---

# Multiple Borrows

But what about when there are multiple borrows passed into a function and one
being returned?

```rust,editable,ignore
fn multiple(a: &i32, b: &i32) -> &i32 {
todo!("Return either `a` or `b`")
}

fn main() {
let mut a = 5;
let mut b = 10;

let r = multiple(&a, &b);

// Which one is still borrowed?
// Should either mutation be allowed?
a += 7;
b += 7;

dbg!(r);
}
```

<details>

- This code does not compile right now because it is missing lifetime
annotations. Before we get it to compile, use this opportunity to have
students to think about which of our argument borrows should be extended by
the return value.

- We pass two borrows into `multiple` and one is going to come back out, which
means we will need to extend the borrow of one of the argument lifetimes.
Which one should be extended? Do we need to see the body of `multiple` to
figure this out?

- When borrow checking, the compiler doesn't look at the body of `multiple` to
reason about the borrows flowing out, instead it looks only at the signature
of the function for borrow analysis.

- In this case there is not enough information to determine if `a` or `b` will
be borrowed by the returned reference. Show students the compiler errors and
introduce the lifetime syntax:

```rust,ignore
fn multiple<'a>(a: &'a i32, b: &'a i32) -> &'a i32 { ... }
```

</details>
38 changes: 38 additions & 0 deletions src/lifetimes/returning-borrows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
minutes: 5
---

# Returning Borrows

But we can also have our function return a reference! This means that a borrow
flows back out of a function:

```rust,editable
fn identity(x: &i32) -> &i32 {
x
}

fn main() {
let mut x = 123;

let out = identity(&x);

// x = 5; // 🛠️❌ `x` is still borrowed!

dbg!(out);
}
```

<details>

- Rust functions can return references, meaning that a borrow can flow back out
of a function.

- If a function returns a reference (or another kind of borrow), it was likely
derived from one of its arguments. This means that the return value of the
function will extend the borrow for one or more argument borrows.

- This case is still fairly simple, in that only one borrow is passed into the
function, so the returned borrow has to be the same one.

</details>
Loading
Loading