RustBrock/Traits.md
2025-02-05 11:14:05 -07:00

405 lines
16 KiB
Markdown

# Traits
This is used to defined shared behavior
A *trait* defines the functionality a particular type has and can share with other types.
Traits are used to define shared behavior in an abstract way.
We can use *trait bounds* to specify that a generic type can be any type that has certain behavior
Note: Traits are similar to a feature often called *interfaces* in other languages, but there are some differences in Rust
## Defining a Trait
A type's behavior consists of the methods we can call on that type
Some types can share the same behavior if we can call the same methods on all of those types.
Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish something.
Here is an example to lay this out
lets say you have multiple structs that hold various kinds and amounts of text
`NewsArticle` struct that holds a news story filed in a particular location
`Tweet` that can have, at most, 280 characters along with metadata that indicates whether it was a new tweet, a retweet, or a reply to another tweet
We want to make a media aggregator library crate named `aggregator` that can display summaries of data that could be stored in a `NewsArticle` or `Tweet`
In order to do this we need a summary from each type, this would be done by calling a `summarize` method on an instance
Example definition of a trait for the above situation
```rust
pub trait Summary {
fn summarize(&self) -> String;
}
```
Inside the trait named scope is where method signatures are defined
After the method signature, instead of providing an implmentation within the curly brackets, you use a semicolon
Each type implementing this trait must provide its own custom behavor for the body of the method
The compliler will enfore that any type that has the `Summary` trait will havee the method `summarize` defined with this signature
Note: a trait can habe multiple methods in its body, each method signature are listed one per line and each lne ends in a semicolon
## Implementing a Trait on a Type
Now that the desirred signatures of the `Summary` trait's methods, we can implement it on the types in our media aggregator
Here is an example where the trait is implmented for both `NewsArticle` and `Tweet` structs
```rust
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)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
```
Implementing this is similar to implementing regular methods.
The difference is after the `impl` we put the trait name we want to implement, then use the `for` keyword, then put the specify the name of the type we want to implement the trait for.
In the `impl` block we put the method's singaure that the trait has defined, but instead of a semicolon you put the implementation with its behavior in curly braces.
Now that the library has implemented thr `Summary` trait on `NewsArticle` and `Tweet`, users of the crate can call the trait methods on instances of `NewsArticle` and `Tweet` in the same way regular methods, the only difference is that you need to bring the trait into scope as well as the type
Here is an example of how a binary crate could use our `aggregator` library crate
```rust
use aggregator::{Summary, Tweet};
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
```
Other cates that depend on the `aggregator` crate can also bring the `Summary` trait into scope to implement `Summary` on their own type
One restriction to note is that we can implement a trait on a type only if either the trait or the tpye, or both are local to your crate
For example, you can implement standard library traits like `Display` on custom tpye like `Tweet` as part of our `aggregator` crate functionality because the type `Tweet` is local to our `aggregator` crate
We could also implement `Summary` on `Vec<T>` in our `aggregator` crate because the trait `Summary` is local to our `aggregator` crate
You cant implement external trait on external types
For example you cant implement `Display` trait on `Vec<T>` within our aggregator create they are both defined in the std library which are both not local to our `aggregator` crate
This restriction is part of a property called *coherence*, and more specifically the *orphan rule*, this is named becuase the parent type is not present
This rule ensures that other people's code can't breake your code and vice versa.
Without this rule the compiler could or would get confused about two implmentations of the same trait on the same type in tow different crates
## Default Implementations
This is sometimes useful to have defualt behavior for some or all of the methods in a trait instead of requiring implementaitons for all method on every type.
This could be overridden by implementation on the type itself of the trait with the speicific singature
Here is an example of this
```rust
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
```
To use the defualt implementation to summarize instances of `NewArticel`, we specifiy an empty `impl` block with `impl Summary for NewsArticle {}`
Even though `summarize` is not defined on `NewsArticle` directly we have a defualt implementation and we specified that `NewsArticle` implements the `Summary` trait
The provided defualt implementation allows you to do something like this, even though the implemntation function scope is empty
```rust
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
```
This still prints `New article available! (Read more...)`
Default implmentations can call other methods in the smae trait, even if other methods don't have a default implementations.
For example
```rust
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
```
This default implementation only requires the signuare in order to use, it relies that the implementation in the type, which the compiler ensures
To use thus version of `Summary`, we only need to define `summarize_author` when we implement the trait on a type
Because we have implemented `summarize_author`, the `Summary` trait has given the behavior of the `summarize` method without requiring us to write any more code
Here is the use of the `summarize_author` trait method
```rust
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
```
This would print `1 new tweet: (Read more from @horse_ebooks...)`
Note: it is impossible to call the default implementation from an overridden implementation of that same method
## Traits as Parameters
This can be used to only accept types that have implemented the trait
Here is an example
This is a function calls the `summarize` method on its `item` parameter which is some type that implenets the `Summary` trait
To do this we need to use the `impl Trait` syntax
```rust
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
```
Instaed of a create tpye ofr the `item` paremeter, use instead specify the `impl` keyword and the trait name
This allows for the parameter to accept any type that implemetns the specified trait
In the body of the function we can call any method on item that come from the `Summary` trait, like `summarize`
We cant call any type that doesn't implement the `Summary` trait like `String` or `i32`, this wont compile either and give an error stating that it needs to implement the methods from the `Summary` trait
### Trait Bound Syntax
The `impl Trait` syntax works for straitforward cases but is actually syntax sugar for a longer form known as a *trait bound*
It looks like this
```rust
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
```
This is longer but it is equivalent to the example before, but way more verbose and boilerplate
We place trait bounds with the declaration of the generic type parameter after a colon and inside angle brackets
The `impl Trait` syntax is convenient and makes for more concise code in simple cases, while the fuller trait bound syntax can express more complexity in other cases
For example we can have two parameters that implement `Summary`
Here is the `impl Trait` syntax
```rust
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
```
Here is the same thing with the trait bound syntax
```rust
pub fn notify<T: Summary>(item1: &T, item2: &T) {
```
The `impl Trait` is appropriate if we want the function to allow `item1` and `item2` to have different types `<X, Y>` (for example)
If you want to force the two parameters to have the same type then you have to use the trait bound syntax
### Specifying Multiple Trait Bounds with the + Syntax
You can specifiy more than one trait bound, lets say you need methods from two traits, here is an example that uses the `Summary` trait and the `Display trait`
You use the `+` after the previous one to specify any additional traits
In `impl Trait` syntax
```rust
pub fn notify(item: &(impl Summary + Display)) {
```
In trait bound syntax
```rust
pub fn notify<T: Summary + Display>(item: &T) {
```
With the two trait bounds specified, the body of `notify` can call `summarize` and use `{}` to format item.
### Clearer Trait Bounds with where Clauses
Using too many trait bounds has its downside of unclear code, this is multipled by each generic needs its own trait bound, so types with multiple parameters can contain lots of trait bound info between the functions name and its paratmeter list
This can make the signautre hard to read
There is an alternative in rust using the `where` keyword clause after the function signature
Here is the orignal bad code
```rust
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
```
Here is the equivalent with the `where` clause
```rust
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
```
This fucntion's signaure is less cluttered, the function name, parameter list, and return type are close toheter, similar to a function with lots of trait bounds
## Returning Types That Implements Traits
You can also use the `impl Trait` syntax in the retrun a value of some type that implements a trait
Here is an example of this
```rust
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,
}
}
```
This specifies that the function returns some tpye that implements the `Summary` trait without naming the concete type
The code that calls this function doesnt need to know the concrete type it returns, like in this case
This is useful to specify a return type by the type that it implements, especially in the context of closures and iterators
Closures and iterators create tpyes that only the complier knows or tpyes that are very long to specify
The `impl Trait` syntax lets you specify that a function returns som type that implements the `Iterator` trait without needs to write out a very long type.
You can only use `impl Trait` if you are returning a single type
For example this code that returns either a `NewsArticle` or a `Tweet` with the return tpye specified as `impl Summary` wouldn't work
```rust
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
}
```
Returning either a `NewsArticle` or a `Tweet` isnt allowed because the compiler has restrictions on how `impl Trait` syntax is implelented
This will be covered in a later section
## Using Trait Bounds to Conditionally Implement Methods
By sing a trait bound with an `impl` block that uses generic tpye parameters, we can implement methods conditionally for tpyes that implement the specified traits
For example the type `Pair<T>` always implements the `new` function to reutnr a new instance of `Pair<T>`
But the net `impl` block `Pair<T>` only implements the `cmp_display` method if its inner tpye `T` implements the `PartialOrd` trait that enalbes comparision *and* the `Display` trait that enables printing
```rust
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);
}
}
}
```
We cna also conditionally implement a trait for an type that implements another trait.
Implementations of a trait on an type that satifies the trait bounds are called *blanket implementations*
The are used extensively in the Rust std library
An example of this is the `ToString` trait on any tpye that implements the `Display` trait
The `impl` block in the std library look similar to this code
```rust
impl<T: Display> ToString for T {
// --snip--
}
```
we can call the `to_string` method defined by the `ToString` trait on any tpye that implements the `Display` trait because of the blanket implementation in the std library
Here is an example, we can turn integers into their corresponding `String` values like this because integers implement `Display` trait
```rust
let s = 3.to_string();
```
Traits and trait bounds let us write code that use generic type parameters to reduce duplication but also specifiy to the compiler that we want a genric tpye with particular behavior.
The compiler can then ue this into to check the concrete types used in your code to provide the correct behavior.
In dynamically typed languages we would get a runtime error if we called a method on a type that didnt define the method.
Rust provides these erros at compile time so that you are forced to fix these problems could even possibily happen.
This also makes it so that we have to write code that check for the behavior becuase the compiler already check for us.
This improves performance without having to give up the flexibility of generics.