diff --git a/book.toml b/book.toml index 948de554297e..825142ce8e0d 100644 --- a/book.toml +++ b/book.toml @@ -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" diff --git a/src/SUMMARY.md b/src/SUMMARY.md index f8941f81cd1a..d25e9366a669 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) diff --git a/src/lifetimes/borrow-both.md b/src/lifetimes/borrow-both.md new file mode 100644 index 000000000000..4191f0886e96 --- /dev/null +++ b/src/lifetimes/borrow-both.md @@ -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); +} +``` + +
+ +- 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. + +
diff --git a/src/lifetimes/borrow-one.md b/src/lifetimes/borrow-one.md new file mode 100644 index 000000000000..ae9fe679821c --- /dev/null +++ b/src/lifetimes/borrow-one.md @@ -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); +} +``` + +
+ +- 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. + +
diff --git a/src/lifetimes/lifetime-annotations.md b/src/lifetimes/lifetime-annotations.md deleted file mode 100644 index 35b88bc94977..000000000000 --- a/src/lifetimes/lifetime-annotations.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -minutes: 10 ---- - -# Lifetime Annotations - -A reference has a _lifetime_, which must not "outlive" the value it refers to. -This is verified by the borrow checker. - -The lifetime can be implicit - this is what we have seen so far. Lifetimes can -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`". - -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. - -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. - - - -```rust,editable,compile_fail -#[derive(Debug)] -struct Point(i32, i32); - -fn left_most(p1: &Point, p2: &Point) -> &Point { - if p1.0 < p2.0 { p1 } else { p2 } -} - -fn main() { - let p1 = Point(10, 10); - let p2 = Point(20, 20); - let p3 = left_most(&p1, &p2); // What is the lifetime of p3? - dbg!(p3); -} -``` - -
- -In this example, the compiler does not know what lifetime to infer for `p3`. -Looking inside the function body shows that it can only safely assume that -`p3`'s lifetime is the shorter of `p1` and `p2`. But just like types, Rust -requires explicit annotations of lifetimes on function arguments and return -values. - -Add `'a` appropriately to `left_most`: - -```rust,ignore -fn left_most<'a>(p1: &'a Point, p2: &'a Point) -> &'a Point { -``` - -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`. - -Try dropping `p2` in `main` before printing `p3`. - -
diff --git a/src/lifetimes/lifetime-elision.md b/src/lifetimes/lifetime-elision.md index 539a6475f029..5094724e9c3d 100644 --- a/src/lifetimes/lifetime-elision.md +++ b/src/lifetimes/lifetime-elision.md @@ -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 + } } ```
-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.
diff --git a/src/lifetimes/multiple-borrows.md b/src/lifetimes/multiple-borrows.md new file mode 100644 index 000000000000..8d1e4680f79b --- /dev/null +++ b/src/lifetimes/multiple-borrows.md @@ -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); +} +``` + +
+ +- 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 { ... } + ``` + +
diff --git a/src/lifetimes/returning-borrows.md b/src/lifetimes/returning-borrows.md new file mode 100644 index 000000000000..083ca9a4b18e --- /dev/null +++ b/src/lifetimes/returning-borrows.md @@ -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); +} +``` + +
+ +- 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. + +
diff --git a/src/lifetimes/simple-borrows.md b/src/lifetimes/simple-borrows.md new file mode 100644 index 000000000000..b1f81b634693 --- /dev/null +++ b/src/lifetimes/simple-borrows.md @@ -0,0 +1,33 @@ +--- +minutes: 3 +--- + +# Borrowing with Functions + +As part of borrow checking, the compiler needs to reason about how borrows flow +into and out of functions. In the simplest case borrows last for the duration of +the function call: + +```rust,editable +fn borrows(x: &i32) { + dbg!(x); +} + +fn main() { + let mut val = 123; + + // Borrow `val` for the function call. + borrows(&val); + + // Borrow has ended and we're free to mutate. + val += 5; +} +``` + +
+ +- In this example we borrow `val` for the call to `borrows`. This would limit + our ability to mutate `val`, but once the function call returns the borrow has + ended and we're free to mutate again. + +