Boom or Bust, 50 Days to Colorado Gold Rust: Day 14

Shawn Bachlet
4 min readAug 16, 2019

This article is the fifteenth part of a 51 part series, so if you haven’t read the previous parts of the series here, it is.

Here is a link to the beginning of the series if you want to start reading from there.

Lifetimes

Lifetimes are how Rust decides the scope for which a variable is valid. Rusts approach to lifetimes is unique among programming languages.

Lifetime’s core purpose is to help avoid dangling references. Dangling references are when one value is referring to another value that is now out of scope, which would cause a null pointer runtime error in most languages. However, in Rust, it will create a compile-time error.

fn main(){
let r;
{
let x = 5;
r = &x;
}
println!(“r: {}”, r);
}

Cause the following error to occur.

error[E0597]: `x` does not live long enough
→ src/main.rs:5:9
|
5 | r = &x;
| ^^^^^^ borrowed value does not live long enough
6 | }
| — `x` dropped here while still borrowed
7 | println!(“r: {}”, r);
| — borrow later used here

The error state that on line 5 we borrowed x, but by the time we used it again, its lifetime had already ended.

Rusts borrow checker is very intuitive. The borrow checker looks to see if the scope of the lifetime of the borrowed value contains the scope of the borrower. In the previous example, we can see the borrowed value x does not contain the lifetime of the borrower; in fact, the exact opposite happens! This error means that the borrow checker will flag our code and it will not compile.

fn main(){
let r;
{
let x = 5;
r = &x;
}
println!(“r: {}”, r);
}

The next example changes the code so that the lifetime of x (the borrowed value) fully contains the lifetime of r (the borrower), meaning that we don’t have a dangling pointer, and the code will compile.

fn main(){
let r;
{
let x = 5;
r = &x;
}
println!(“r: {}”, r);
}

Generic lifetime annotations are how we explicitly link the lifetimes of variables. Look at the following code.

fn main() {
let string1 = String::from(“abcd”);
let string2 = “xyz”;
let result = longest(string1.as_str(), string2);
println!(“The longest string is {}”, result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

When we try and compile it, we will see this error.

error[E0106]: missing lifetime specifier
→ src/main.rs:8:33
|
8 | fn longest(x: &str, y: &str) -> &str {
| ^ expected lifetime parameter
|
= help: this function’s return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`

It will not compile due to x and y having possibly different lifetimes. When longest returns the longer of two string references one of the references lifetimes could have ended.

fn longest<’a>(x: &’a str, y: &’a str) -> &’a str {
if x.len() > y.len() {
x
} else {
y
}
}

Annotating the lifetimes of both x and y to be equal means that, both values will be limited to whichever is the shorter lifetime of the two.
So this code will work.

fn main() {
let string1 = String::from(“long string is long”);
{
let string2 = String::from(“xyz”);
let result = longest(string1.as_str(), string2.as_str());
println!(“The longest string is {}”, result);
}
}

But, this code will not work.

fn main() {
let string1 = String::from(“abcd”);
let result;
{
let string2 = String::from(“xyz”);
result = longest(string1.as_str(), string2.as_str());
}
println!(“The longest string is {}”, result);
}

In the second example, even though it returns “string1”, “string2” is no longer in scope, so both values are invalid when returned from “longest.”

Rust allows for the use of lifetimes in Structs. But, when using a reference as the parameter to a struct. All the parameters which are references must have a lifetime annotation. Being annotated with a lifetime also means that a struct that holds a reference cannot outlive the reference that it holds.

So this will work.

fn main() {
let novel = String::from(“Call me Ishmael. Some years ago…”);
let first_sentence = novel.split(‘.’)
.next()
.expect(“Could not find a ‘.’”);
let i = ImportantExcerpt { part: first_sentence };
println!(“{:?}”, i);
}

But this will not.

fn main() {
let i;
{
let novel = String::from(“Call me Ishmael. Some years ago…”);
let first_sentence = novel.split(‘.’)
.next()
.expect(“Could not find a ‘.’”);
i = ImportantExcerpt { part: first_sentence }
}
println!(“{:?}”, i);
}

However, this will. I’ll let you hypothesize why.

fn main() {
let novel = String::from(“Call me Ishmael. Some years ago…”);
let i;
{
let first_sentence = novel.split(‘.’)
.next()
.expect(“Could not find a ‘.’”);
i = ImportantExcerpt { part: first_sentence }
}
println!(“{:?}”, i);
}

Summary

Over the last three posts, we have covered generics, traits, and lifetimes. To summarize all of these, let’s look at a function that uses all three in one go.

use std::fmt::Display;
fn longest_with_an_announcement<’a, T>(x: &’a str, y: &’a str, ann: T) -> &’a str
where T: Display {
println!(“Announcement! {}”, ann);
if x.len() > y.len() {
x
} else {
y
}
}

fn main() {
let x = “abc”;
let y = “wxyz”;
let announcement = 42;
let longest = longest_with_an_announcement(x, y, announcement);
println!(“{}”, longest)
}

--

--