Intermediate Level
Lifetimes
Chapter 13: Lifetimes ā³
Understand how Rust manages memory through lifetimes to prevent dangling references.
Understanding Lifetimes
The Borrow Checker
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+This code won't compile because r references x which goes out of scope before r is used.
Lifetime Annotations
Basic Syntax
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let string2 = String::from("xyz");
let result = longest(&string1, &string2);
println!("The longest string is: {}", result);
}Multiple Lifetime Parameters
fn first_word<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
let words: Vec<&str> = x.split_whitespace().collect();
words[0]
}
fn main() {
let string1 = String::from("Hello world");
let string2 = String::from("Rust programming");
let result = first_word(&string1, &string2);
println!("First word: {}", result);
}Lifetime Annotations in Structs
Structs with References
struct ImportantExcerpt<'a> {
part: &'a str,
}
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!("Excerpt: {}", i.part);
}Lifetime Elision
Rules for Automatic Lifetime Inference
// Rule 1: Each parameter that is a reference gets its own lifetime parameter
fn first_word(s: &str) -> &str { // Actually: fn first_word<'a>(s: &'a str) -> &'a str
let words: Vec<&str> = s.split_whitespace().collect();
words[0]
}
// Rule 2: If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
fn return_input(x: &str) -> &str { // Actually: fn return_input<'a>(x: &'a str) -> &'a str
x
}
// Rule 3: If there are multiple input lifetime parameters, but one of them is &self or &mut self,
// the lifetime of self is assigned to all output lifetime parameters
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 { // Actually: fn level<'a>(&'a self) -> i32
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
// Actually: fn announce_and_return_part<'a, 'b>(&'a self, announcement: &'b str) -> &'a str
println!("Attention please: {}", announcement);
self.part
}
}Static Lifetimes
References that Live for the Entire Program
let s: &'static str = "I have a static lifetime.";
// Static lifetimes are often used for string literals
// because they're stored in the program's binaryGeneric Lifetimes with Traits
Combining Lifetimes and Traits
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 string1 = String::from("long string is long");
let string2 = String::from("xyz");
let result = longest_with_an_announcement(
&string1,
&string2,
"Today is someone's birthday!"
);
println!("The longest string is: {}", result);
}Lifetime Annotations in Methods
Implementing Methods with Lifetimes
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn new(part: &'a str) -> ImportantExcerpt<'a> {
ImportantExcerpt { part }
}
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
fn part(&self) -> &str {
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let excerpt = ImportantExcerpt::new(first_sentence);
println!("Level: {}", excerpt.level());
println!("Part: {}", excerpt.part());
println!("Announcement: {}", excerpt.announce_and_return_part("Chapter 1"));
}Practical Examples
Example 1: Config Parser with Lifetimes
struct Config<'a> {
name: &'a str,
value: &'a str,
}
impl<'a> Config<'a> {
fn new(name: &'a str, value: &'a str) -> Config<'a> {
Config { name, value }
}
fn get_name(&self) -> &str {
self.name
}
fn get_value(&self) -> &str {
self.value
}
fn display(&self) {
println!("{}: {}", self.name, self.value);
}
}
fn parse_config_line<'a>(line: &'a str) -> Option<Config<'a>> {
let parts: Vec<&str> = line.split('=').collect();
if parts.len() == 2 {
Some(Config::new(parts[0].trim(), parts[1].trim()))
} else {
None
}
}
fn main() {
let config_line = "database_url = postgresql://localhost:5432/mydb";
if let Some(config) = parse_config_line(config_line) {
config.display();
println!("Name: {}", config.get_name());
println!("Value: {}", config.get_value());
}
}Example 2: Text Processing with Lifetimes
struct TextProcessor<'a> {
text: &'a str,
processed_lines: Vec<&'a str>,
}
impl<'a> TextProcessor<'a> {
fn new(text: &'a str) -> TextProcessor<'a> {
TextProcessor {
text,
processed_lines: Vec::new(),
}
}
fn split_lines(&mut self) -> &Vec<&'a str> {
self.processed_lines = self.text.lines().collect();
&self.processed_lines
}
fn find_line_containing(&self, pattern: &str) -> Option<&'a str> {
self.processed_lines.iter()
.find(|line| line.contains(pattern))
.copied()
}
fn get_longest_line(&self) -> &'a str {
self.processed_lines.iter()
.max_by_key(|line| line.len())
.unwrap_or(&"")
}
}
fn main() {
let text = "Rust is a systems programming language
It's known for memory safety and performance
Rust prevents segfaults and guarantees thread safety";
let mut processor = TextProcessor::new(text);
processor.split_lines();
if let Some(line) = processor.find_line_containing("memory") {
println!("Found line: {}", line);
}
println!("Longest line: {}", processor.get_longest_line());
}Common Mistakes
ā Forgetting Lifetime Annotations
// This won't compile without lifetime annotations
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}ā Adding Proper Lifetime Annotations
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}ā Returning References to Local Variables
fn bad_function<'a>() -> &'a str {
let s = String::from("Hello");
&s // Error: s doesn't live long enough
}ā Returning Static References or Owned Values
fn good_function_static() -> &'static str {
"Hello" // String literals have static lifetime
}
fn good_function_owned() -> String {
String::from("Hello") // Return owned value instead
}Key Takeaways
- ā Lifetimes ensure references are valid for as long as needed
- ā The borrow checker prevents dangling references at compile time
- ā Lifetime annotations specify how long references live
- ā Most lifetime annotations can be inferred through elision rules
- ā
Static lifetimes (
'static) live for the entire program duration - ā Structs with references must have lifetime annotations
- ā Methods can have lifetime annotations in their signatures
- ā
Follow Rust naming conventions (lifetime names start with
'and are typically lowercase)
Ready for Chapter 14? ā Testing