Generics
-
Generics allow generalizing functions and types to reduce code duplication.
-
Generics are used in type parameters.
-
Type parameters are declared inside angle brackets and are conventionally uppercase letters (eg.
<T>
).
Using Generics
fn main() {
let v1: Vec<i32> = vec![1, 2, 3, 4, 5];
let v2: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let v3: Vec<&str> = vec!["crab", "penguin", "language"];
}
The type of a vector is Vec<T>
.
fn main() {
let maybe_int: Option<i32> = Some(5);
let maybe_int_2: Option<i32> = None;
}
The type of an option is Option<T>
.
Generics in Structs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer: Point<i32> = Point { x: 5, y: 10 };
let float: Point<f32> = Point { x: 1.0, y: 4.0 };
}
-
The type
Point<T>
is generic over the typeT
-
The fields
x
andy
can be any typeT
, but they must be the same type.
Generics in Enums
enum Either<L, R> {
Left(L),
Right(R),
}
fn main() {
let int_or_float: Either<i32, f32> = Either::Left(5);
let int_or_float_2: Either<i32, f32> = Either::Right(100.0);
let int_or_int: Either<i32, i32> = Either::Left(100);
}
-
The type
Either<L, R>
is generic over the typesL
andR
-
L
andR
can be the same type, but they don’t have to be!
Generics in Functions
Without generics:
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> &char {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
Generics in Functions (2)
With generics:
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
-
The function
largest
is generic over typeT
, which normally can be any type… -
BUT since we want to compare
item > largest
, we need to restrict the possible typesT
can be to types that can be compared (implements thePartialOrd
trait)
Generics in Methods
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &T {
&self.y
}
}
The <T>
after impl
allows us to refer to the T
in Point<T>
inside the impl
block.
Generics in Methods (2)
Concrete methods can be implemented for generic types by specifying a concrete type for generics:
struct Point<T> {
x: T,
y: T,
}
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Performance of Generics
Rust performs monomorphization of generic code, meaning the compiler turns generic code into specific code by generating separate code for each concrete type:
let integer = Some(5);
let float = Some(5.0);
is automatically turned at compile time into:
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);
}
Traits
- A type is a set of values and a set of operations we can perform on those values
- The set of operations are the methods we can call on that type
- Different types share the same behavior if we can call the same methods on all of those types
- A trait is a grouping of methods to define a set of behaviors necessary to accomplish some purpose
trait Summary {
fn summarize(&self) -> String;
}
The Summary
trait requires implementors to define a method named summarize
that takes &self
and returns a String
.
What the implementor does under the hood is no concern to the trait as long as the method signature is upheld.
Trait Implementation
A struct can implement a trait by using the impl Trait for Type
syntax:
pub struct NewsArticle {
pub headline: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {}", self.headline, self.author)
}
}
Trait Implementation (2)
The same trait can be implemented by multipe types:
pub struct Tweet {
pub username: String,
pub content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Default Trait Implementation
Traits can have default implementations:
trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {}
The corresponding implementation block for a type can then be left empty to use the default implementation of the trait.
Default Trait Implementation (2)
Default trait implementations can call other trait methods:
trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct NewsArticle {
pub headline: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize_author(&self) -> String {
format!("Author {}", self.author)
}
}
Trait bounds in functions
Functions can accept “anything that implements a particular trait”. The following are all equivalent:
-
With
impl Trait
syntax:fn print_summary(item: &impl Summary) { println!("Summary: {}", item.summarize()); }
-
With generics and trait bounds:
fn print_summary<T: Summary>(item: &T) { println!("Summary: {}", item.summarize()); }
Multiple Trait Bounds
Multiple trait bounds can be specified with the +
syntax:
fn print_summary<T: Summary + Display>(item: &T) {
println!("Summary: {}", item.summarize());
}
The type T
must implement both the Summary
and Display
traits.
For functions with many generics and trait bounds, using where
clauses can make functions signature more readable:
fn print_summary<T>(item: &T) where T: Summary {
println!("Summary: {}", item.summarize());
}
fn some_function_2<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug
{
// Function body
}
Conditional Methods
Trait bounds can be used to conditionally implement methods for types that implement the specified traits.
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
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);
}
}
}