Generics & Traits
By using Generics and Traits we can leverage their combined power to write efficient, reusable, and type-safe code.
Generics
- Generics used to create definitions for items like function signatures or structs, which we can then use with many different concrete data types.
- So instead of writing separate implementations for each type, you define a generic type that can be substituted with concrete types when the code is used.
- Represented by angle brackets (
<>
) and often denoted byT
,U
, or other descriptive names. - Generics in Rust are resolved at compile time, ensuring that invalid types cannot be used.
- Rust generates optimized code for each concrete type used with a generic at compile time, avoiding runtime overhead.
Generic Functions
-
Functions can use generics to operate on multiple types.
fn hello<T: std::fmt::Display>(name: T) {
println!("hello {}!", name);
}
fn main() {
hello(777);
hello("World");
}
Generic Structs
-
Structs can hold values of any type using generics.
struct Point<T> {
x: T,
y: T,
}
let int_point = Point { x: 1, y: 2 };
let float_point = Point { x: 1.7, y: 2.4 };
Generic Enums
-
Enums can also use generics, as seen with Option or Result.
enum Option<T> {
Some(T),
None,
}
let int_option: Option<i32> = Option::Some(10);
let float_option: Option<f64> = Option::Some(5.7);
Traits
- A trait is similar to an interface in other programming languages.
- It defines a set of methods that a type must implement, allowing you to specify what functionality a type provides without dictating how it provides it.
- Implementing a trait on a type is similar to implementing regular methods.
- The difference is that after
impl
, we put the trait name we want to implement, then use thefor
keyword, and then specify the name of the type we want to implement the trait for. - Traits can provide default method implementations that types can override.
- The
impl Trait
syntax works for straightforward cases but is actually syntax sugar for a longer form known as a trait bound. - We can also specify more than one trait bound using
+
syntax.
Defining a Trait
-
A trait defines methods that other types can implement.
trait Summary {
fn summarize(&self) -> String;
}
Implementing a Trait
-
Implementing a trait similar like implementing method by put the trait name after
impl
and then specify the type afterfor
.struct NewsArticle {
headline: String,
location: String,
author: String,
content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
struct Tweet {
username: String,
content: String,
reply: bool,
retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Default Implementation
-
Traits can provide default method implementations that types can override.
trait Summary {
fn summarize(&self) -> String {
"Default Summary".to_string()
}
}
Traits as Parameters
-
Traits can be used as parameters using
impl Trait
syntax.fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
} -
The
impl Trait
syntax works for straightforward cases but is actually syntax sugar for a longer form known as a trait bound; it looks like this:pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
} -
We also can have multiple parameters that implement a trait.
-
For example this function have two parameters that implement Summary:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
-
We can also do it using generics.
-
But in the example below the generic type
T
specified as the type of theitem1
anditem2
parameters constrains the function such that the concrete type of the value passed as an argument foritem1
anditem2
must be the same.pub fn notify<T: Summary>(item1: &T, item2: &T) {
-
We can also use the impl Trait syntax in the return position to return a value of some type that implements a trait.
fn returns_summarizable() -> impl Summary {
NewsArticle {
headline: "Good News",
location: "Indonesia",
author: "Someone",
content: "Something",
}
}
Specifying Multiple Trait Bounds
-
We can also specify more than one trait bound using
+
syntax.pub fn notify(item: &(impl Summary + Display)) {
-
The
+
syntax is also valid with trait bounds on generic types.pub fn notify<T: Summary + Display>(item: &T) {
Generic Traits
-
Traits can also be generic, allowing them to operate over a range of types.
trait Magic<T> {
fn add(&self, other: T) -> T;
}
impl Magic<String> for i32 {
fn add(&self, other: String) -> String {
format!("{}:{}", self, other)
}
}
fn main() {
println!("{}", 71.add("Magic".to_string()));
}
Extending Existing Traits
-
We can extend existing traits by combining them or adding additional behavior.
trait Printable: Display {
fn print(&self) {
println!("{}", self);
}
}
impl Printable for i32 {}
impl Printable for String {}
let number: i32 = 71;
number.print(); // Output: 42
let text = String::from("FooBar");
text.print(); // Output: FooBar