Traits and Generics in Rust
A trait defines behaviour that types can implement, similar to an interface. Generics let functions work with any type that implements the traits they need.
Defining and implementing a trait
trait Notifier {
fn send(&self, to: &str, message: &str) -> Result<(), String>;
// default method
fn send_all(&self, recipients: &[&str], message: &str) {
for r in recipients {
let _ = self.send(r, message);
}
}
}
struct Email;
struct Sms { sender_id: String }
impl Notifier for Email {
fn send(&self, to: &str, message: &str) -> Result<(), String> {
println!("email to {to}: {message}");
Ok(())
}
}
impl Notifier for Sms {
fn send(&self, to: &str, message: &str) -> Result<(), String> {
println!("sms from {} to {to}: {message}", self.sender_id);
Ok(())
}
}
Generic functions with trait bounds
fn alert<N: Notifier>(notifier: &N, user: &str) {
notifier.send(user, "Your order has shipped").ok();
}
// Same thing, shorter syntax
fn alert2(notifier: &impl Notifier, user: &str) { /* ... */ }
// Several bounds
fn log_and_compare<T: std::fmt::Debug + PartialOrd>(a: T, b: T) -> T {
println!("{a:?} vs {b:?}");
if a > b { a } else { b }
}
Generics are monomorphised: the compiler creates a specialised copy for each concrete type, so there's no runtime cost.
Trait objects: dyn Trait
When you need a collection of different types, use a trait object:
let channels: Vec<Box<dyn Notifier>> = vec![
Box::new(Email),
Box::new(Sms { sender_id: "SHOP".into() }),
];
for ch in &channels {
ch.send("asha", "Sale starts tomorrow").ok();
}
dyn uses dynamic dispatch through a vtable: slightly slower, but flexible.
Traits you'll derive all the time
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
struct Settings { theme: String, notifications: bool }
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.