Generic Types, Traits, and Lifetimes1
To run the program:
$ cargo run --bin generics-traits-lifetimes
Compiling generics-traits-lifetimes v0.1.0 ...
Rust has generics, similar to other languages, to improve code reusability:
Option<T>
Vec<T>
HashMap<K, V>
Result<T, T>
Generics apply to both types and functions:
struct Point<T> {
x: T;
y: T;
}
fn first<T>(items: &[T]) -> &T {
&items[0]
}
And method definitions:
impl<T> Point<T> {
fn new(x: T, y: T) -> Point<T> {
Point { x, y }
}
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &T {
&self.y
}
}
By adding restrictions on what T
can be, we can write largest
:
fn largest<T: std::cmp::PartialOrd>(items: &[T]) -> &T {
let mut largest = &items[0];
for item in items {
if item > largest {
largest = item;
}
}
largest
}
NOTE: Rust uses monomorphization at compile-time, there is no runtime cost to generics.
A trait defines functionlality that a type can share with other types. They are similar to interfaces in other languages, except that they are static (compile-time):
trait Summary {
fn summarize(&self) -> String;
// Can be provided, but otherwise uses this fallback default implementation.
fn promotion(&self) -> String {
String::from("No promotions attached")
}
}
struct Tweet {
username: String,
message: String,
}
/// Satisfies the Summary trait for Tweet.
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{} by {}", self.message, self.username)
}
}
fn print_summary(summary: &impl Summary) { /* ... */ }
NOTE: You can only declare traits on types local to your crate.
For generic bound traits and for multiple traits:
fn notify1<T: Summary>(item1: &T, item2: &T) { /* ... */ }
fn notify2<T: Summary + Display>(item1: &T, item2: &T) { /* ... */ }
Can also use where
clause to make the generics more readable:
fn notify3<T>(item1: &T, item2: &T)
where
T: Summary + Display,
{
/* ... */
}
There are some restrictions for return types, for example this doesn't work:
// Apparently this type of behavior is covered in Chapter 17.
fn returns(switch: bool) -> impl Summary {
if switch {
Article { ... }
} else {
Tweet { ... }
}
}
Trait bounds can also conditionally implement methods:
impl<T: Display + PartialOrd> Point<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest value is x = {}", self.x);
} else {
println!("The largest value is y = {}", self.y);
}
}
}
Also conditionally implement a trait for any type that implements another trait:
impl<T: Display> ToString For T {
/* ... */
}
// This is how things like this work in the SDK:
let s = 3.to_string();
Every reference in Rust has a lifetime (scope in which the reference is valid). Most of the time, lifetimes are implicit and inferred (the same way that most types are inferred). We only annotate with lifetimes when the lifetime of references could be related in a few different ways.
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
The main aim of lifetimes is to prevent dangling references, which otherwise would cause a program to reference data other than the data it's intended to reference:
fn main() {
let r;
{
let x = 5;
r = &x;
// ^^ Error: Borrowed value does not live long enough.
}
println!("r: {}" , r);
}
The borrow checker tries to infer lifetimes, and determines it won't work:
/// As the `'b` lifetime is shorter than the `'a`, the project is rejected.
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}" , r); // ---------+
}
To fix the code:
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
For functions and structs, it's not as simple, so we use lifetime generics:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
struct Excerpt<'a> {
part: &'a str,
}
It's worth mentioning some common inference patterns are programmed directly into Rust's compiler, called lifetime elison rules. One simple example:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
NOTE: Pre Rust 1.0, you would have had to write the signature as:
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
The rules are:
-
The compiler assigns a lifetime parameter to each reference parameter:
// A function with 1 reference. fn foo<'a>(x: &'a i32) { /* ... */ } // A function with 2 references. fn foo<'a, 'b>(x: &'a i32, y: &'b i32) { /* ... */ }
-
If there is exactly one input lifetime parameter, assign to the output:
fn foo<'a>(x: &'a i32) -> &'a i32 { /* ... */ }
-
If there are multiple input lifetime parameters, but one is
&self
or&mut self
because this is a method, the lifetime ofself
is assigned to all output lifetime parameters:impl Foo { fn foo<'a, 'b>(&'a self), y: &'b i32) -> &'a 132 { /* ... */ } }
Another example of &self
:
impl<'a> ImportantExcerpt<'a> {
// No explicit lifetime annotations are required because of Rule 3.
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
Finally, there is also a 'static
lifetime, or the entire program duration:
// All string literals have the 'static lifetime, but here is it explicitly:
let s: &'static str = "I have a static lifetime.";
And, putting it all together:
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}