Traits
In Rust, traits define shared behavior that types can implement. They're similar to interfaces in C#, but with some important differences.
What is a Trait?
A trait is a collection of methods that a type must implement to satisfy the trait. For example a DataAccess interface in .NET might be defined as:
public interface IDataAccess {
public Task Store(User user);
public Task<User> WithEmailAddress(string emailAddress);
}
To implement a similar thing in Rust using traits, it would look like:
pub trait DataAccess: Send + Sync {
fn with_email_address(&self, email_address: &str) -> Option<User>;
fn store(&self, user: User);
}
This trait defines two methods that any implementing type must provide. Like an interface in .NET, it specifies a contract without implementation details.
Implementing Traits
As you learned in an earlier module, implementations for a given struct are defined in a seperate impl {} block. To implement a trait, you use the impl Trait for Type syntax:
impl DataAccess {
// Implementations specific to the DataAccess trait
}
// Implementations for the `InMemoryDataAccess` trait
impl DataAccess for InMemoryDataAccess {
fn with_email_address(&self, email_address: &str) -> Option<User> {
self.users.lock().unwrap().iter()
.find(|u| u.email_address() == email_address)
.cloned()
}
fn store(&self, user: User) {
self.users.lock().unwrap().push(user);
}
}
This is similar to implementing an interface in C#, but note that the implementation are separate from the type definition.
Traits vs. Interfaces: Key Differences
- Orphan Rule: In Rust, you can only implement a trait for a type if either the trait or the type is defined in your crate
- Default Implementations: Traits can provide default implementations for methods
- Coherence: A type can only have one implementation of a trait
- Static Dispatch: Rust typically resolves trait methods at compile time (zero cost)
Async Traits
When working with async functions in traits, you'll encounter a limitation: Rust doesn't (currently, but it's in progress) directly support async functions in traits yet. This is where the async_trait crate comes in:
use async_trait::async_trait;
#[async_trait]
pub trait AsyncDataAccess {
async fn with_email_address(&self, email_address: &str) -> Option<User>;
async fn store(&self, user: User);
}
The #[async_trait] macro transforms the async methods into functions that return Future implementations, making them compatible with Rust's current trait system.
As well as adding #[async_trait] on the trait itself, you also need to add the macro to the implementation block as well:
use async_trait::async_trait;
#[async_trait]
impl AsyncDataAcess for DataAccessStruct {
async fn with_email_address(&self, email_address: &str) -> Option<User> {
// Actual implementatoons
}
async fn store(&self, user: User) {
// Actual implementatoons
}
}