105-lifetime-basics — Lifetime Basics
Tutorial
The Problem
Returning a reference from a function is safe only if the referenced data outlives the reference. In C, returning a pointer to a local variable is undefined behavior — the data is destroyed when the function returns. Rust's lifetime system prevents this at compile time by tracking how long each reference is valid.
Lifetime annotations ('a) do not change runtime behavior — they are purely compile-time metadata that the borrow checker uses to verify that no reference outlives its data.
🎯 Learning Outcomes
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str and explain what 'a means'static lifetime and when it appliesCode Example
#![allow(clippy::all)]
// 105: Lifetime Basics
// Lifetime annotations tell the compiler how long references live
// Approach 1: Lifetime in function signature
// 'a means: the returned reference lives as long as the inputs
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
fn first_element<'a>(v: &'a [i32]) -> Option<&'a i32> {
v.first()
}
// Approach 2: Lifetime in struct
struct Important<'a> {
content: &'a str,
}
impl<'a> Important<'a> {
fn new(content: &'a str) -> Self {
Important { content }
}
fn content(&self) -> &str {
self.content
}
}
// Approach 3: Multiple lifetimes
fn first_word<'a>(s: &'a str) -> &'a str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
// This would NOT compile — dangling reference:
// fn dangling() -> &str {
// let s = String::from("hello");
// &s // ERROR: s dropped here, reference would dangle
// }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_longest() {
assert_eq!(longest("hello", "hi"), "hello");
assert_eq!(longest("a", "bb"), "bb");
}
#[test]
fn test_first_element() {
assert_eq!(first_element(&[1, 2, 3]), Some(&1));
assert_eq!(first_element(&[]), None);
}
#[test]
fn test_important() {
let msg = Important::new("test");
assert_eq!(msg.content(), "test");
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
}
}Key Differences
'static lifetime**: Rust's &'static str lives for the entire program duration (string literals); OCaml has no equivalent concept because the GC handles all lifetimes uniformly.OCaml Approach
OCaml has no lifetime system. The GC ensures referenced data lives at least as long as any reference to it:
let longest s1 s2 =
if String.length s1 >= String.length s2 then s1 else s2
(* Both s1 and s2 are GC-managed; the returned reference is always valid *)
A struct holding a reference is just a record with a string field — the GC tracks all references and prevents dangling pointers at runtime.
Full Source
#![allow(clippy::all)]
// 105: Lifetime Basics
// Lifetime annotations tell the compiler how long references live
// Approach 1: Lifetime in function signature
// 'a means: the returned reference lives as long as the inputs
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
fn first_element<'a>(v: &'a [i32]) -> Option<&'a i32> {
v.first()
}
// Approach 2: Lifetime in struct
struct Important<'a> {
content: &'a str,
}
impl<'a> Important<'a> {
fn new(content: &'a str) -> Self {
Important { content }
}
fn content(&self) -> &str {
self.content
}
}
// Approach 3: Multiple lifetimes
fn first_word<'a>(s: &'a str) -> &'a str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
// This would NOT compile — dangling reference:
// fn dangling() -> &str {
// let s = String::from("hello");
// &s // ERROR: s dropped here, reference would dangle
// }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_longest() {
assert_eq!(longest("hello", "hi"), "hello");
assert_eq!(longest("a", "bb"), "bb");
}
#[test]
fn test_first_element() {
assert_eq!(first_element(&[1, 2, 3]), Some(&1));
assert_eq!(first_element(&[]), None);
}
#[test]
fn test_important() {
let msg = Important::new("test");
assert_eq!(msg.content(), "test");
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_longest() {
assert_eq!(longest("hello", "hi"), "hello");
assert_eq!(longest("a", "bb"), "bb");
}
#[test]
fn test_first_element() {
assert_eq!(first_element(&[1, 2, 3]), Some(&1));
assert_eq!(first_element(&[]), None);
}
#[test]
fn test_important() {
let msg = Important::new("test");
assert_eq!(msg.content(), "test");
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
}
}
Deep Comparison
Core Insight
Lifetimes tell the compiler how long references are valid — preventing dangling references at compile time
OCaml Approach
Rust Approach
Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| See | example.ml | example.rs |
Exercises
longest_word<'a>(sentence: &'a str) -> &'a str that returns the longest word from a sentence as a borrowed slice.Cache<'a> { data: &'a [i32], computed: Vec<i32> } struct and implement a method that borrows from data and populates computed.