Bird
Raised Fist0
Rustprogramming~15 mins

Slice lifetime basics in Rust - Deep Dive

Choose your learning style10 modes available
Overview - Slice lifetime basics
What is it?
In Rust, a slice is a view into a sequence of elements, like part of an array or vector. A slice does not own the data it points to, so it uses lifetimes to ensure the data it references lives long enough. Slice lifetimes tell the compiler how long the slice can safely be used without causing errors or crashes.
Why it matters
Without slice lifetimes, Rust could not guarantee memory safety when using slices. If a slice outlived the data it points to, it could cause crashes or corrupted data. Lifetimes prevent these bugs by making sure slices never live longer than their data, helping Rust programs be safe and reliable.
Where it fits
Before learning slice lifetimes, you should understand Rust's ownership and borrowing rules. After mastering slice lifetimes, you can learn about more complex lifetime scenarios, like lifetime annotations in functions and structs, and advanced borrowing patterns.
Mental Model
Core Idea
A slice's lifetime is a promise that the data it points to will stay valid for the slice's entire usage.
Think of it like...
Imagine borrowing a book from a friend. The slice is like the borrowed book, and the lifetime is the agreed time you can keep it before returning it. You can't read the book after returning it, just like you can't use a slice after its data is gone.
Data owner (array/vector)
┌─────────────────────────┐
│ [10, 20, 30, 40, 50]   │
└─────────┬───────────────┘
          │
          ▼
Slice (view into data)
┌───────────────┐
│ [20, 30, 40]  │
└───────────────┘

Lifetime: The slice must not outlive the data owner.
Build-Up - 7 Steps
1
FoundationWhat is a slice in Rust
🤔
Concept: Slices are references to a part of a collection without owning it.
In Rust, a slice lets you look at a part of an array or vector without copying it. For example, if you have an array [1, 2, 3, 4], a slice can be the middle two elements [2, 3]. Slices use & to borrow the data, like &arr[1..3].
Result
You get a reference to part of the data without copying it.
Understanding slices as borrowed views helps you write efficient code without unnecessary copying.
2
FoundationWhy lifetimes matter for slices
🤔
Concept: Lifetimes tell Rust how long a slice can safely be used.
Since slices don't own data, Rust needs to know the data lives long enough. Lifetimes are annotations that track this. If a slice tries to live longer than the data, Rust will give a compile error to prevent unsafe memory access.
Result
Rust ensures slices never outlive their data, preventing bugs.
Knowing lifetimes protect data safety is key to understanding Rust's memory guarantees.
3
IntermediateBasic lifetime syntax for slices
🤔
Concept: How to write lifetime annotations for slices in functions.
When a function takes a slice, you often write lifetimes like fn print_slice<'a>(slice: &'a [i32]) { ... }. The 'a means the slice borrows data that lives at least as long as 'a. This tells Rust the slice and data lifetimes are connected.
Result
Functions can safely accept slices with clear lifetime rules.
Learning lifetime syntax helps you communicate borrowing rules to the compiler.
4
IntermediateLifetime elision rules simplify slices
🤔Before reading on: do you think Rust always requires explicit lifetimes on slice references in functions? Commit to yes or no.
Concept: Rust can often infer lifetimes for slices using elision rules.
Rust has rules that let you skip writing lifetimes in simple cases. For example, fn print_slice(slice: &[i32]) works because Rust assumes the slice's lifetime matches the input reference. This makes code cleaner while keeping safety.
Result
You can write simpler code without losing lifetime safety.
Understanding elision rules reduces boilerplate and helps you read Rust code more easily.
5
IntermediateMultiple slices and lifetime relationships
🤔Before reading on: if a function takes two slices with different lifetimes, do you think Rust treats them as the same or separate lifetimes? Commit to your answer.
Concept: Each slice can have its own lifetime, and Rust tracks them separately.
When a function takes multiple slices, you can give each a different lifetime, like fn compare<'a, 'b>(a: &'a [i32], b: &'b [i32]) { ... }. Rust ensures each slice is valid for its own lifetime, preventing mixing data that might not live long enough.
Result
Rust enforces safety even with multiple borrowed slices.
Knowing lifetimes are tracked individually helps avoid confusing lifetime errors.
6
AdvancedSlices in structs with lifetime annotations
🤔Before reading on: do you think a struct holding a slice needs a lifetime annotation? Commit to yes or no.
Concept: Structs that hold slices must declare lifetimes to ensure data validity.
If you store a slice inside a struct, you must add a lifetime parameter, like struct View<'a> { data: &'a [i32] }. This tells Rust the struct cannot outlive the data it points to. Without this, Rust won't compile the code.
Result
Structs safely hold references without risking dangling pointers.
Understanding lifetimes in structs is crucial for designing safe data structures.
7
ExpertCommon lifetime pitfalls with slices
🤔Before reading on: do you think returning a slice from a function that creates data inside it is safe? Commit to yes or no.
Concept: Returning slices referencing local data causes lifetime errors and unsafe code.
If a function creates data inside and returns a slice to it, like fn get_slice() -> &[i32] { let arr = [1,2,3]; &arr[..] }, Rust rejects this because arr is dropped when the function ends. The slice would point to invalid memory, causing bugs.
Result
Rust prevents returning invalid slices, ensuring memory safety.
Knowing this prevents a common source of crashes and undefined behavior in Rust.
Under the Hood
Rust uses lifetimes as compile-time checks to track how long references, including slices, are valid. The compiler analyzes the code to ensure no reference outlives the data it points to. Lifetimes do not exist at runtime; they are erased after compilation. This static analysis prevents dangling pointers and data races.
Why designed this way?
Rust was designed to guarantee memory safety without a garbage collector. Lifetimes provide a way to enforce safe borrowing rules at compile time, avoiding runtime overhead. This design balances safety and performance, unlike languages that rely on runtime checks or manual memory management.
┌───────────────┐
│ Data Owner    │
│ (array/vector)│
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Slice (&[T])  │
│ with lifetime │
│ annotation    │
└──────┬────────┘
       │
       ▼
Compile-time check: slice lifetime ≤ data lifetime
       │
       ▼
Memory safety guaranteed
Myth Busters - 4 Common Misconceptions
Quick: Do slices own the data they point to? Commit to yes or no.
Common Belief:Slices own the data they reference, so you can use them independently.
Tap to reveal reality
Reality:Slices only borrow data; they do not own it. The original data must live as long as the slice.
Why it matters:Assuming slices own data can lead to using invalid memory and crashes.
Quick: Can you safely return a slice to local data from a function? Commit to yes or no.
Common Belief:It's safe to return a slice to data created inside a function.
Tap to reveal reality
Reality:You cannot return a slice to local data because the data is dropped when the function ends, making the slice invalid.
Why it matters:Ignoring this causes dangling references and undefined behavior.
Quick: Does Rust always require explicit lifetime annotations on slices? Commit to yes or no.
Common Belief:You must always write lifetime annotations for slices in function signatures.
Tap to reveal reality
Reality:Rust often infers lifetimes using elision rules, so explicit annotations are not always needed.
Why it matters:Believing otherwise can make code unnecessarily complex and harder to read.
Quick: Are all lifetimes in a function signature treated as the same? Commit to yes or no.
Common Belief:All lifetimes in a function signature are the same and interchangeable.
Tap to reveal reality
Reality:Each lifetime can be different and Rust tracks them separately to ensure safety.
Why it matters:Misunderstanding this leads to confusing errors and unsafe code.
Expert Zone
1
Lifetime elision rules only apply in specific cases; understanding when to write explicit lifetimes is key for complex functions.
2
Slices can have sub-lifetimes when nested inside other references, requiring careful lifetime chaining.
3
Using higher-ranked trait bounds (HRTBs) can express functions that accept slices with any lifetime, enabling more flexible APIs.
When NOT to use
Avoid using slices when you need ownership or mutable access that outlives the original data. Instead, use owned types like Vec or String. Also, for asynchronous code, consider using owned data or smart pointers to avoid lifetime conflicts.
Production Patterns
Slices are widely used for efficient read-only access to data without copying. In APIs, slices allow flexible input types. Lifetime annotations in structs enable zero-copy views into data. Advanced libraries use lifetime parameters to build safe abstractions over buffers and streams.
Connections
Ownership and Borrowing in Rust
Slices build directly on Rust's ownership and borrowing rules.
Understanding ownership is essential to grasp why slices need lifetimes to safely borrow data.
Reference Counting (Rc/Arc)
Slices differ from reference counting by borrowing without ownership or runtime overhead.
Knowing the difference helps choose between lightweight borrowing (slices) and shared ownership (Rc/Arc).
Borrow Checker
The borrow checker enforces lifetime rules for slices at compile time.
Understanding the borrow checker clarifies why lifetime errors occur and how to fix them.
Common Pitfalls
#1Returning a slice to local data causes dangling reference.
Wrong approach:fn get_slice() -> &[i32] { let arr = [1, 2, 3]; &arr[..] }
Correct approach:fn get_slice() -> Vec { vec![1, 2, 3] }
Root cause:The slice references data that is dropped when the function ends, making the slice invalid.
#2Omitting lifetime annotations in structs holding slices.
Wrong approach:struct View { data: &[i32], }
Correct approach:struct View<'a> { data: &'a [i32], }
Root cause:Rust needs explicit lifetimes to ensure the struct does not outlive the data it borrows.
#3Assuming all lifetimes in a function are the same.
Wrong approach:fn compare(a: &[i32], b: &[i32]) -> bool { // no lifetime annotations, assuming same lifetime a == b }
Correct approach:fn compare<'a, 'b>(a: &'a [i32], b: &'b [i32]) -> bool { a == b }
Root cause:Different slices can have different lifetimes; Rust tracks them separately to ensure safety.
Key Takeaways
Slices are borrowed views into data and do not own the data they point to.
Lifetimes ensure slices never outlive the data they reference, preventing unsafe memory access.
Rust often infers lifetimes for slices, but explicit annotations are needed in complex cases.
Structs holding slices must declare lifetime parameters to guarantee data validity.
Returning slices to local data is unsafe and disallowed by Rust's lifetime rules.

Practice

(1/5)
1. What does a slice lifetime in Rust indicate?
fn example<'a>(slice: &'a [i32]) {}
easy
A. How long the slice reference is valid in the program
B. The size of the slice in memory
C. The number of elements in the slice
D. The type of elements inside the slice

Solution

  1. Step 1: Understand what a lifetime means in Rust

    Lifetimes tell Rust how long a reference is valid to prevent using invalid data.
  2. Step 2: Apply this to a slice reference

    The lifetime on a slice reference means how long the slice can be safely used.
  3. Final Answer:

    How long the slice reference is valid in the program -> Option A
  4. Quick Check:

    Slice lifetime = reference validity [OK]
Hint: Lifetimes track reference validity duration [OK]
Common Mistakes:
  • Confusing lifetime with size or length
  • Thinking lifetime is about element type
  • Ignoring lifetime annotations on references
2. Which of the following is the correct syntax to declare a function that takes a slice with a lifetime parameter?
fn process_slice(____) {}
easy
A. &'a [i32]
B. &'a [i32];
C. &[i32]
D. &[i32]'a

Solution

  1. Step 1: Recall lifetime syntax for references

    Lifetimes are written as &'a before the type to indicate the lifetime of the reference.
  2. Step 2: Check each option

    &'a [i32] correctly places &'a before the slice type [i32]. &[i32] misses lifetime, &'a [i32]; has an extra semicolon, &[i32]'a places lifetime incorrectly.
  3. Final Answer:

    &'a [i32] -> Option A
  4. Quick Check:

    Lifetime syntax = &'a [type] [OK]
Hint: Lifetimes go right after & before type [OK]
Common Mistakes:
  • Putting lifetime after the type
  • Adding extra semicolons inside parameters
  • Omitting lifetime when needed
3. What will be the output of this Rust code?
fn get_first<'a>(slice: &'a [i32]) -> &'a i32 {
    &slice[0]
}

fn main() {
    let numbers = vec![10, 20, 30];
    let first = get_first(&numbers);
    println!("{}", first);
}
medium
A. 20
B. 10
C. Compilation error due to lifetime mismatch
D. Runtime panic

Solution

  1. Step 1: Understand the function behavior

    The function returns a reference to the first element of the slice, which is 10.
  2. Step 2: Check lifetime correctness and usage

    The lifetime 'a ensures the returned reference is valid as long as the input slice is valid. The vector lives long enough, so no error.
  3. Final Answer:

    10 -> Option B
  4. Quick Check:

    Returned first element = 10 [OK]
Hint: Returned slice element matches first index [OK]
Common Mistakes:
  • Thinking lifetime causes compile error here
  • Confusing element values
  • Expecting runtime panic without reason
4. Identify the error in this Rust code snippet:
fn invalid<'a>() -> &'a [i32] {
    let data = vec![1, 2, 3];
    &data[..]
}
medium
A. Missing lifetime annotation on data
B. Syntax error in slice syntax
C. The function returns a reference to data that will be dropped
D. No error, code is correct

Solution

  1. Step 1: Analyze the function return and data lifetime

    The function returns a slice reference with lifetime 'a, but data is created inside the function and dropped when it ends.
  2. Step 2: Understand why this causes an error

    Returning a reference to local data is invalid because the data won't live long enough, causing a dangling reference.
  3. Final Answer:

    The function returns a reference to data that will be dropped -> Option C
  4. Quick Check:

    Returning local reference = error [OK]
Hint: Never return reference to local variable [OK]
Common Mistakes:
  • Assuming lifetime annotation fixes local data drop
  • Ignoring ownership rules
  • Thinking slice syntax is wrong
5. Given this function that returns a slice from two input slices, which lifetime annotation is correct to ensure safety?
fn longest_slice<'a>(x: &'a [i32], y: &'a [i32]) -> &____ [i32] {
    if x.len() > y.len() { x } else { y }
}
hard
A. 'a + 'b
B. 'b
C. 'static
D. 'a

Solution

  1. Step 1: Understand the function's return lifetime

    The function returns a slice that could come from either input slice, both sharing lifetime 'a.
  2. Step 2: Use shared lifetime for safety

    The correct lifetime for the return is 'a, matching the lifetime of both input slices.
  3. Final Answer:

    'a -> Option D
  4. Quick Check:

    Return lifetime = 'a of inputs [OK]
Hint: Return lifetime must cover both inputs [OK]
Common Mistakes:
  • Choosing only one input lifetime
  • Using 'static without reason
  • Ignoring lifetime intersection rules
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard