Marcin Jahn | Dev Notebook
  • Home
  • Programming
  • Technologies
  • Projects
  • About
  • Home
  • Programming
  • Technologies
  • Projects
  • About
  • An icon of the Core section Core
    • Programs Execution
    • Stack and Heap
    • Asynchronous Programming
      • Overview
      • Event Queues
      • Fibers
      • Stackless Coroutines
  • An icon of the .NET section .NET
    • HTTPClient
    • Async
      • How Async Works
      • TAP Tips
    • Equality
    • Comparisons
    • Enumerables
    • Unit Tests
    • Generic Host
    • Logging
    • Configuration
    • Records
    • Nullability
    • Garbage Collector
    • IL and Allocations
    • gRPC
    • Source Generators
    • Platform Invoke
    • ASP.NET Core
      • Overview
      • Middleware
      • Razor Pages
      • Routing in Razor Pages
      • Web APIs
      • Filters
      • Identity
      • Validation
      • Tips
    • Entity Framework Core
      • Overview
      • Testing
      • Tips
  • An icon of the Angular section Angular
    • Overview
    • Components
    • Directives
    • Services and DI
    • Routing
    • Observables (RxJS)
    • Forms
    • Pipes
    • HTTP
    • Modules
    • NgRx
    • Angular Universal
    • Tips
    • Standalone Components
  • An icon of the JavaScript section JavaScript
    • OOP
    • JavaScript - The Weird Parts
    • JS Functions
    • ES Modules
    • Node.js
    • Axios Tips
    • TypeScript
      • TypeScript Environment Setup
      • TypeScript Tips
    • React
      • React Routing
      • MobX
    • Advanced Vue.js Features
  • An icon of the Rust section Rust
    • Overview
    • Cargo
    • Basics
    • Ownership
    • Structures
    • Enums
    • Organization
    • Collections
    • Error Handling
    • Generics
    • Traits
    • Lifetimes
    • Closures
    • Raw Pointers
    • Smart Pointers
    • Concurrency
    • Testing
    • Tips
  • An icon of the C/C++ section C/C++
    • Compilation
    • Structures
    • OOP in C
    • Pointers
    • Strings
    • Dynamic Memory
    • argc and argv Visualization
  • An icon of the GTK section GTK
    • Overview
    • GObject
    • GJS
  • An icon of the CSS section CSS
    • Responsive Design
    • CSS Tips
    • CSS Pixel
  • An icon of the Unity section Unity
    • Unity
  • An icon of the Functional Programming section Functional Programming
    • Fundamentals of Functional Programming
    • .NET Functional Features
    • Signatures
    • Function Composition
    • Error Handling
    • Partial Application
    • Modularity
    • Category Theory
      • Overview
      • Monoid
      • Other Magmas
      • Functors
  • An icon of the Algorithms section Algorithms
    • Big O Notation
    • Array
    • Linked List
    • Queue
    • Hash Table and Set
    • Tree
    • Sorting
    • Searching
  • An icon of the Architecture section Architecture
    • What is architecture?
    • Domain-Driven Design
    • ASP.NET Core Projects
  • An icon of the Core section Core
    • Programs Execution
    • Stack and Heap
    • Asynchronous Programming
      • Overview
      • Event Queues
      • Fibers
      • Stackless Coroutines
  • An icon of the .NET section .NET
    • HTTPClient
    • Async
      • How Async Works
      • TAP Tips
    • Equality
    • Comparisons
    • Enumerables
    • Unit Tests
    • Generic Host
    • Logging
    • Configuration
    • Records
    • Nullability
    • Garbage Collector
    • IL and Allocations
    • gRPC
    • Source Generators
    • Platform Invoke
    • ASP.NET Core
      • Overview
      • Middleware
      • Razor Pages
      • Routing in Razor Pages
      • Web APIs
      • Filters
      • Identity
      • Validation
      • Tips
    • Entity Framework Core
      • Overview
      • Testing
      • Tips
  • An icon of the Angular section Angular
    • Overview
    • Components
    • Directives
    • Services and DI
    • Routing
    • Observables (RxJS)
    • Forms
    • Pipes
    • HTTP
    • Modules
    • NgRx
    • Angular Universal
    • Tips
    • Standalone Components
  • An icon of the JavaScript section JavaScript
    • OOP
    • JavaScript - The Weird Parts
    • JS Functions
    • ES Modules
    • Node.js
    • Axios Tips
    • TypeScript
      • TypeScript Environment Setup
      • TypeScript Tips
    • React
      • React Routing
      • MobX
    • Advanced Vue.js Features
  • An icon of the Rust section Rust
    • Overview
    • Cargo
    • Basics
    • Ownership
    • Structures
    • Enums
    • Organization
    • Collections
    • Error Handling
    • Generics
    • Traits
    • Lifetimes
    • Closures
    • Raw Pointers
    • Smart Pointers
    • Concurrency
    • Testing
    • Tips
  • An icon of the C/C++ section C/C++
    • Compilation
    • Structures
    • OOP in C
    • Pointers
    • Strings
    • Dynamic Memory
    • argc and argv Visualization
  • An icon of the GTK section GTK
    • Overview
    • GObject
    • GJS
  • An icon of the CSS section CSS
    • Responsive Design
    • CSS Tips
    • CSS Pixel
  • An icon of the Unity section Unity
    • Unity
  • An icon of the Functional Programming section Functional Programming
    • Fundamentals of Functional Programming
    • .NET Functional Features
    • Signatures
    • Function Composition
    • Error Handling
    • Partial Application
    • Modularity
    • Category Theory
      • Overview
      • Monoid
      • Other Magmas
      • Functors
  • An icon of the Algorithms section Algorithms
    • Big O Notation
    • Array
    • Linked List
    • Queue
    • Hash Table and Set
    • Tree
    • Sorting
    • Searching
  • An icon of the Architecture section Architecture
    • What is architecture?
    • Domain-Driven Design
    • ASP.NET Core Projects

Lifetimes

References have their lifetimes, which define a scope for when a reference is valid. Lifetimes can be implicitly infered but sometimes we need to specify them explicitly. The main aim of lifetimes is to prevent dangling references.

Validity of lifetimes is checked during compilation by the borrow checker.

Example:

{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+

The program will not compile, because we’re trying to print reference r, which points to x that already went out of scope (its lifetime 'b is over).

Lifetime Elision

Not all usages of references require explicit lifetime annotations. There are some situations where the compiler is able to infer the proper lifetimes on its own.

There are three rules of how compiler infers lifetimes. They apply to functions and methods:

  1. Each parameter that is a reference gets its own lifetime parameter.
  2. If there is exactly one input parameter, its lifetime is assigned to all output parameters (that’s one functions with one parameter don’t need lifetime specifications).
  3. If there are multiple input parameters, but one of them is &self or mut &self (so, it’s a method), the lifetime of self is alligned to all output parameters.

If after applying these rules there are still some references left without lifetime information, compilation will fail and explicit lifetime information will be required.

Lifetime Annotations

Lifetime annotations describe the relationships of the lifetimes of multiple references to each other without affecting the lifetimes.

&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

'a is a common naming convention for lifetimes. It could be some word too.

Functions

Lifetime annotations in functions have meaning only when multiple types have annotations. Then the compiler can understand if some bindings have the same or different lifetimes.

Here’s a function that will not compile due to lack of lifetime annotations:

fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

The compiler needs to know how the returned reference is related to the parameters of the function.

When returning a reference from a function, it has to be one of the references passed as arguments of the function. Otherwise, the reference would have to be created from some local function’s variable. The returned value would be a dangling pointer!

What if I want to return a reference to some global/static value? Isn’t it allowed?

Here’s a valid version:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

Generics

Lifetimes are defined similarly to generic types.

Potentially, the compiler could infer the lifetime knowledge from the code itself - since I’m returning either x or y, probably the lifetime of the result should be the smallest common lifetime of these. However, that would bind the lifetime of the result with that particular implementation! If I, as an author of that function, change this implementation one day to always return x, the lifetime infered by the compiler will change! It could than break some code of clients of this function. That’s why Rust requires us to specify the lifetime information explicitly.

When returning a reference from a function, the lifetime parameter for the return type needs to match the lifetime parameter for one of the parameters.

Since the function can return either x or y reference, the lifetime of both the parameters and the return value will be the same - 'a. The function signature now tells Rust that for some lifetime 'a, the function takes two parameters, both of which are string slices that live at least as long as lifetime 'a. Additionally, the string slice returned from the function will live at least as long as lifetime 'a.

When we pass concrete references to longest, the concrete lifetime that is substituted for 'a is the part of the scope of x that overlaps with the scope of y. In other words, the generic lifetime 'a will get the concrete lifetime that is equal to the smaller of the lifetimes of x and y. Because we’ve annotated the returned reference with the same lifetime parameter 'a, the returned reference will also be valid for the length of the smaller of the lifetimes of x and y.

When we specify the lifetime parameters in the function signature, we’re not changing the lifetimes of any values passed in or returned. Rather, we’re specifying that the borrow checker should reject any values that don’t adhere to these constraints.

Using a function with lifetimes defined

Here’re examples of how lifetimes help in enforcing proper usage of references in terms of scope:

fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result); // GOOD
}
}

In the code above, string2 has shorter lifetime than string1. It means that the lifetime of result will end at the same time as string2‘s. Since we’re using result when the lifetime is still valid, the code compiles and runs.

fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}

In the code above, the situation is similar, however, we’re trying to use (print) result outside of the inner scope, when the lifetime of string2 is already over. Since the lifetime of result is the same as that of string2, result cannot be used at this point. The code fails to compile.

Structs

If a struct contains references as fields, they all need to have lifetime annotations.

struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let langs = String::from("rust,js,c#");
let first_lang = novel.split(',').next().expect("Could not find a ','");
let i = ImportantExcerpt {
part: first_lang,
};
}

This annotation means an instance of ImportantExcerpt can’t outlive the reference it holds in its part field.

Struct Methods

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

Thanks to the elision rules we don’t have to annotate the method explicitly. The lifetime of the output &str is the same as the lifetime of the object (&self).

Static Lifetime

There is a special lifetime - 'static - which marks a value pointed by a reference to be “alive” during the entire execution of the program.

String Literals

All string literals have 'static lifetime. We can annotate them explicitly:

let s1: &'static str = "Explicit static lifetime";
let s1 = "Implicit static lifetime";

The texts are stored directly in program’s binary.

←  Traits
Closures  →
© 2023 Marcin Jahn | Dev Notebook | All Rights Reserved. | Built with Astro.