Rust Lifetimes uitgelegd: Annotaties, Elision en Sollicitatievragen 2026

Een uitgebreide gids over Rust lifetimes: van basisconcepten tot lifetime-annotaties, elision-regels en veelgestelde vragen tijdens technische sollicitatiegesprekken.

Rust Lifetimes annotaties en borrow checker visualisatie

Rust lifetimes vormen het mechanisme waarmee de compiler bijhoudt hoe lang referenties geldig blijven. In tegenstelling tot talen met garbage collection, waar geheugenbeheer tijdens runtime plaatsvindt, valideert de borrow checker van Rust de geldigheid van referenties tijdens compilatie. Dit elimineert volledige categorieën geheugenfouten voordat de code überhaupt wordt uitgevoerd.

Snelle Antwoord voor Sollicitaties

Een lifetime in Rust is een compile-time constructie die beschrijft gedurende welke scope een referentie geldig is. De borrow checker gebruikt lifetimes om te garanderen dat referenties nooit langer leven dan de data waarnaar ze verwijzen, waardoor dangling pointers worden voorkomen zonder runtime overhead.

Wat Lifetimes daadwerkelijk representeren in het geheugen

Lifetimes gaan niet over hoe lang waarden bestaan. Ze beschrijven hoe lang referenties naar waarden geldig blijven. Elke referentie in Rust heeft een lifetime, zelfs wanneer annotaties worden weggelaten.

Bekijk deze functie die de compiler afwijst:

dangling_reference.rsrust
fn create_dangling() -> &String {
    let s = String::from("hello");
    &s  // ERROR: `s` is dropped at end of function
}

De String s leeft alleen binnen de scope van de functie. Het retourneren van een referentie ernaar zou een dangling pointer creëren, aangezien het geheugen van de String wordt vrijgegeven wanneer de functie terugkeert. De borrow checker vangt dit op tijdens compilatie.

De oplossing vereist ofwel het retourneren van een owned waarde of het garanderen dat de gerefereerde data langer leeft dan de functieaanroep:

valid_return.rsrust
// Option 1: Return owned value
fn create_owned() -> String {
    String::from("hello")
}

// Option 2: Reference data that outlives the function
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

In first_word leent de geretourneerde &str van de input s, dus blijft deze geldig zolang s bestaat. De aanroeper controleert de lifetime van de input.

Syntax en semantiek van Lifetime-annotaties

Expliciete lifetime-annotaties gebruiken de syntax 'a, 'b, enzovoort. Dit zijn geen instructies aan de compiler over hoe lang referenties moeten leven. Ze beschrijven relaties tussen de lifetimes van meerdere referenties.

De Rust Reference definieert lifetime-annotaties als generieke parameters die beperken hoe lang referenties geldig moeten blijven ten opzichte van elkaar.

lifetime_annotations.rsrust
// Both inputs and output share the same lifetime 'a
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");
    let result;
    {
        let string2 = String::from("short");
        result = longest(&string1, &string2);
        println!("Longest: {}", result);  // Valid: both strings alive
    }
    // println!("{}", result);  // ERROR: string2 dropped
}

De annotatie 'a vertelt de compiler: de geretourneerde referentie zal geldig zijn voor de doorsnede van de lifetimes van x en y. Aangezien string2 een kortere lifetime heeft, kan result niet worden gebruikt nadat string2 wordt verwijderd.

Meerdere verschillende lifetimes drukken complexere relaties uit:

multiple_lifetimes.rsrust
// Output tied only to first parameter's lifetime
fn first_only<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
    x
}

fn main() {
    let owned = String::from("owned");
    let result;
    {
        let temporary = String::from("temporary");
        result = first_only(&owned, &temporary);
    }
    // result still valid: only depends on owned
    println!("{}", result);
}

Lifetime Elision-regels in Rust 2024

De compiler past drie elision-regels toe om lifetimes af te leiden wanneer annotaties worden weggelaten. Deze regels, gedocumenteerd in de Rustonomicon, verminderen boilerplate zonder de veiligheid aan te tasten.

De drie Elision-regels
  1. Elke input-referentie krijgt zijn eigen lifetime-parameter
  2. Als er precies één input-lifetime bestaat, wordt deze toegepast op alle output-referenties
  3. Als &self of &mut self bestaat, wordt de lifetime daarvan toegepast op alle output-referenties

Deze regels dekken de meeste gangbare patronen:

elision_examples.rsrust
// Rule 1: Each input gets own lifetime
fn takes_two(x: &str, y: &str) {}
// Compiler reads: fn takes_two<'a, 'b>(x: &'a str, y: &'b str) {}

// Rule 2: Single input lifetime propagates to output  
fn first_char(s: &str) -> &str {
    &s[0..1]
}
// Compiler reads: fn first_char<'a>(s: &'a str) -> &'a str

// Rule 3: &self lifetime propagates to output
impl Parser {
    fn peek(&self) -> &Token {
        &self.tokens[self.position]
    }
    // Compiler reads: fn peek<'a>(&'a self) -> &'a Token
}

Wanneer elision faalt, vereist de compiler expliciete annotaties. Dit gebeurt het vaakst bij functies die referenties retourneren die zijn afgeleid van meerdere inputs:

elision_fails.rsrust
// ERROR: Can't determine output lifetime
fn ambiguous(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// FIX: Explicit annotation resolves ambiguity
fn unambiguous<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Struct Lifetimes en de Outlives-relatie

Structs die referenties bevatten vereisen lifetime-annotaties om uit te drukken dat geleende data langer moet leven dan de struct:

struct_lifetimes.rsrust
struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    // new borrows from input, so Excerpt can't outlive the source
    fn new(source: &'a str, start: usize, end: usize) -> Self {
        Excerpt { text: &source[start..end] }
    }
    
    // level() returns owned data, no lifetime in signature
    fn level(&self) -> u32 {
        self.text.len() as u32 / 10
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(&novel, 0, 16);
    println!("Excerpt: {}", excerpt.text);
}  // novel dropped, but excerpt already out of scope

De 'a in Excerpt<'a> bindt de geldigheid van de struct aan de lifetime van de geleende text. Proberen om een Excerpt te gebruiken nadat de bron-String is verwijderd, veroorzaakt een compilatiefout.

Voor structs met meerdere referenties kan elk verschillende lifetimes hebben:

multiple_struct_lifetimes.rsrust
struct Comparison<'a, 'b> {
    baseline: &'a str,
    candidate: &'b str,
}

impl<'a, 'b> Comparison<'a, 'b> {
    fn new(baseline: &'a str, candidate: &'b str) -> Self {
        Comparison { baseline, candidate }
    }
    
    fn baseline_only(&self) -> &'a str {
        self.baseline
    }
}

Klaar om je Rust gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

De 'static Lifetime en wanneer deze te gebruiken

De 'static lifetime geeft aan dat data leeft voor de gehele programma-uitvoering. String literals hebben deze lifetime omdat ze zijn ingebed in de binary:

static_lifetime.rsrust
let s: &'static str = "I live forever";

// Constants are implicitly 'static
const CONFIG_VERSION: &str = "2.0.0";

// Thread-safe globals require 'static
static COUNTER: AtomicU64 = AtomicU64::new(0);

Een veelvoorkomend patroon betreft T: 'static bounds, wat ontwikkelaars vaak verwart. Deze bound betekent dat T geen niet-statische referenties bevat, niet dat T een referentie moet zijn:

static_bound.rsrust
use std::thread;

fn spawn_task<T: Send + 'static>(data: T) {
    thread::spawn(move || {
        // data moved into thread, must live independently
        println!("Processing in thread");
    });
}

fn main() {
    let owned = String::from("owned data");
    spawn_task(owned);  // OK: String is 'static (owns its data)
    
    let reference = "borrowed";
    // spawn_task(reference);  // OK: &'static str
}

De thread zou langer kunnen leven dan de functie die hem heeft gestart, dus data die naar threads wordt verplaatst mag geen referenties naar stack-lokale variabelen bevatten.

Veelvoorkomende Lifetime-fouten en hun oplossingen

Verschillende patronen veroorzaken regelmatig problemen voor ontwikkelaars. Het begrijpen hiervan versnelt het debuggen.

Referenties naar lokale variabelen retourneren:

error_local_ref.rsrust
fn bad_split(text: &str, delimiter: char) -> (&str, &str) {
    let parts: Vec<&str> = text.split(delimiter).collect();
    (parts[0], parts[1])  // OK: parts[i] borrow from text, not from Vec
}

fn truly_bad() -> &str {
    let local = String::from("local");
    &local  // ERROR: local dropped at end of function
}

Referenties opslaan in structs met niet-overeenkomende lifetimes:

error_struct_lifetime.rsrust
struct Cache {
    data: String,
    // view: &str,  // ERROR: needs lifetime parameter
}

// Self-referential structs require unsafe or crates like ouroboros
struct CacheWithView<'a> {
    data: String,
    view: Option<&'a str>,  // Can't point to self.data safely
}

Zelf-referentiële structs, waarbij een veld naar een ander veld verwijst, vereisen ofwel Pin met unsafe code of crates zoals ouroboros. De module Rust Smart Pointers behandelt deze geavanceerde patronen.

Lifetime bounds op trait-implementaties:

trait_lifetime_bounds.rsrust
trait Processor {
    fn process<'a>(&self, input: &'a str) -> &'a str;
}

struct Prefixer {
    prefix: String,
}

impl Processor for Prefixer {
    // Can't return &format!(...) - would be temporary
    fn process<'a>(&self, input: &'a str) -> &'a str {
        input  // Must return input or part of it
    }
}

Higher-Ranked Trait Bounds (HRTBs) voor generieke Lifetimes

Higher-Ranked Trait Bounds gebruiken de for<'a> syntax om uit te drukken dat een type een trait moet voldoen voor elke lifetime, niet alleen een specifieke:

hrtb.rsrust
use std::fmt::Debug;

// F must be callable with any lifetime 'a
fn apply_to_refs<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let owned = String::from("test");
    let result = f(&owned);
    println!("{}", result);
}

fn identity(s: &str) -> &str { s }

fn main() {
    apply_to_refs(identity);
}

HRTBs komen vaak voor in APIs die closures accepteren en in het Rust async/await ecosysteem waar futures moeten werken met referenties van variërende lifetimes.

Sollicitatievragen over Rust Lifetimes

Technische sollicitatiegesprekken peilen het begrip van lifetimes op meerdere niveaus. Deze vragen komen regelmatig voor bij Rust-posities.

Vraag: Waarom compileert deze code niet?

interview_q1.rsrust
fn get_str() -> &str {
    "hello"
}

Antwoord: Het returntype heeft een expliciete lifetime-annotatie nodig. Hoewel de string literal een 'static lifetime heeft, drukt de functiesignatuur dit niet uit. De oplossing: fn get_str() -> &'static str.

Vraag: Leg uit waarom dit compileert en of het veilig is:

interview_q2.rsrust
fn longest<'a>(x: &'a str, _y: &str) -> &'a str {
    x
}

Antwoord: Dit compileert omdat de output-lifetime alleen aan x is gebonden. De _y parameter kan elke lifetime hebben aangezien de returnwaarde er niet van afhangt. Dit is veilig: de geldigheid van de geretourneerde referentie hangt uitsluitend af van de lifetime van x.

Vraag: Wat gebeurt er wanneer je probeert een referentie naast owned data op te slaan?

interview_q3.rsrust
struct Config<'a> {
    name: String,
    description: &'a str,
}

Antwoord: Dit patroon is geldig maar beperkt hoe Config kan worden gebruikt. De struct kan niet langer leven dan waar description naar verwijst. Voor owned data die naar zichzelf moet verwijzen, overweeg het gebruik van String voor beide velden, of technieken behandeld in Rust ownership en borrowing.

Vraag: Hoe interageren lifetimes met trait objects?

interview_q4.rsrust
trait Formatter {
    fn format(&self, input: &str) -> String;
}

fn get_formatter<'a>() -> Box<dyn Formatter + 'a> {
    // ...
}

Antwoord: Trait objects hebben standaard een impliciete 'static lifetime bound. Het schrijven van Box<dyn Formatter + 'a> staat expliciet toe dat het trait object referenties met lifetime 'a bevat. Zonder de expliciete bound is Box<dyn Formatter> gelijk aan Box<dyn Formatter + 'static>.

Lifetime Variance: Covariantie en Contravariantie

Variance bepaalt hoe lifetimes zich verhouden wanneer types genest zijn. Rust referenties volgen deze regels:

  • &'a T is covariant in 'a: een langere lifetime kan een kortere vervangen
  • &'a mut T is invariant in T: het type moet exact overeenkomen
  • fn(&'a T) is contravariant in 'a: een kortere lifetime kan een langere vervangen
variance.rsrust
fn covariant_example() {
    let s: &'static str = "static";
    let r: &str = s;  // OK: 'static lives longer than any 'a
}

fn invariant_example() {
    let mut vec: Vec<&'static str> = vec!["a"];
    // let s = String::from("local");
    // vec.push(&s);  // ERROR: &s is not &'static str
}

Het begrijpen van variance is belangrijk bij het ontwerpen van generieke APIs die referenties accepteren of retourneren. Het artikel Rust traits en generics behandelt geavanceerde generieke patronen.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Lifetime-annotaties in de praktijk: belangrijkste punten

  • Lifetimes beschrijven de geldigheid van referenties, niet het bestaan van waarden. De borrow checker gebruikt ze om dangling pointers te voorkomen tijdens compilatie.
  • Elision-regels behandelen de meeste gevallen automatisch. Expliciete annotaties worden noodzakelijk wanneer referenties worden geretourneerd die zijn afgeleid van meerdere inputs.
  • Struct lifetimes drukken de outlives-relatie uit: elke struct die referenties bevat moet worden geparametriseerd door de lifetimes van die referenties.
  • De 'static bound op generics betekent "bevat geen niet-statische referenties", niet "moet een referentie zijn".
  • Zelf-referentiële structs vereisen speciale behandeling via Pin, unsafe code of helper crates.
  • Sollicitatievragen richten zich op het begrijpen waarom code niet compileert en hoe lifetime-annotaties de geldigheidsgaranties veranderen.
Dagelijkse challenge

Zie jij de bug in Rust?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter van SharpSkill

Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.

Bijgewerkt op 27 augustus 2026

Tags

#rust
#lifetimes
#borrow-checker
#memory-safety
#interview

Delen

Gerelateerde artikelen