Lifetime Specifiers in Rust
Lifetimes, ownership, and borrowing are some of the fundamental concepts to grasp if I must write fluent rust. Lifetimes, in particular, ensure that functions, closures, structs, enums, and structs that own certain objects remain their owners after other variables have borrowed and/or returned them. It is “some stretch of your program for which a reference could be safe to use”.
They are “structures” we put in place to inform the compiler that objects borrowed remain, even after the borrowing object has been destroyed. We never really have to worry about lifetimes except when we create structs whose fields have borrowed data, or when we want to return borrowed data from a function.
I learned how to build a utility bitcoin library, in Python, in February, and set a goal to port the said library to Rust. In the course of doing so, I ran into a missing lifetime specifier
error that questioned my knowledge of lifetimes.
This article is captures what I learned about lifetimes that helped me resolve my error. It presents a simple explanation of struct and function lifetimes.
Struct Lifetimes
Say, for example, that you have a structure that models a base dimension with two fields: length and breadth, as shown below
If another structure, a Tank
, references an instance of BaseDimension
, then we know that the instance of the BaseDimension
struct must exist before the Tank
struct is created and must remain after the tank is destroyed. We would need to reference the BaseDimension
instance.
To define a lifetime for a structure, we use the following syntax:
For our tank example, we define a lifetime 'base
for the BaseDimension
as shown below, without which you would encounter a missing lifetime specifier error.
You can run a test of the code below on the rust playground. Here is a walk-through:
- [Lines 2–5]: A
BaseDimension
struct is defined and has two fields:length
andbreadth
- [Line 8]: A
Colour
enum with three (3) variancesR
,G
, andB
is defined. - [Lines 11–14]: A generic
Tank
structure with a lifetime of'base
is defined, having two fields:colour
which is an enumeration of R, G, or B, and'base
which is a reference to aBaseDimension
structure with a defined lifetime of'base
. - [Lines 16–21]: Within main, a base instance and a tank instance, with reference to the base, are created. The
println!
macro references the tank instance after it is called. The base and tank instances remain alive. Note thatbase20x40
will be alive for however longtank20x40
is, and can only be safe for destruction after the tank instance has been dropped.
Function Lifetimes
The second case to consider where lifetimes matter is with functions and returning borrowed values from them. The syntax is as shown below. Here we see the creation of two lifetimes 'lt_a
and 'lt_b
belonging to instances of TypeA
and TypeB
respectively. This function func
returns a tuple of references to types TypeA
and TypeB
.
An illustrative example to show the use of lifetimes in a function is shown in Listing 2.
A walk-through of the example is listed below:
- [Lines 2–6]: A
City
struct with three String fields:name
,country
,continent
is defined. - [Lines 9–12]: A
Hospital
struct with a lifetime of'city
which will reference a borrowed instance ofCity
- [Lines 14–16]: A function
healthcare_centres
with lifetime specifiers'h
and'city
for the parameters that are references to instances of&Hospital
andCity
. Note how the struct lifetime is passed and the type of the return value: a tuple of borrowed objects. - [Lines 18–34]: Within main, instances of
City
andHospital
are created, with their references passed as arguments tohealthcare_centre
. Theprintln!
macros show how objects referenced by the function and tracked by their lifetimes outlive the function that called them.
The Rust compiler knows to save referenced instances in such a manner that it will outlive the function call. With the defined lifetimes, Rust can assess the relationship between the arguments passed to the function and the values returned in a safe way. We can tell that the reference(s) of the returned value from the function points to the arguments passed, and no where else.
Note:
- A special lifetime
'static
is reserved by Rust for objects that must remain in memory until the program ends. - Rust infers lifetimes. They (lifetimes) are only needed in defining functions or types.
Conclusion
Lifetimes are ways the compiler keeps track of what objects can/must remain alive as they are borrowed by other objects. This is how I like to think about them. If you liked this article and would like to see where I am applying some of the new Rust knowledge I have gained over the past two weeks, please check out bitlib — my attempt to port a bitcoin utility library to Rust.
Looking forward to any feedback I can get.
References
- Blandy Jim, Orendorff, J., & Tindall, F, S, Leonora. (2021). Programming rust: Fast, safe systems development
(2nd ed.) O’Reilly Media Inc. - Rust Programming: The Complete Developer’s Guide by Jason Lennon