Rust Fundamentals: Ownership, Errors and Fearless Concurrency · Errors, Collections, Traits and Concurrency
Traits and Generics
Traits define shared behaviour (like interfaces); generics write code once for many types; trait bounds say what a generic type must be able to do. Implement Display, use impl Trait and dyn Trait.
What you'll build
A notification system where emails and SMS messages share a Notify trait, a generic function that works with any type implementing it, and a list holding different kinds of notifiers.
The idea
A trait declares methods that types can implement, like an interface:
trait Notify {
fn send(&self, message: &str) -> String;
fn channel(&self) -> &str { "generic" } // a default method
}
Generics let one function or struct work with many types: fn largest<T: PartialOrd>(items: &[T]) -> &T. The trait bound (T: PartialOrd) says which abilities T must have. Rust generates a specialised copy per type (static dispatch), so generics cost nothing at runtime.
Ways to accept "anything that implements a trait":
fn alert(n: &impl Notify): short syntax for a generic parameter (static dispatch).fn alert<N: Notify>(n: &N)or withwhere N: Notify: the explicit form.Vec<Box<dyn Notify>>: a trait object for mixing different types in one collection (dynamic dispatch through a vtable).
Standard traits you'll implement often: Display (for {}), Debug, Clone, PartialEq, Default, From, Iterator.
The code
use std::fmt;
trait Notify {
fn send(&self, message: &str) -> String;
fn channel(&self) -> &str {
"generic" // default implementation
}
}
struct Email { to: String }
struct Sms { number: String }
impl Notify for Email {
fn send(&self, message: &str) -> String {
format!("email to {}: {}", self.to, message)
}
fn channel(&self) -> &str { "email" }
}
impl Notify for Sms {
fn send(&self, message: &str) -> String {
let short: String = message.chars().take(20).collect(); // SMS gets a short version
format!("sms to {}: {}", self.number, short)
}
}
fn alert(n: &impl Notify, message: &str) -> String {
format!("[{}] {}", n.channel(), n.send(message))
}
fn largest<T: PartialOrd>(items: &[T]) -> &T {
let mut best = &items[0];
for item in items {
if item > best {
best = item;
}
}
best
}
struct Money(i64); // cents
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.0 / 100, self.0 % 100)
}
}
fn main() {
let email = Email { to: "[email protected]".into() };
let sms = Sms { number: "+44 7700 900123".into() };
println!("{}", alert(&email, "Your order has shipped"));
println!("{}", alert(&sms, "Your order has shipped and arrives Friday"));
let notifiers: Vec<Box<dyn Notify>> = vec![Box::new(email), Box::new(sms)]; // mixed types
for n in ¬ifiers {
println!("{} ready", n.channel());
}
println!("largest number: {}", largest(&[3, 9, 4]));
println!("largest word: {}", largest(&["pear", "apple", "zucchini"]));
println!("price: {}", Money(129_905));
}
Output
[email] email to [email protected]: Your order has shipped [generic] sms to +44 7700 900123: Your order has shipp email ready generic ready largest number: 9 largest word: zucchini price: $1299.05
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Reaching for
dyn Traitby default. Generics (impl Trait) are faster and simpler; use trait objects when you need mixed types in one collection or to shrink compile times. - Missing trait bounds.
largestcan't compare items unlessT: PartialOrd; the compiler error says which bound to add. - Implementing foreign traits on foreign types (e.g.
DisplayforVec<T>) isn't allowed (the orphan rule); wrap the type in your own struct ("newtype"), likeMoneyhere. - Overly generic code that's hard to read; start concrete, generalise when there's a second use.
Key points
- Traits define shared behaviour, with optional default methods.
- Generics + trait bounds: one implementation, specialised per type at compile time.
impl Trait(static) vsBox<dyn Trait>(dynamic, mixed collections).- Implement standard traits like
Displayto integrate with the language.
Further reading: Generic types, traits, and lifetimes, Trait objects.
Practice questions
What is the difference between `impl Trait` and `dyn Trait`?
Answer
impl Trait (or a generic with a trait bound) uses static dispatch: the compiler generates code for each concrete type, which is fast. dyn Trait is a trait object using dynamic dispatch through a vtable, allowing different concrete types behind one pointer (e.g. Vec<Box<dyn Trait>>), at a small runtime cost.
What is a trait bound?
Answer
A requirement on a generic type parameter, such as T: PartialOrd or where T: Display + Clone, stating which traits the type must implement so the generic code can use those methods.
Open in the app to mark lessons complete, take notes and track your progress.