Check out a free preview of the full The Rust Programming Language course
The "Lifetime Annotations" Lesson is part of the full, The Rust Programming Language course featured in this preview video. Here's what you'd learn in this lesson:
Richard demonstrates how to assign lifetime annotations to help keep track of when variables become out of scope. Lifetime annotations can be called anything, but must start with an apostrophe.
Transcript from the "Lifetime Annotations" Lesson
[00:00:00]
>> Now, what we could do is, we could add these things, these are lifetime annotations. And what this says is basically, I'm defining a new type parameter, but it's actually, sorry. Not a type parameter, a lifetime parameter that is giving a name to that lifetime. That highlighted section, we can now give it a name, and we can now refer to it in our code.
[00:00:20]
The name that I've chosen is 'a, because I just love the word 'a, just kidding. So the way that lifetime annotations work is that they always begin with an apostrophe. And because apostrophe's kind of a long word, we usually say tick instead of apostrophe. So the way that I would say this out loud is tick-a.
[00:00:38]
Now, like type parameters, where we saw previously with option, we chose capital T. Or with Rust, we chose capital O, sorry, with result, we chose capital O or capital E. You can choose any name you want for this. I could have said tick-a, I could have said tick-b or tick-foo or tick-hamburger.
[00:00:53]
Anything you wanna choose, you're totally free to name it whatever you want. And once you've chosen that name, you put it in these angle brackets in here. And then from then on, you can reference that same tick-a or whatever else inside your type annotation. Well, right now, I'm only referencing it once, so I could write this, this is legal Rust code to write.
[00:01:11]
But really, the whole point of defining this thing is, I wanna use it in multiple places, in order to explicitly encode a dependency like the one that we have bent this Releases struct and this years argument. So let's see, how can we make this lifetime parameter named 'a more useful?
[00:01:29]
Well, one way I can do this is, when I've got my eighties and nineties, I can reuse this inside here to say, you know what? I want to explicitly annotate and represent the fact that these things have to have the same lifetime. And we know we have the same lifetime, because the lifetime refers to the time between when an allocation happens and when a deallocation happens, of that same memory.
[00:01:50]
And all of these things, because they're all slices, they're all referencing the same memory, the same heap memory. And because of that, they all have the same lifetime as that heap memory. That's just kind of a rule of slices. Whenever you slice some heap memory, you have the same lifetime as that heap memory.
[00:02:04]
Whatever goes out of scope and gets deallocated, then the reference is no longer valid. Because if it were, we would have a use-after-free. So now I've made that relationship between them explicit in the code. This is saying, all three of these have the same lifetime. And I've happened to name that lifetime 'a, but I could have chosen a different name.
[00:02:21]
Okay, so now, here's a way that I can take that one step further and actually put it inside the struct. Here I've chosen 'y, again, could have chosen anything. But I chose a different name to illustrate the fact that you sort of choose it on a case-by-case basis.
[00:02:38]
But now I've said something different about this struct. I've introduced a lifetime parameter to this struct. And I've said, inside this struct, Releases now has a lifetime parameter called 'y. And inside that struct, it has three fields, and each of them have the same lifetime. So I'm essentially saying that whenever you make this release a struct, I don't care what the lifetime is that you're gonna put in for years, eighties, and nineties.
[00:03:05]
You can give whatever lifetime you want for those things, but all three of them have to have the same lifetime. Which means that either, most likely, they all need to be referencing the same exact heap memory, or they all need to be referencing heap memory allocations which happen to be, that are going to be deallocated at the same time.
[00:03:24]
They're going to live for the same amount of time. So putting that back together with our previous example, with all the 'a's, we can now incorporate this new concept of Releases into this. And we can do that like so. So we can say jazz takes 'a as its lifetime parameter.
[00:03:43]
Years now has a lifetime annotation of 'a, and Releases also does too. And now we've tied these two together syntactically. Now when I look at this, this is telling me more information than before. And it's saying, I'm taking this slice of i64s, and I'm returning a Releases struct.
[00:04:03]
And those slices inside that Releases struct all have the same lifetime as this years slice does itself. And that's sort of propagated through here by use of 'a's. And so basically, putting these things together, I now have a way to express inside code the actual relationship between these lifetimes.
[00:04:24]
Okay, so let's zoom back out and take a look our previous code, where we had the potential use-after-free if we didn't get a compiler error. So here we have our all_years vec, which is an i64, and it's got the same years inside of it. We're calling jazz, passing all_years.
[00:04:42]
And remember, now we've changed our jazz function so that it's taking an explicit lifetime here of 'a, and our Releases also has a lifetime of 'a. So now, the lifetime of all_years, which is the highlighted part in here, is going to be sort of threaded through this function through the argument, because the argument has to have that same lifetime as all_years, because that's what we're passing in.
[00:05:06]
And then it makes it all the way into the return value, such that now, when Releases comes back, we can see. This thing has to have a lifetime of 'a, and that means that this thing can only exist inside the highlighted part. Outside the highlighted part, it's outside of its lifetime, and there's going to be a use-after-free.
[00:05:24]
And so now, when we do reference something from Releases, knowing that Releases has this lifetime which has now already ended? We can actually see much more clearly that we are, in effect, already referring to all_years after its lifetime has ended by way of Releases. Because Releases has this same highlighted lifetime, as we can see by having tracked it through all these different steps of the function calls.
[00:05:48]
And yet we're still trying to use it outside of the highlighted area, after that lifetime which we've named 'a in this function has ended. And that's exactly why this thing is a use-after-free, and sort of the value of these lifetime annotations is helping us see this. So lifetime annotations are something that you don't necessarily need to use in your own code, depending on the code that you're writing.
[00:06:14]
But in some cases, they are required. One of the reasons that I use an example that has a struct is that, actually, [LAUGH], you do always have to put a lifetime annotation in a struct, if that struct is gonna hold on to references. So previously, when I had the example of Releases without a 'y inside of there?
[00:06:31]
That actually would not have compiled. Because whenever you have references inside of a struct, Rust will require that you put an explicit lifetime annotation inside your struct that corresponds to every single reference that you have inside of there. Now granted, you can have references with different lifetimes if you want.
[00:06:48]
I could say, this one's 'y, and this one's 'a, and this one's 'b, or something like that. And then I'd have to define 'y and 'b and 'c, or whatever names you wanna choose for them. But every reference inside of a struct has to have an explicit lifetime annotation.
[00:07:06]
The compiler will tell you to add them if you don't do them up-front. So that code I have previously, that didn't have this angle brackets with the tick inside of it would not have compiled, just because I didn't have that. It's the same thing with enums, same rule.
[00:07:18]
If you wanna have an enum with references inside of it, you have to reflect those in the type, and explicitly define their annotation. The other case where this can come up is, quote often, you'll find this in documentation. So this is one of the things that I think it's important to include the concept of an lifetimes in an introductory course.
[00:07:37]
Is when you're going out and trying to write real Rust programs, you're gonna see things like this in type annotations of packages that you'll use, or in the standard library, even. So it's pretty common to see these things sort of floating around. And now can kind of hopefully get a mental model for, what's the problem that these are solving, and how do I read this?
[00:07:56]
How do I read this 'a or this 'y, or whatever the case may be between these different things?
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops