Generics
Generics can be used to abstract over concrete types:
struct Point<T> {
x: T,
y: T,
}
This is also extendable to impl
blocks:
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
As implementations are tightly coupled with the type, it is possible to implement
methods for Point<f32>
. However, if you want to implement a method for type T
this has to be generically independent
which is why the syntax forces you to be
“reduntant” and specify the generically independent
type:
impl<T> Point<T> {};
Monomorphization
Rust uses a technique called monomorphization
to turn generic code into specific
code by filling in the concrete types that are used when compiled. This is why
generic code is just as fast as non-generic code.
let integer = Some(5);
let float = Some(5.0);
// Will be compiled to:
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
Traits
Rust favours traits
over inheritance
as it allows for code reuse in a more
flexible way.
trait Summary {
fn summarize(&self) -> String;
}
struct NewsArticle {
headline: String,
location: String,
author: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
Static Dispatching vs Dynamic Dispatching
Static dispatching
is when the compiler knows which method to call at compile
time. This is possible when the compiler knows all the types that might be used
with the code that is calling the method.
Dynamic dispatching
is when the compiler can’t tell which method to call at
compile time. This is possible when the compiler doesn’t know all the types that
might be used with the code that is calling the method.
See an excellent explanation here.
Trait Objects
This allows for objects of different types to be stored in the same data structure.
trait Pet {
fn name(&self) -> String;
}
struct Cat; // Zero sized type
impl Pet for Cat {
fn name(&self) -> String {
String::from("Cat")
}
}
struct Dog; // Zero sized type
impl Pet for Dog {
fn name(&self) -> String {
String::from("Dog")
}
}
fn main() {
let pets: Vec<Box<dyn Pet>> = vec![
Box::new(Cat),
Box::new(Dog { name: String::from("Fido") }),
];
for pet in pets {
println!("Hello {}!", pet.name());
}
}
Note: The memory layout for the previous example is as follows:
Notes:
Traits
may have different sizes, hence it is impossible to do things likelet pets: Vec<Pet> = vec![Cat, Dog];
as the compiler doesn’t know how much memory to allocate for each element.- In the previous example,
pets
holds a fat pointers to the heap. A fat pointer is a pointer to the data and a pointer to thevtable
which contains the information about the methods that can be called on the data. See fat pointers.
Deriving Traits
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 is {:?}", rect1);
}
Trait Bounds
This is very common way to specify that a generic can use or call methods from
a trait. You can do this with T: Trait or impl Trait or using where clauses
.
fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
fn notify<T>(item: T)
where T: Summary // In this case, the where clause allows an extra
// features which is that the type on the left of the ':' can be
// arbitrary, i.e. Option<T> or Vec<T> or any other type.
{
println!("Breaking news! {}", item.summarize());
}
Returning Trait | Trait Parameters
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,
}
}
fn accepts_summarizable(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}