Lesson 29 of 94
Traits: Rust's Interfaces
A trait defines behavior that types can share. It is central to the language: nearly every abstraction, including printing, comparing, iterating, and operators, is a trait.
Key points about traits:
- A type implements a trait in a separate
impl Trait for Typeblock, not in the type's declaration. You can even implement your own traits for types you didn't write, such asi32orString. - There is no inheritance of implementation between types. Traits can have default methods, and that is the main way code is shared.
- Traits power both static dispatch (generics, resolved at compile time, zero cost) and dynamic dispatch (
dyn Trait, resolved at runtime through virtual calls). This lesson and the next ones cover both.
| Concept | Rust |
|---|---|
| Declaring a trait | trait Shape { fn area(&self) -> f64; } |
| Implementing a trait | impl Shape for Circle { ... } |
| Default methods | Default method bodies in the trait |
| Static methods | Associated functions in the trait |
| Taking any implementor | fn draw(s: &impl Shape) (generic) or fn draw(s: &dyn Shape) (dynamic) |
| Adding a trait to an existing type | You can implement your own trait for String |
1. Defining and implementing a trait
trait Describe {
fn describe(&self) -> String;
}
struct Book {
title: String,
author: String,
}
struct Movie {
title: String,
minutes: u32,
}
impl Describe for Book {
fn describe(&self) -> String {
format!("'{}' by {}", self.title, self.author)
}
}
impl Describe for Movie {
fn describe(&self) -> String {
format!("'{}' ({} min)", self.title, self.minutes)
}
}
fn main() {
let b = Book { title: String::from("Dune"), author: String::from("Frank Herbert") };
let m = Movie { title: String::from("Arrival"), minutes: 116 };
println!("{}", b.describe());
println!("{}", m.describe());
}'Dune' by Frank Herbert
'Arrival' (116 min)2. Every method must be implemented
Leaving out a required method is an error.
trait Shape {
fn area(&self) -> f64;
fn perimeter(&self) -> f64;
}
struct Square {
side: f64,
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn main() {}error[E0046]: not all trait items implemented, missing: `perimeter`
--> src/main.rs:10:1
|
3 | fn perimeter(&self) -> f64;
| --------------------------- `perimeter` from trait
...
10 | impl Shape for Square {
| ^^^^^^^^^^^^^^^^^^^^^ missing `perimeter` in implementation3. Default methods
A trait can provide a method body. Implementors get it for free and may override it.
trait Greeter {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Hello, {}!", self.name())
}
}
struct English;
struct Pirate;
impl Greeter for English {
fn name(&self) -> String {
String::from("friend")
}
}
impl Greeter for Pirate {
fn name(&self) -> String {
String::from("matey")
}
fn greet(&self) -> String {
format!("Ahoy, {}!", self.name())
}
}
fn main() {
println!("{}", English.greet());
println!("{}", Pirate.greet());
}Hello, friend!
Ahoy, matey!English and Pirate are unit structs, so English is both the type and its only value.
4. Default methods built on required methods
The most powerful use of defaults: implement one or two small methods, and get a whole family of behavior. The standard library's Iterator trait works exactly like this: implement next, and you get over 70 methods.
trait Scored {
fn score(&self) -> u32;
fn grade(&self) -> char {
match self.score() {
90.. => 'A',
80..=89 => 'B',
70..=79 => 'C',
_ => 'F',
}
}
fn passed(&self) -> bool {
self.grade() != 'F'
}
fn report(&self) -> String {
format!("score {} grade {} {}", self.score(), self.grade(), if self.passed() { "PASS" } else { "FAIL" })
}
}
struct Exam {
points: u32,
}
impl Scored for Exam {
fn score(&self) -> u32 {
self.points
}
}
fn main() {
for p in [95, 82, 40] {
println!("{}", Exam { points: p }.report());
}
}score 95 grade A PASS
score 82 grade B PASS
score 40 grade F FAIL5. Taking any type that implements a trait: impl Trait
item: &impl Describe means "a reference to any type that implements Describe".
trait Describe {
fn describe(&self) -> String;
}
struct Book {
title: String,
}
struct Podcast {
episode: u32,
}
impl Describe for Book {
fn describe(&self) -> String {
format!("book '{}'", self.title)
}
}
impl Describe for Podcast {
fn describe(&self) -> String {
format!("podcast episode {}", self.episode)
}
}
fn announce(item: &impl Describe) {
println!("New: {}", item.describe());
}
fn main() {
announce(&Book { title: String::from("Rust in Action") });
announce(&Podcast { episode: 42 });
}New: book 'Rust in Action'
New: podcast episode 42Behind the scenes, the compiler generates a separate copy of announce for each type it's called with, so each call is a direct, inlinable function call. That is static dispatch, and it is explained in Generics.
6. Passing a type that doesn't implement the trait
trait Describe {
fn describe(&self) -> String;
}
struct Book;
fn announce(item: &impl Describe) {
println!("{}", item.describe());
}
fn main() {
announce(&Book);
}error[E0277]: the trait bound `Book: Describe` is not satisfied
--> src/main.rs:12:14
|
12 | announce(&Book);
| -------- ^^^^^ unsatisfied trait bound
| |
| required by a bound introduced by this call
|
help: the trait `Describe` is not implemented for `Book`
--> src/main.rs:5:1
|
5 | struct Book;
| ^^^^^^^^^^^
help: this trait has no implementations, consider adding one
--> src/main.rs:1:1
|
1 | trait Describe {
| ^^^^^^^^^^^^^^
note: required by a bound in `announce`
--> src/main.rs:7:25
|
7 | fn announce(item: &impl Describe) {
| ^^^^^^^^ required by this bound in `announce`7. Traits with associated functions
A trait can require functions without self, such as constructors. They are called on the type: T::new() or Type::name().
trait Named {
fn type_name() -> &'static str;
fn create_default() -> Self;
}
#[derive(Debug)]
struct Config {
retries: u32,
}
impl Named for Config {
fn type_name() -> &'static str {
"Config"
}
fn create_default() -> Self {
Config { retries: 3 }
}
}
fn main() {
println!("{}: {:?}", Config::type_name(), Config::create_default());
}Config: Config { retries: 3 }Self in a trait means "the type implementing this trait".
8. Traits with associated constants
trait Currency {
const CODE: &'static str;
const DECIMALS: u32;
fn format(amount_minor: i64) -> String {
let divisor = 10_i64.pow(Self::DECIMALS);
if Self::DECIMALS == 0 {
format!("{} {}", amount_minor, Self::CODE)
} else {
format!("{}.{:0width$} {}", amount_minor / divisor, amount_minor % divisor, Self::CODE, width = Self::DECIMALS as usize)
}
}
}
struct Euro;
struct Yen;
impl Currency for Euro {
const CODE: &'static str = "EUR";
const DECIMALS: u32 = 2;
}
impl Currency for Yen {
const CODE: &'static str = "JPY";
const DECIMALS: u32 = 0;
}
fn main() {
println!("{}", Euro::format(123_456));
println!("{}", Yen::format(123_456));
}1234.56 EUR
123456 JPY9. Implementing a trait for a type you didn't write
You can implement your own traits for standard types such as i32, str, or Vec<T>. This is called an extension trait, and it lets you add methods to existing types.
trait Percent {
fn percent_of(self, total: Self) -> f64;
}
impl Percent for u32 {
fn percent_of(self, total: u32) -> f64 {
if total == 0 { 0.0 } else { self as f64 * 100.0 / total as f64 }
}
}
trait Shout {
fn shout(&self) -> String;
}
impl Shout for str {
fn shout(&self) -> String {
format!("{}!", self.to_uppercase())
}
}
fn main() {
println!("{:.1}%", 37_u32.percent_of(50));
println!("{}", "deploy finished".shout());
let owned = String::from("works on String too");
println!("{}", owned.shout()); // String derefs to str
}74.0%
DEPLOY FINISHED!
WORKS ON STRING TOO!10. The trait must be in scope to call its methods
Trait methods are only callable where the trait is visible. In one file that is automatic; across modules you bring a trait into scope with use, even if you never mention its name directly.
mod text_utils {
pub trait WordCount {
fn word_count(&self) -> usize;
}
impl WordCount for str {
fn word_count(&self) -> usize {
self.split_whitespace().count()
}
}
}
use text_utils::WordCount; // without this line, `.word_count()` is not found
fn main() {
println!("{}", "traits must be in scope".word_count());
}5This is why you will often see lines like use std::io::Write; or use std::fmt::Write; at the top of Rust files: they enable methods.
11. The orphan rule
You can implement a trait for a type only if the trait or the type is defined in your crate. You cannot implement a standard trait for a standard type, because two libraries could both do it and conflict.
use std::fmt;
impl fmt::Display for Vec<i32> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} numbers", self.len())
}
}
fn main() {}error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
--> src/main.rs:3:1
|
3 | impl fmt::Display for Vec<i32> {
| ^^^^^^^^^^^^^^^^^^^^^^--------
| |
| `Vec` is not defined in the current crate
|
= note: impl doesn't have any local type before any uncovered type parameters
= note: for more information see https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
= note: define and implement a trait or new type insteadThe workaround is the newtype pattern: wrap the foreign type in your own struct and implement the trait for that. Advanced Traits shows it.
12. Traits can call each other through Self
trait Area {
fn area(&self) -> f64;
}
trait Cost {
fn cost_per_unit_area(&self) -> f64;
}
struct Floor {
width_m: f64,
length_m: f64,
tile_price_per_m2: f64,
}
impl Area for Floor {
fn area(&self) -> f64 {
self.width_m * self.length_m
}
}
impl Cost for Floor {
fn cost_per_unit_area(&self) -> f64 {
self.tile_price_per_m2
}
}
impl Floor {
fn total_cost(&self) -> f64 {
self.area() * self.cost_per_unit_area()
}
}
fn main() {
let kitchen = Floor { width_m: 3.5, length_m: 4.0, tile_price_per_m2: 42.0 };
println!("{} m2, {:.2} total", kitchen.area(), kitchen.total_cost());
}14 m2, 588.00 totalA type can implement any number of traits.
13. Implementing a standard trait: Display
Display controls how {} prints a value. The derive shortcut does not exist for Display, because there is no single right way to show a type to users.
use std::fmt;
struct Money {
cents: i64,
currency: &'static str,
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let sign = if self.cents < 0 { "-" } else { "" };
let abs = self.cents.abs();
write!(f, "{sign}{}.{:02} {}", abs / 100, abs % 100, self.currency)
}
}
fn main() {
let price = Money { cents: 129_99, currency: "USD" };
let refund = Money { cents: -5_00, currency: "USD" };
println!("{price}");
println!("refund of {refund}");
let text = price.to_string(); // every Display type gets to_string()
println!("{} characters", text.len());
}129.99 USD
refund of -5.00 USD
10 characterswrite! works like format!, but writes into the formatter. fmt::Result reports whether writing succeeded. The next lesson covers Display and the other standard traits in depth.
14. impl Trait in return position
A function can promise "I return some type implementing this trait" without naming the type.
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String;
}
struct Circle {
r: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.r * self.r
}
fn name(&self) -> String {
format!("circle r={}", self.r)
}
}
fn unit_circle() -> impl Shape {
Circle { r: 1.0 }
}
fn main() {
let s = unit_circle();
println!("{}: {:.4}", s.name(), s.area());
}circle r=1: 3.1416The function must still return one concrete type. Returning a Circle on one path and a Square on another needs a trait object, covered in Trait Objects.
15. Several implementations, one function
trait Notifier {
fn send(&self, to: &str, message: &str) -> String;
}
struct Email {
from: String,
}
struct Sms;
struct Slack {
channel: String,
}
impl Notifier for Email {
fn send(&self, to: &str, message: &str) -> String {
format!("email {} -> {to}: {message}", self.from)
}
}
impl Notifier for Sms {
fn send(&self, to: &str, message: &str) -> String {
let short: String = message.chars().take(20).collect();
format!("sms -> {to}: {short}")
}
}
impl Notifier for Slack {
fn send(&self, to: &str, message: &str) -> String {
format!("slack #{} @{to}: {message}", self.channel)
}
}
fn alert(n: &impl Notifier, on_call: &str) {
println!("{}", n.send(on_call, "Disk usage above 90% on db-1"));
}
fn main() {
alert(&Email { from: String::from("alerts@corp.com") }, "ops@corp.com");
alert(&Sms, "+15550100");
alert(&Slack { channel: String::from("incidents") }, "oncall");
}email alerts@corp.com -> ops@corp.com: Disk usage above 90% on db-1
sms -> +15550100: Disk usage above 90%
slack #incidents @oncall: Disk usage above 90% on db-116. A trait for mutable behavior
Trait methods can take &mut self too.
trait Counter {
fn increment(&mut self);
fn value(&self) -> u64;
fn increment_by(&mut self, n: u64) {
for _ in 0..n {
self.increment();
}
}
}
struct Simple {
n: u64,
}
struct Saturating {
n: u64,
max: u64,
}
impl Counter for Simple {
fn increment(&mut self) {
self.n += 1;
}
fn value(&self) -> u64 {
self.n
}
}
impl Counter for Saturating {
fn increment(&mut self) {
if self.n < self.max {
self.n += 1;
}
}
fn value(&self) -> u64 {
self.n
}
}
fn run(c: &mut impl Counter) -> u64 {
c.increment_by(7);
c.value()
}
fn main() {
println!("{}", run(&mut Simple { n: 0 }));
println!("{}", run(&mut Saturating { n: 0, max: 5 }));
}7
517. A trait that consumes self
trait IntoReport {
fn into_report(self) -> String;
}
struct TestRun {
passed: u32,
failed: Vec<String>,
}
impl IntoReport for TestRun {
fn into_report(self) -> String {
let mut out = format!("{} passed, {} failed", self.passed, self.failed.len());
for name in self.failed {
out.push_str(&format!("\n FAILED {name}"));
}
out
}
}
fn main() {
let run = TestRun { passed: 41, failed: vec![String::from("login_rejects_bad_password")] };
println!("{}", run.into_report());
}41 passed, 1 failed
FAILED login_rejects_bad_passwordBy convention, methods named into_* consume self, as_* borrow cheaply, and to_* create a new owned value.
18. Default methods can't access fields
A trait knows nothing about the fields of the types implementing it. Default methods can only use other trait methods.
trait Named {
fn greet(&self) -> String {
format!("hi {}", self.name)
}
}
fn main() {}error[E0609]: no field `name` on type `&Self`
--> src/main.rs:3:31
|
1 | trait Named {
| ----------- type parameter 'Self' declared here
2 | fn greet(&self) -> String {
3 | format!("hi {}", self.name)
| ^^^^ unknown fieldRequire an accessor method instead: fn name(&self) -> &str; and call self.name() in the default method.
19. Traits on enums
Enums implement traits exactly like structs, usually with a match inside each method.
trait Priced {
fn price_cents(&self) -> u64;
}
enum Subscription {
Free,
Monthly { seats: u32 },
Yearly { seats: u32 },
}
impl Priced for Subscription {
fn price_cents(&self) -> u64 {
match self {
Subscription::Free => 0,
Subscription::Monthly { seats } => 1_200 * *seats as u64,
Subscription::Yearly { seats } => 12_000 * *seats as u64 / 12,
}
}
}
fn main() {
let plans = [Subscription::Free, Subscription::Monthly { seats: 5 }, Subscription::Yearly { seats: 5 }];
for p in &plans {
println!("{} cents per month", p.price_cents());
}
}0 cents per month
6000 cents per month
5000 cents per month20. Shared data and hooks with composition
For "shared fields + shared code + abstract hooks", Rust splits the design into a struct for the shared data and a trait for the hooks, and uses composition.
trait Step {
fn run_step(&self) -> String;
}
struct Job<S: Step> {
name: String,
step: S,
}
impl<S: Step> Job<S> {
fn run(&self) {
println!("start {}", self.name);
println!(" {}", self.step.run_step());
println!("end {}", self.name);
}
}
struct Backup;
impl Step for Backup {
fn run_step(&self) -> String {
String::from("copying files")
}
}
fn main() {
let job = Job { name: String::from("nightly-backup"), step: Backup };
job.run();
}start nightly-backup
copying files
end nightly-backupJob<S: Step> is a generic struct: it works for any step type S that implements Step. The next lessons explain generics fully.
21. Realistic example: pluggable pricing strategies
The strategy pattern is a natural fit for traits.
trait PricingStrategy {
fn name(&self) -> String;
fn price(&self, base_cents: u64, quantity: u32) -> u64;
}
struct Regular;
struct Bulk {
min_quantity: u32,
percent_off: u64,
}
struct Clearance {
fixed_cents: u64,
}
impl PricingStrategy for Regular {
fn name(&self) -> String {
String::from("regular")
}
fn price(&self, base: u64, qty: u32) -> u64 {
base * qty as u64
}
}
impl PricingStrategy for Bulk {
fn name(&self) -> String {
format!("bulk ({}% off from {})", self.percent_off, self.min_quantity)
}
fn price(&self, base: u64, qty: u32) -> u64 {
let full = base * qty as u64;
if qty >= self.min_quantity { full - full * self.percent_off / 100 } else { full }
}
}
impl PricingStrategy for Clearance {
fn name(&self) -> String {
String::from("clearance")
}
fn price(&self, _base: u64, qty: u32) -> u64 {
self.fixed_cents * qty as u64
}
}
fn quote(strategy: &impl PricingStrategy, base: u64, qty: u32) {
println!("{:<24} {:>3} x {:>5} = {:>7}", strategy.name(), qty, base, strategy.price(base, qty));
}
fn main() {
quote(&Regular, 1_999, 12);
quote(&Bulk { min_quantity: 10, percent_off: 15 }, 1_999, 12);
quote(&Bulk { min_quantity: 10, percent_off: 15 }, 1_999, 4);
quote(&Clearance { fixed_cents: 500 }, 1_999, 12);
}regular 12 x 1999 = 23988
bulk (15% off from 10) 12 x 1999 = 20390
bulk (15% off from 10) 4 x 1999 = 7996
clearance 12 x 1999 = 600022. Realistic example: a storage abstraction
Code that depends on a trait instead of a concrete store can be tested with an in-memory fake.
trait KeyValueStore {
fn get(&self, key: &str) -> Option<String>;
fn set(&mut self, key: &str, value: &str);
fn get_or(&self, key: &str, default: &str) -> String {
self.get(key).unwrap_or_else(|| default.to_string())
}
}
struct MemoryStore {
entries: Vec<(String, String)>,
}
impl KeyValueStore for MemoryStore {
fn get(&self, key: &str) -> Option<String> {
self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
}
fn set(&mut self, key: &str, value: &str) {
match self.entries.iter_mut().find(|(k, _)| k == key) {
Some(entry) => entry.1 = value.to_string(),
None => self.entries.push((key.to_string(), value.to_string())),
}
}
}
fn record_login(store: &mut impl KeyValueStore, user: &str) -> u32 {
let key = format!("logins:{user}");
let count: u32 = store.get_or(&key, "0").parse().unwrap_or(0) + 1;
store.set(&key, &count.to_string());
count
}
fn main() {
let mut store = MemoryStore { entries: Vec::new() };
record_login(&mut store, "ana");
record_login(&mut store, "ben");
let n = record_login(&mut store, "ana");
println!("ana has logged in {n} times");
println!("{:?}", store.entries);
}ana has logged in 2 times
[("logins:ana", "2"), ("logins:ben", "1")]23. Realistic example: validation rules as traits
Each rule is a small type implementing one trait. New rules can be added without touching the others.
trait Rule {
fn check(&self, password: &str) -> Result<(), String>;
}
struct MinLength(usize);
struct RequiresDigit;
struct NotContaining(&'static str);
impl Rule for MinLength {
fn check(&self, p: &str) -> Result<(), String> {
if p.chars().count() >= self.0 { Ok(()) } else { Err(format!("must be at least {} characters", self.0)) }
}
}
impl Rule for RequiresDigit {
fn check(&self, p: &str) -> Result<(), String> {
if p.chars().any(|c| c.is_ascii_digit()) { Ok(()) } else { Err(String::from("must contain a digit")) }
}
}
impl Rule for NotContaining {
fn check(&self, p: &str) -> Result<(), String> {
if p.to_lowercase().contains(self.0) { Err(format!("must not contain '{}'", self.0)) } else { Ok(()) }
}
}
fn validate(p: &str, r1: &impl Rule, r2: &impl Rule, r3: &impl Rule) -> Vec<String> {
[r1.check(p), r2.check(p), r3.check(p)].into_iter().filter_map(|r| r.err()).collect()
}
fn main() {
for pw in ["password", "Tr0ub4dor&3", "hunter2"] {
let errors = validate(pw, &MinLength(8), &RequiresDigit, &NotContaining("password"));
println!("{pw:>12}: {:?}", errors);
}
} password: ["must contain a digit", "must not contain 'password'"]
Tr0ub4dor&3: []
hunter2: ["must be at least 8 characters"]Passing each rule as a separate parameter is clumsy. A list of rules of different types needs Vec<Box<dyn Rule>>, which you'll build in Trait Objects.
24. Realistic example: exporting data in several formats
struct Employee {
name: String,
role: String,
salary: u32,
}
trait Exporter {
fn header(&self) -> String;
fn row(&self, e: &Employee) -> String;
fn footer(&self) -> String {
String::new()
}
fn export(&self, staff: &[Employee]) -> String {
let mut out = self.header();
for e in staff {
out.push_str(&self.row(e));
}
out.push_str(&self.footer());
out
}
}
struct Csv;
struct Markdown;
impl Exporter for Csv {
fn header(&self) -> String {
String::from("name,role,salary\n")
}
fn row(&self, e: &Employee) -> String {
format!("{},{},{}\n", e.name, e.role, e.salary)
}
}
impl Exporter for Markdown {
fn header(&self) -> String {
String::from("| Name | Role | Salary |\n|---|---|---|\n")
}
fn row(&self, e: &Employee) -> String {
format!("| {} | {} | {} |\n", e.name, e.role, e.salary)
}
fn footer(&self) -> String {
String::from("\n_generated report_\n")
}
}
fn main() {
let staff = vec![
Employee { name: String::from("Ana"), role: String::from("Engineer"), salary: 9_000 },
Employee { name: String::from("Ben"), role: String::from("Designer"), salary: 7_500 },
];
print!("{}", Csv.export(&staff));
println!();
print!("{}", Markdown.export(&staff));
}name,role,salary
Ana,Engineer,9000
Ben,Designer,7500
| Name | Role | Salary |
|---|---|---|
| Ana | Engineer | 9000 |
| Ben | Designer | 7500 |
_generated report_export is a template method: the trait defines the algorithm, and each implementation fills in the steps.
25. Realistic example: an event handler trait
#[derive(Debug)]
enum Event {
UserSignedUp { email: String },
OrderPlaced { id: u32, total_cents: u64 },
PaymentFailed { order_id: u32 },
}
trait Handler {
fn name(&self) -> &'static str;
fn handle(&mut self, event: &Event) -> Option<String>;
}
struct WelcomeMailer {
sent: u32,
}
struct FraudCheck {
threshold_cents: u64,
}
impl Handler for WelcomeMailer {
fn name(&self) -> &'static str {
"welcome-mailer"
}
fn handle(&mut self, event: &Event) -> Option<String> {
if let Event::UserSignedUp { email } = event {
self.sent += 1;
return Some(format!("welcome email #{} to {email}", self.sent));
}
None
}
}
impl Handler for FraudCheck {
fn name(&self) -> &'static str {
"fraud-check"
}
fn handle(&mut self, event: &Event) -> Option<String> {
match event {
Event::OrderPlaced { id, total_cents } if *total_cents > self.threshold_cents => {
Some(format!("order {id} flagged for review"))
}
Event::PaymentFailed { order_id } => Some(format!("order {order_id}: payment failed, watch account")),
_ => None,
}
}
}
fn dispatch(events: &[Event], handler: &mut impl Handler) {
for e in events {
if let Some(action) = handler.handle(e) {
println!("[{}] {action}", handler.name());
}
}
}
fn main() {
let events = vec![
Event::UserSignedUp { email: String::from("ana@example.com") },
Event::OrderPlaced { id: 1, total_cents: 4_500 },
Event::OrderPlaced { id: 2, total_cents: 250_000 },
Event::PaymentFailed { order_id: 2 },
Event::UserSignedUp { email: String::from("ben@example.com") },
];
dispatch(&events, &mut WelcomeMailer { sent: 0 });
dispatch(&events, &mut FraudCheck { threshold_cents: 100_000 });
}[welcome-mailer] welcome email #1 to ana@example.com
[welcome-mailer] welcome email #2 to ben@example.com
[fraud-check] order 2 flagged for review
[fraud-check] order 2: payment failed, watch account26. Realistic example: extending Vec with statistics
An extension trait adds domain-specific methods to a standard type, without wrapping it.
trait Stats {
fn mean(&self) -> Option<f64>;
fn percentile(&self, p: f64) -> Option<f64>;
}
impl Stats for [f64] {
fn mean(&self) -> Option<f64> {
if self.is_empty() {
return None;
}
Some(self.iter().sum::<f64>() / self.len() as f64)
}
fn percentile(&self, p: f64) -> Option<f64> {
if self.is_empty() {
return None;
}
let mut sorted = self.to_vec();
sorted.sort_by(|a, b| a.total_cmp(b));
let rank = (p / 100.0 * (sorted.len() - 1) as f64).round() as usize;
Some(sorted[rank])
}
}
fn main() {
let latencies_ms = vec![12.0, 15.0, 11.0, 250.0, 14.0, 13.0, 16.0, 12.0, 18.0, 900.0];
println!("mean: {:?}", latencies_ms.mean());
println!("p50: {:?}", latencies_ms.percentile(50.0));
println!("p90: {:?}", latencies_ms.percentile(90.0));
let empty: Vec<f64> = Vec::new();
println!("empty mean: {:?}", empty.mean());
}mean: Some(126.1)
p50: Some(15.0)
p90: Some(250.0)
empty mean: NoneImplementing the trait for the slice type [f64] makes it work on arrays, vectors, and slices alike, because Vec<f64> automatically derefs to [f64].
Summary
trait Name { fn method(&self) -> T; }declares shared behavior;impl Name for Type { ... }implements it.- Default methods give implementors behavior for free, built on the required methods.
&impl Traitparameters accept any implementing type, with static dispatch.- Traits can have associated functions and constants.
Selfmeans the implementing type. - You can implement your traits for any type (extension traits), and any trait for your types. The orphan rule forbids foreign traits on foreign types.
- Trait methods need the trait in scope (
use). - Traits have no fields. Share data through composition, not inheritance.
Next: Deriving Standard Traits.
