avatarapplied.math.coding

Summary

Understanding lifetimes in Rust is crucial for ensuring memory safety and avoiding reference errors without needing a garbage collector.

Abstract

The article discusses the concept of lifetimes in Rust, emphasizing their importance in maintaining memory safety by managing references effectively. It explains how lifetimes prevent references from outliving the data they point to, thus avoiding dangling references. The author illustrates common scenarios where Rust's borrowing rules and lifetime annotations help catch potential errors at compile time. By providing examples of code that would lead to dropped values and the corresponding lifetime annotations to fix them, the article demonstrates how lifetimes are a fundamental part of Rust's ownership model. The author also acknowledges the learning curve associated with lifetimes, especially for those new to Rust, but highlights the benefits of this system in writing efficient and safe code.

Opinions

  • The author believes that lifetimes in Rust, although initially challenging, are a powerful feature that adds a layer of security to programs.
  • They assert that Rust's ownership and borrowing rules, including lifetimes, eliminate the need for a garbage collector, leading to efficient memory management.
  • The author suggests that while writing sophisticated code involving lifetimes, one might encounter compiler bugs, but these cases are rare, especially in typical web backend development.
  • The author expresses confidence in Rust's compiler to automatically detect and prevent many common reference-related errors, reducing the need for manual memory management.
  • They also imply that the benefits of using lifetimes in Rust, such as the ability to sort large files efficiently, outweigh the initial complexity of understanding the concept.

Lifetimes in Rust are not that hard to understand.

When learning Rust, the first time one encounters a compilation error that states about lifetimes, can be a confusing experience. The first thing to understand about lifetimes is that it provides yet another level of security to our program.

As we all know and probably do, sharing references is a lot better than passing data by copying their values. Though, references are easily out of scope when not considered carefully.

So for instance

let mut r: &Vec<u32>;
{
   let x = vec![1,2,3];
   r = &x;
}     // <-- the value owned by x is dropped
r    // <-- r points to a dropped value: ERROR!

The “life” of x‘s value is bound to the code block wrapped by { ... }. Therefore, the “life” of a reference like r pointing to this value, exactly has the same “time of life”.

All good — and the Rust compiler would complain when detected an situation like the one above.

Another (C++ -traditional) example is the following:

fn f() -> &Vec<u32> {
  let x = vec![1,2,3];
  &x  // <-- life of x's owned value ends: ERROR!
}

Also here, the compiler would complain since the returned references points to a dropped value.

So far, there was no need for any lifetime parameter because Rust was able to detect all the bad scenarios for us. But let us look at this one:

struct S{
  x_ref: &Vec<u32>   
}     // does not compile!

fn f() -> S{
  let x = vec![1,2,3];
  S{ x_ref: &x }
}  // <-- life of x's owned value ends

In comparison with the previous example, here we kind of try to trick the compiler. Instead of a reference, we now try to return a variable that owns its value. But this variable, i.e. the instance of S, wraps a reference to the value owned by x. However, this reference after being returned again points to a dropped value.

Rust, will not complain at the same position as before, but instead does not allow us to declare a struct containing references without lifetime bounds. So it forces us to write:

struct S<'a>{
  x_ref: &'a Vec<u32>   
}     

fn f<'a>() -> S<'a> {
  let x = vec![1,2,3];
  S{ x_ref: &x }
}  // <-- life of referrence x_ref is shorter than life of S: ERROR!

The compiler now detects a breach in the use of S, that is, it is tried to have an instance of S containing a reference which lives shorter than the instance itself. Note, the instance lives longer since it is returned, whereas the by x_ref referred value is getting dropped at the end of the function.

As you can see, lifetime parameters is just a way the compilers ensures its checks do run properly. Most of the time, the compiler is telling us automatically where these bounds are required.

Most important to note, lifetimes do not alter be any means the “life” of a value! Lifetimes are to be considered as additional information of a contract that we do have with the compiler.

For instance, you could have a function looking like

fn f<'a, 'b, T>(x: &'a T, y: &'b T, z: &'a T) -> &'a T {
    ...
}

No matter what the function’s body is, this just tells us the following thing:

The return value of f cannot be used longer than x‘s and z’s life is. An alternative interpretation is this: The use of f‘s return value, determines how long the passed references x and z must live at least.

The latter interpretation once more underlines that lifetime parameter are additional contract information the compiler from time to time requests from us.

Why is this all worth the hassle?

Rust’s concept of ownership and rules of borrowing is a lot new material to learn when coming from traditional languages. Especially, with regard to the concept of lifetime parameter that introduces yet another layer of syntactical symbols, you might be tempted to ask: “Why not just keep the value until no reference points to it anymore?” The answer, as easy it is, as easy can be forgotten: “By this, Rust does not need a garbage collector, nor you have to garbage collect on yourself.” What this actually means, can be felt in my article about sorting large files (see here).

Although I am here trying to describe Rust’s ‘lifetime’ as an easy and friendly concept, one note of care remains. The more you dive into sophisticated code that uses trait bounds on referenced types, the required lifetime parameter sometimes can lead to a very weird sort of compilation error. In some cases it is even a result of a bug in Rust’s compiler. But under normal circumstances, especially for plain web back-ends, you probably never encounter such things.

Thank you for reading!

Rust
Lifetime
Programming Languages
Borrow Checker
Scientific Computing
Recommended from ReadMedium