Preface

You see lifetime annotations throughout rust code. This is a short post to briefly explain them.

Intro

Rust’s ownership memory model requires enforces single ownership of a variable with the ability to create references to that owned variable. These variables and their references are dropped at the end of their lifetime. This is enforced at compile time with the borrow checker.

This short blog only focuses on variable references and not owned values.

The default lifetime of a variable is the region (block/scope) that it is declared in. Aka. Between its parent curly braces. This begs the question, what if I want to return a reference from a function?

Explicit notation. You need to tell the compiler what lifetime to use.

fn explicit_lifetime<'a>(input: &'a i32) -> &'a i32 {
    input
}

By annotating the input and output reference with 'a we assign the existing lifetime of the input to the output.

Thats it! It’s really that simple.

Elision

The Rust compiler is able to infer lifetimes for trivial cases. This is called elision.

fn complete_explicit_lifetimes<'a, 'b>(int_1: &'a i32, int_2: &'b i32) -> &'a i32 {
    int_1
}

Manually tagging lifetime of both input variables.

fn explicit_lifetime<'a>(int_1: &'a i32, int_2: &i32) -> &'a i32 {
    int_1
}

Eliding a single lifetime.

fn elided_lifetime(input: &i32) -> &i32 {
    input
}

When there is one reference lifetime annotations can be completely removed.

Static Lifetimes

TODO

Just examples past this point

There isn’t any new explanations past this point. The below samples show a few situations where lifetime annotations are required.

Sub arrays


Want a function to return an array slice?

fn first_two<'a>(x: &'a [String], r_str: &str) -> &'a [String] {
    &x[0..2]
}

Iterate and filter an arrays contents?

fn filter_on_pattern<'a>(x: &'a [String], r_str: &str) -> Vec<&'a String> {
    x
     .iter()
     .filter(|&s| s.contains(r_str))
     .collect()
}

Elision and Types


fn count_chars(input: &str, charset: HashSet<char>) -> (HashMap<char, u32>, &str) {
    // Count each character is the set.
    let mut map: HashMap<char, u32> = HashMap::new();
    for character in input.chars() {
        if charset.contains(&character) {
            map.entry(character).and_modify(|c| *c += 1).or_insert(1);
        }
    }
    (map, input)
}

fn count_chars_str<'a>(input: &'a str, charset: &str) -> (HashMap<char, u32>, &'a str) {
    // Create a set of the str for O(1) lookup.
    let chars_to_count = charset.chars().collect::<HashSet<char>>();

    // Count each character is the set.
    let mut map: HashMap<char, u32> = HashMap::new();
    for character in input.chars() {
        if chars_to_count.contains(&character) {
            map.entry(character).and_modify(|c| *c += 1).or_insert(1);
        }
    }

    (map, input)
}

Iterator Implementation

Writing an iterator for a custom collection is a great way to understand how lifetimes work. Below is a collection that stores strings in vectors based of their first character. The iterator allows trivial traversal of the 2D matrix. Although the structure is not very useful it demonstrates the use of lifetimes in an iterator implementation.

use std::fmt;

#[derive(Debug, Clone)]
pub struct InvalidStartingChar;
impl fmt::Display for InvalidStartingChar {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Only supports characters starting in the range of a - Z")
    }
}
#[derive(Debug)]
pub struct Strange2DVec {
    items: [Vec<String>; 26],
}

impl Strange2DVec {
    pub fn new() -> Strange2DVec {
        let x: [Vec<String>; 26] = Default::default();
        Self { items: x }
    }

    pub fn insert(&mut self, element: String) -> Result<(), InvalidStartingChar> {
        let first_char = element.chars().next().ok_or(InvalidStartingChar)?;
        if !first_char.is_ascii_alphabetic() {
            return Err(InvalidStartingChar);
        }
        let lowered = first_char.to_ascii_lowercase();
        self.items[25 - (122 - lowered as usize)].push(element);
        Ok(())
    }

    // You can use '_ to explicitly show lifetimes are required whilst still allowing the compiler
    // to elide the lifetimes automatically. (pure readability)
    pub fn iter(&self) -> Iter<'_> {
        self.items.iter().next();
        let mut parent_iter = self.items.iter();
        parent_iter.next(); // TEMP
        Iter {
            parent_iter: Box::new(parent_iter),
            item_iter: Box::new(self.items[0].iter()),
        }
    }
}

// Everything in this struct cannot outlive the lifetime of the parent struct.
pub struct Iter<'a> {
    parent_iter: Box<dyn Iterator<Item = &'a Vec<String>> + 'a>,
    item_iter: Box<dyn Iterator<Item = &'a String> + 'a>,
}

// Each element must have a lifetime shorter or equal to the collection.
impl<'a> Iterator for Iter<'a> {
    type Item = &'a String;
    fn next(&mut self) -> Option<Self::Item> {
        self.item_iter.next().or_else(|| {
            // Find the next vec with contents
            while let Some(parent) = self.parent_iter.next() {
                self.item_iter = Box::new(parent.iter());
                if let Some(child) = self.item_iter.next() {
                    return Some(child);
                }
            }
            None
        })
    }
}

Comments