Generic: Data Type & Traits & Lifetime
Comprehension
Generic form 1: Data Types
// generic declaration - struct struct Point<T> { x: T, y: T, } let point = Point { x: 5, y: 4 }; // must be the same type struct PointDiffType<T, U> { x: T, y: U, } let point_diff_type = PointDiffType{ x: 5, y: 4.0 }; // different types // generic declaration - impl impl<T> Point<T> { fn x(&self) -> &T { &self.x } } // generic declaration - enum enum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E), } // a mixed implementation struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { // generic flexibility allows fields to be mixed to create a third generic instance // this syntax can be used for middleware fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }Using generics does not affect runtime performance
The monomorphization process converts generics to concrete types during compilation
// for example, generic code using Some let integer = Some(5); let float = Some(5.0); // monomorphization produces the following code at compile time enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); }Generic form 2: Traits & Trait Bounds
A trait describes shared behavior; in basic usage, it can be applied to structs, function parameters, and return values
For a struct, a trait specifies the methods that the struct must provide, similar to a Java
interfaceimpl structA for traitAFor a generic type, a trait specifies that its concrete type must have certain traits (also called trait bounds) to use a given set of functions
impl<T: traitA + traitB + ...> structA<T> {...}For function parameters, traits restrict which arguments can be passed (polymorphism)
pub fn notify(item: &impl Summary) {...}For return values, a trait guarantees that the returned value implements it
// Defining a Trait pub trait Summary { fn summarize(&self) -> String; } // Implementing a Trait on a Type pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } // Default Implementations pub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } } // default implementations can call other impls pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } } // Traits as Parameters pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); } // generic types can specify trait bounds pub fn notify<T: Summary>(item1: &T, item2: &T) {} // multiple traits, case 1 - use the + operator pub fn notify(item: &(impl Summary + Display)) { pub fn notify<T: Summary + Display>(item: &T) { // multiple traits, case 2 - use where fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug // Returning Types that Implement Traits fn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } } // trait bounds ensure that only generic types with the required trait have the method use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } }Generic form 3: Lifetimes
&i32 // a reference &'a i32 // a reference with an explicit lifetime &'a mut i32 // a mutable reference with an explicit lifetime // generally used in function definitions fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // can be used with structs struct ImportantExcerpt<'a> { part: &'a str, } // lifetime elision - applying three rules fn first_word(s: &str) -> &str {...} // the static lifetime remains available for the entire program let s: &'static str = "I have a static lifetime.";The compiler uses three rules to infer lifetimes for references when they are not explicitly annotated
The first rule is that each parameter that is a reference gets its own lifetime parameter.
First: each reference parameter gets its own lifetime parameter.
The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
Second: if there is exactly one input lifetime parameter, it is assigned to all output lifetime parameters.
The third rule is if there are multiple input lifetime parameters, but one of them is
&selfor&mut selfbecause this is a method, the lifetime ofselfis assigned to all output lifetime parameters.Third: if there are multiple input lifetime parameters and one is
&selfor&mut self(a method), the lifetime ofselfis assigned to all output lifetime parameters.Combining the three forms
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 } }
Origin
…
Performance of Code Using Generics
You might be wondering whether there is a runtime cost when you’re using generic type parameters. The good news is that Rust implements generics in such a way that your code doesn’t run any slower using generic types than it would with concrete types.
Rust accomplishes this by performing monomorphization of the code that is using generics at compile time. Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled.
…
The compiler uses three rules to figure out what lifetimes references have when there aren’t explicit annotations. The first rule applies to input lifetimes, and the second and third rules apply to output lifetimes. If the compiler gets to the end of the three rules and there are still references for which it can’t figure out lifetimes, the compiler will stop with an error. These rules apply to fn definitions as well as impl blocks.
The first rule is that each parameter that is a reference gets its own lifetime parameter. In other words, a function with one parameter gets one lifetime parameter: fn foo<'a>(x: &'a i32); a function with two parameters gets two separate lifetime parameters: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); and so on.
The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters: fn foo<'a>(x: &'a i32) -> &'a i32.
The third rule is if there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters. This third rule makes methods much nicer to read and write because fewer symbols are necessary.