Lesson 33 of 94
Trait Objects & Dynamic Dispatch
Generics give you static dispatch: one specialized copy of the code per type, decided at compile time. That can't handle a Vec holding circles and squares, or a function that returns different types depending on runtime input.
Trait objects give you dynamic dispatch: a value of type dyn Shape is "some type implementing Shape", and each method call looks up the right implementation at runtime.
| Use case | Rust |
|---|---|
| A collection holding circles and squares | Vec<Box<dyn Shape>> |
| A boxed shape value | let s: Box<dyn Shape> = Box::new(Circle(1.0)); |
| A function taking any shape | fn draw(s: &dyn Shape) |
| Which calls are virtual | Only calls through dyn Trait are virtual |
| Recovering the concrete type | Any::downcast_ref (rarely needed) |
1. The problem: a list of different types
A Vec<T> holds values of one type. A circle and a square are different types, even if both implement Shape.
trait Shape {
fn area(&self) -> f64;
}
struct Circle(f64);
struct Square(f64);
impl Shape for Circle {
fn area(&self) -> f64 { 3.14159 * self.0 * self.0 }
}
impl Shape for Square {
fn area(&self) -> f64 { self.0 * self.0 }
}
fn main() {
let shapes = vec![Circle(1.0), Square(2.0)];
}error[E0308]: mismatched types
--> src/main.rs:17:36
|
17 | let shapes = vec![Circle(1.0), Square(2.0)];
| ^^^^^^^^^^^ expected `Circle`, found `Square`2. Box<dyn Shape>: a list of trait objects
Box::new(value) puts the value on the heap (see Box), and Box<dyn Shape> is a pointer to "some Shape". All the boxes have the same size, so they fit in one Vec.
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String;
}
struct Circle(f64);
struct Square(f64);
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.0 * self.0
}
fn name(&self) -> String {
format!("circle r={}", self.0)
}
}
impl Shape for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
fn name(&self) -> String {
format!("square s={}", self.0)
}
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle(1.0)), Box::new(Square(2.0)), Box::new(Circle(0.5))];
let mut total = 0.0;
for s in &shapes {
println!("{:<14} {:.3}", s.name(), s.area());
total += s.area();
}
println!("total {:.3}", total);
}circle r=1 3.142
square s=2 4.000
circle r=0.5 0.785
total 7.9273. &dyn Trait parameters
A function can take a reference to a trait object. Unlike a generic function, only one copy of it is compiled, and it works with any implementing type.
trait Describe {
fn describe(&self) -> String;
}
struct User {
name: String,
}
struct Server {
host: String,
port: u16,
}
impl Describe for User {
fn describe(&self) -> String {
format!("user {}", self.name)
}
}
impl Describe for Server {
fn describe(&self) -> String {
format!("server {}:{}", self.host, self.port)
}
}
fn log(item: &dyn Describe) {
println!("[log] {}", item.describe());
}
fn main() {
let u = User { name: String::from("ada") };
let s = Server { host: String::from("10.0.0.5"), port: 443 };
log(&u);
log(&s);
}[log] user ada
[log] server 10.0.0.5:4434. Returning different types: Box<dyn Trait>
This fixes the impl Trait error from Trait Bounds: the function can return a different concrete type on each path.
trait Shape {
fn area(&self) -> f64;
}
struct Circle(f64);
struct Square(f64);
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.0 * self.0
}
}
impl Shape for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
}
fn make(kind: &str, size: f64) -> Option<Box<dyn Shape>> {
match kind {
"circle" => Some(Box::new(Circle(size))),
"square" => Some(Box::new(Square(size))),
_ => None,
}
}
fn main() {
for (kind, size) in [("circle", 2.0), ("square", 3.0), ("hexagon", 1.0)] {
match make(kind, size) {
Some(shape) => println!("{kind}: {:.2}", shape.area()),
None => println!("{kind}: unknown shape"),
}
}
}circle: 12.57
square: 9.00
hexagon: unknown shape5. What a trait object looks like in memory
A &dyn Shape or Box<dyn Shape> is a fat pointer: one pointer to the data, and one pointer to a vtable, a table of function pointers for that type's implementation of the trait. A method call reads the function's address from the vtable, then calls it.
Box<dyn Shape> heap vtable for Circle as Shape
┌──────────────┐ ┌──────────┐ ┌───────────────────────┐
│ data ptr ──┼────────► │ Circle │ │ drop_in_place::<Circle>│
│ vtable ptr ──┼────┐ │ radius │ │ size, align │
└──────────────┘ │ └──────────┘ │ area -> Circle::area │
└─────────────────────────────►│ name -> Circle::name │
└───────────────────────┘trait Shape {
fn area(&self) -> f64;
}
struct Circle(f64);
impl Shape for Circle {
fn area(&self) -> f64 {
self.0
}
}
fn main() {
println!("&Circle: {} bytes", std::mem::size_of::<&Circle>());
println!("&dyn Shape: {} bytes", std::mem::size_of::<&dyn Shape>());
println!("Box<dyn Shape>: {} bytes", std::mem::size_of::<Box<dyn Shape>>());
let c = Circle(2.0);
let s: &dyn Shape = &c;
println!("area via vtable: {}", s.area());
}&Circle: 8 bytes
&dyn Shape: 16 bytes
Box<dyn Shape>: 16 bytes
area via vtable: 26. Static dispatch vs dynamic dispatch
The same trait can be used both ways. Pick per call site.
trait Tax {
fn apply(&self, cents: u64) -> u64;
}
struct Vat(u64);
struct Exempt;
impl Tax for Vat {
fn apply(&self, cents: u64) -> u64 {
cents + cents * self.0 / 100
}
}
impl Tax for Exempt {
fn apply(&self, cents: u64) -> u64 {
cents
}
}
// Static: compiled once per T, calls are direct and can be inlined.
fn total_static<T: Tax>(tax: &T, items: &[u64]) -> u64 {
items.iter().map(|&i| tax.apply(i)).sum()
}
// Dynamic: compiled once, each call goes through the vtable.
fn total_dynamic(tax: &dyn Tax, items: &[u64]) -> u64 {
items.iter().map(|&i| tax.apply(i)).sum()
}
fn main() {
let items = [1_000, 2_000];
println!("{} {}", total_static(&Vat(20), &items), total_static(&Exempt, &items));
println!("{} {}", total_dynamic(&Vat(20), &items), total_dynamic(&Exempt, &items));
let taxes: Vec<Box<dyn Tax>> = vec![Box::new(Vat(20)), Box::new(Exempt), Box::new(Vat(5))];
for t in &taxes {
print!("{} ", total_dynamic(t.as_ref(), &items));
}
println!();
}3600 3000
3600 3000
3600 3000 3150Generics (T: Trait, impl Trait) |
Trait objects (dyn Trait) |
|
|---|---|---|
| Resolved | At compile time | At runtime, through a vtable |
| Mixed types in one collection | No | Yes |
| Code size | One copy per type | One copy total |
| Call cost | Direct call, inlinable | Indirect call, usually not inlined |
| Can use generic methods of the trait | Yes | No |
t.as_ref() turns a &Box<dyn Tax> into a &dyn Tax.
7. Default methods work through dyn
trait Greeter {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Hello, {}!", self.name())
}
}
struct Formal;
struct Casual;
impl Greeter for Formal {
fn name(&self) -> String {
String::from("Dr. Hopper")
}
fn greet(&self) -> String {
format!("Good evening, {}.", self.name())
}
}
impl Greeter for Casual {
fn name(&self) -> String {
String::from("Grace")
}
}
fn main() {
let greeters: [&dyn Greeter; 2] = [&Formal, &Casual];
for g in greeters {
println!("{}", g.greet());
}
}Good evening, Dr. Hopper.
Hello, Grace!8. &mut dyn Trait for mutable behavior
trait Counter {
fn record(&mut self, value: u64);
fn summary(&self) -> String;
}
struct Sum(u64);
struct Max(u64);
impl Counter for Sum {
fn record(&mut self, value: u64) {
self.0 += value;
}
fn summary(&self) -> String {
format!("sum={}", self.0)
}
}
impl Counter for Max {
fn record(&mut self, value: u64) {
self.0 = self.0.max(value);
}
fn summary(&self) -> String {
format!("max={}", self.0)
}
}
fn feed(counter: &mut dyn Counter, values: &[u64]) {
for &v in values {
counter.record(v);
}
}
fn main() {
let mut counters: Vec<Box<dyn Counter>> = vec![Box::new(Sum(0)), Box::new(Max(0))];
for c in counters.iter_mut() {
feed(c.as_mut(), &[12, 7, 30, 1]);
}
for c in &counters {
println!("{}", c.summary());
}
}sum=50
max=309. Dyn compatibility: generic methods aren't allowed
A vtable holds one function pointer per method. A generic method would need one entry for every possible type argument, which is impossible. So a trait with a generic method can't be used as dyn Trait. (This used to be called object safety.)
trait Logger {
fn log<T: std::fmt::Debug>(&self, value: T);
}
struct Console;
impl Logger for Console {
fn log<T: std::fmt::Debug>(&self, value: T) {
println!("{:?}", value);
}
}
fn main() {
let l: Box<dyn Logger> = Box::new(Console);
}error[E0038]: the trait `Logger` is not dyn compatible
--> src/main.rs:14:20
|
14 | let l: Box<dyn Logger> = Box::new(Console);
| ^^^^^^ `Logger` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
--> src/main.rs:2:8
|
1 | trait Logger {
| ------ this trait is not dyn compatible...
2 | fn log<T: std::fmt::Debug>(&self, value: T);
| ^^^ ...because method `log` has generic type parameters
= help: consider moving `log` to another trait
= help: only type `Console` implements `Logger`; consider using it directly instead.10. Dyn compatibility: methods that return Self
A method returning Self by value can't be called through dyn Trait, because the caller doesn't know the concrete type or its size. The trait Clone is the famous example: Box<dyn Clone> is not allowed.
trait Shape {
fn area(&self) -> f64;
fn duplicate(&self) -> Self;
}
struct Square(f64);
impl Shape for Square {
fn area(&self) -> f64 { self.0 * self.0 }
fn duplicate(&self) -> Self { Square(self.0) }
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Square(1.0))];
}error[E0038]: the trait `Shape` is not dyn compatible
--> src/main.rs:14:29
|
14 | let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Square(1.0))];
| ^^^^^ `Shape` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
--> src/main.rs:3:28
|
1 | trait Shape {
| ----- this trait is not dyn compatible...
2 | fn area(&self) -> f64;
3 | fn duplicate(&self) -> Self;
| ^^^^ ...because method `duplicate` references the `Self` type in its return type
= help: consider moving `duplicate` to another trait
= help: only type `Square` implements `Shape`; consider using it directly instead.11. The escape hatch: where Self: Sized
Marking a method where Self: Sized excludes it from the vtable. The trait becomes dyn-compatible, and the method is still available on concrete types.
trait Shape {
fn area(&self) -> f64;
fn duplicate(&self) -> Self
where
Self: Sized;
}
#[derive(Debug)]
struct Square(f64);
impl Shape for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
fn duplicate(&self) -> Self {
Square(self.0)
}
}
fn main() {
let sq = Square(3.0);
println!("{:?}", sq.duplicate()); // fine on the concrete type
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(sq)];
println!("{}", shapes[0].area()); // fine through dyn
}Square(3.0)
912. Cloning a Box<dyn Trait>
Since Clone isn't dyn-compatible, the standard trick is a clone_box method that returns Box<dyn Trait>, plus an impl Clone for Box<dyn Trait> that calls it.
trait Shape {
fn area(&self) -> f64;
fn clone_box(&self) -> Box<dyn Shape>;
}
#[derive(Clone)]
struct Circle(f64);
#[derive(Clone)]
struct Square(f64);
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.0 * self.0
}
fn clone_box(&self) -> Box<dyn Shape> {
Box::new(self.clone())
}
}
impl Shape for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
fn clone_box(&self) -> Box<dyn Shape> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn Shape> {
fn clone(&self) -> Self {
self.clone_box()
}
}
fn main() {
let original: Vec<Box<dyn Shape>> = vec![Box::new(Circle(1.0)), Box::new(Square(2.0))];
let copy = original.clone(); // Vec::clone works because Box<dyn Shape>: Clone
let areas: Vec<String> = copy.iter().map(|s| format!("{:.2}", s.area())).collect();
println!("{:?} (from {} originals)", areas, original.len());
}["3.14", "4.00"] (from 2 originals)13. Supertraits make trait objects printable
A trait object only has the methods of its trait. To print one with {:?}, make Debug a supertrait: every Shape must also be Debug, so dyn Shape can be printed.
use std::fmt::Debug;
trait Shape: Debug {
fn area(&self) -> f64;
}
#[derive(Debug)]
struct Rect {
w: f64,
h: f64,
}
#[derive(Debug)]
struct Dot;
impl Shape for Rect {
fn area(&self) -> f64 {
self.w * self.h
}
}
impl Shape for Dot {
fn area(&self) -> f64 {
0.0
}
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Rect { w: 2.0, h: 3.0 }), Box::new(Dot)];
println!("{:?}", shapes);
}[Rect { w: 2.0, h: 3.0 }, Dot]14. Upcasting: from dyn Sub to dyn Super
Since Rust 1.86, a trait object converts to a trait object of any of its supertraits: &dyn Shape becomes &dyn Debug when Shape: Debug. The most useful case is Any. A component system declares trait Component: Any, stores Box<dyn Component>, and upcasts to &dyn Any to downcast back to the concrete type:
use std::any::Any;
use std::fmt::Debug;
trait Component: Any + Debug {
fn name(&self) -> &'static str;
}
#[derive(Debug)]
struct Health(u32);
#[derive(Debug)]
struct Position {
x: i32,
y: i32,
}
impl Component for Health {
fn name(&self) -> &'static str {
"health"
}
}
impl Component for Position {
fn name(&self) -> &'static str {
"position"
}
}
fn log(value: &dyn Debug) {
println!("log: {value:?}");
}
fn main() {
let components: Vec<Box<dyn Component>> = vec![Box::new(Health(80)), Box::new(Position { x: 3, y: -1 })];
for component in &components {
log(component.as_ref()); // &dyn Component -> &dyn Debug
let any: &dyn Any = component.as_ref(); // &dyn Component -> &dyn Any
if let Some(Health(hp)) = any.downcast_ref::<Health>() {
println!("{} is {hp}", component.name());
} else if let Some(pos) = any.downcast_ref::<Position>() {
println!("{} is ({}, {})", component.name(), pos.x, pos.y);
}
}
}log: Health(80)
health is 80
log: Position { x: 3, y: -1 }
position is (3, -1)Code written before Rust 1.86 can't upcast, so you'll often see a helper method instead: fn as_any(&self) -> &dyn Any { self } in every implementation.
15. Downcasting with Any
Occasionally you need to get the concrete type back. std::any::Any supports it. The trait exposes an as_any method that returns &dyn Any, and downcast_ref::<T>() returns Some only if the type matches.
use std::any::Any;
trait Component {
fn name(&self) -> &str;
fn as_any(&self) -> &dyn Any;
}
struct Button {
label: String,
}
struct Slider {
value: u8,
}
impl Component for Button {
fn name(&self) -> &str {
"button"
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl Component for Slider {
fn name(&self) -> &str {
"slider"
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn main() {
let ui: Vec<Box<dyn Component>> = vec![
Box::new(Button { label: String::from("Save") }),
Box::new(Slider { value: 70 }),
];
for c in &ui {
if let Some(b) = c.as_any().downcast_ref::<Button>() {
println!("{} with label '{}'", c.name(), b.label);
} else if let Some(s) = c.as_any().downcast_ref::<Slider>() {
println!("{} at {}%", c.name(), s.value);
}
}
}button with label 'Save'
slider at 70%Needing to downcast is usually a sign the trait is missing a method, or that an enum would fit better (example 21).
16. A trait object as a struct field
Holding a Box<dyn Trait> in a struct lets the behavior be chosen at runtime, which is dependency injection without a framework.
trait Clock {
fn now_ms(&self) -> u64;
}
struct FixedClock(u64);
impl Clock for FixedClock {
fn now_ms(&self) -> u64 {
self.0
}
}
struct SessionService {
clock: Box<dyn Clock>,
ttl_ms: u64,
}
impl SessionService {
fn is_expired(&self, created_at_ms: u64) -> bool {
self.clock.now_ms() - created_at_ms > self.ttl_ms
}
}
fn main() {
let service = SessionService { clock: Box::new(FixedClock(10_000)), ttl_ms: 3_000 };
println!("{}", service.is_expired(8_000));
println!("{}", service.is_expired(5_000));
}false
trueIn tests you inject a fixed clock, and in production a real one.
17. Trait objects that borrow
A trait object can hold references. By default, Box<dyn Trait> means Box<dyn Trait + 'static> (it may not borrow anything short-lived). Add a lifetime to allow borrowing: Box<dyn Trait + 'a>.
trait Filter {
fn keep(&self, line: &str) -> bool;
}
struct Contains<'a> {
needle: &'a str,
}
impl Filter for Contains<'_> {
fn keep(&self, line: &str) -> bool {
line.contains(self.needle)
}
}
fn build_filter<'a>(needle: &'a str) -> Box<dyn Filter + 'a> {
Box::new(Contains { needle })
}
fn main() {
let search_term = String::from("ERROR");
let filter = build_filter(&search_term);
for line in ["INFO ok", "ERROR disk", "WARN slow", "ERROR net"] {
if filter.keep(line) {
println!("{line}");
}
}
}ERROR disk
ERROR net18. Realistic example: validation rules as a list
This finishes the password rules from Traits: rules of different types in one Vec, checked in a loop.
trait Rule {
fn check(&self, password: &str) -> Result<(), String>;
}
struct MinLength(usize);
struct RequiresDigit;
struct RequiresSymbol;
struct NotIn(Vec<&'static str>);
impl Rule for MinLength {
fn check(&self, p: &str) -> Result<(), String> {
if p.chars().count() >= self.0 { Ok(()) } else { Err(format!("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("needs a digit")) }
}
}
impl Rule for RequiresSymbol {
fn check(&self, p: &str) -> Result<(), String> {
if p.chars().any(|c| !c.is_alphanumeric()) { Ok(()) } else { Err(String::from("needs a symbol")) }
}
}
impl Rule for NotIn {
fn check(&self, p: &str) -> Result<(), String> {
if self.0.contains(&p.to_lowercase().as_str()) { Err(String::from("too common")) } else { Ok(()) }
}
}
fn main() {
let rules: Vec<Box<dyn Rule>> = vec![
Box::new(MinLength(10)),
Box::new(RequiresDigit),
Box::new(RequiresSymbol),
Box::new(NotIn(vec!["password1!", "qwerty123!"])),
];
for pw in ["short", "Password1!", "correct-horse-7"] {
let errors: Vec<String> = rules.iter().filter_map(|r| r.check(pw).err()).collect();
println!("{pw:>16}: {}", if errors.is_empty() { String::from("OK") } else { errors.join(", ") });
}
} short: at least 10 characters, needs a digit, needs a symbol
Password1!: too common
correct-horse-7: OK19. Realistic example: a command registry
A map from command names to handlers is the classic plugin pattern. New commands are added without touching the dispatcher.
use std::collections::HashMap;
trait Command {
fn run(&self, args: &[&str]) -> String;
fn help(&self) -> &'static str;
}
struct Echo;
struct Add;
struct Upper;
impl Command for Echo {
fn run(&self, args: &[&str]) -> String {
args.join(" ")
}
fn help(&self) -> &'static str {
"print the arguments"
}
}
impl Command for Add {
fn run(&self, args: &[&str]) -> String {
let total: i64 = args.iter().filter_map(|a| a.parse::<i64>().ok()).sum();
total.to_string()
}
fn help(&self) -> &'static str {
"add numbers"
}
}
impl Command for Upper {
fn run(&self, args: &[&str]) -> String {
args.join(" ").to_uppercase()
}
fn help(&self) -> &'static str {
"uppercase the arguments"
}
}
fn main() {
let mut commands: HashMap<&str, Box<dyn Command>> = HashMap::new();
commands.insert("echo", Box::new(Echo));
commands.insert("add", Box::new(Add));
commands.insert("upper", Box::new(Upper));
for line in ["echo hello world", "add 2 40 x 3", "upper rust traits", "rm -rf /"] {
let parts: Vec<&str> = line.split_whitespace().collect();
let (name, args) = parts.split_first().unwrap();
match commands.get(name) {
Some(cmd) => println!("{line:<20} => {}", cmd.run(args)),
None => println!("{line:<20} => unknown command"),
}
}
let mut names: Vec<&&str> = commands.keys().collect();
names.sort();
for n in names {
println!("{n:>6}: {}", commands[n].help());
}
}echo hello world => hello world
add 2 40 x 3 => 45
upper rust traits => RUST TRAITS
rm -rf / => unknown command
add: add numbers
echo: print the arguments
upper: uppercase the arguments20. Realistic example: the observer pattern
trait Observer {
fn notify(&mut self, event: &str);
fn report(&self) -> String;
}
struct AuditLog {
entries: Vec<String>,
}
struct AlertCounter {
alerts: u32,
}
impl Observer for AuditLog {
fn notify(&mut self, event: &str) {
self.entries.push(event.to_string());
}
fn report(&self) -> String {
format!("audit log: {} entries", self.entries.len())
}
}
impl Observer for AlertCounter {
fn notify(&mut self, event: &str) {
if event.starts_with("ALERT") {
self.alerts += 1;
}
}
fn report(&self) -> String {
format!("alerts: {}", self.alerts)
}
}
struct Subject {
observers: Vec<Box<dyn Observer>>,
}
impl Subject {
fn emit(&mut self, event: &str) {
for o in &mut self.observers {
o.notify(event);
}
}
}
fn main() {
let mut subject = Subject {
observers: vec![Box::new(AuditLog { entries: Vec::new() }), Box::new(AlertCounter { alerts: 0 })],
};
for e in ["user login", "ALERT cpu 99%", "config changed", "ALERT disk full"] {
subject.emit(e);
}
for o in &subject.observers {
println!("{}", o.report());
}
}audit log: 4 entries
alerts: 221. Enums vs trait objects
Both give you "one of several types". Choose based on whether the set of types is closed (known now) or open (others will add more):
- Enum: closed set. You can add operations easily (just write a new
match), andmatchis exhaustive. No heap allocation, no vtable. - Trait object: open set. Other code can add new types without changing yours, but adding a new operation means changing the trait and every implementation.
// Closed set: every payment method is known and handled in one place.
enum Payment {
Card { last4: &'static str },
Paypal { email: &'static str },
Invoice { days: u32 },
}
impl Payment {
fn describe(&self) -> String {
match self {
Payment::Card { last4 } => format!("card ending {last4}"),
Payment::Paypal { email } => format!("PayPal {email}"),
Payment::Invoice { days } => format!("invoice, net {days}"),
}
}
}
// Open set: plugins can add new exporters without changing this code.
trait Exporter {
fn export(&self, rows: &[&str]) -> String;
}
struct Csv;
struct Json;
impl Exporter for Csv {
fn export(&self, rows: &[&str]) -> String {
rows.join(",")
}
}
impl Exporter for Json {
fn export(&self, rows: &[&str]) -> String {
let quoted: Vec<String> = rows.iter().map(|r| format!("\"{r}\"")).collect();
format!("[{}]", quoted.join(","))
}
}
fn main() {
let payments = [Payment::Card { last4: "4242" }, Payment::Paypal { email: "a@b.c" }, Payment::Invoice { days: 30 }];
for p in &payments {
println!("{}", p.describe());
}
let exporters: Vec<Box<dyn Exporter>> = vec![Box::new(Csv), Box::new(Json)];
for e in &exporters {
println!("{}", e.export(&["a", "b"]));
}
}card ending 4242
PayPal a@b.c
invoice, net 30
a,b
["a","b"]In Rust, reach for an enum first when the set of cases is yours to define.
22. Realistic example: HTTP middleware chain
Each middleware can inspect or modify the request, or stop it. The chain is a Vec<Box<dyn Middleware>> built at startup from configuration.
#[derive(Debug)]
struct Request {
path: String,
headers: Vec<(String, String)>,
}
trait Middleware {
fn handle(&self, req: &mut Request) -> Result<(), String>;
}
struct RequireAuth;
struct AddRequestId(u32);
struct BlockPaths(Vec<&'static str>);
impl Middleware for RequireAuth {
fn handle(&self, req: &mut Request) -> Result<(), String> {
if req.headers.iter().any(|(k, _)| k == "Authorization") { Ok(()) } else { Err(String::from("401 unauthorized")) }
}
}
impl Middleware for AddRequestId {
fn handle(&self, req: &mut Request) -> Result<(), String> {
req.headers.push((String::from("X-Request-Id"), format!("req-{}", self.0)));
Ok(())
}
}
impl Middleware for BlockPaths {
fn handle(&self, req: &mut Request) -> Result<(), String> {
if self.0.iter().any(|p| req.path.starts_with(p)) { Err(format!("403 forbidden: {}", req.path)) } else { Ok(()) }
}
}
fn run(chain: &[Box<dyn Middleware>], mut req: Request) -> Result<Request, String> {
for m in chain {
m.handle(&mut req)?;
}
Ok(req)
}
fn main() {
let chain: Vec<Box<dyn Middleware>> = vec![Box::new(AddRequestId(77)), Box::new(BlockPaths(vec!["/admin"])), Box::new(RequireAuth)];
let ok = Request { path: String::from("/api/orders"), headers: vec![(String::from("Authorization"), String::from("Bearer x"))] };
let no_auth = Request { path: String::from("/api/orders"), headers: vec![] };
let admin = Request { path: String::from("/admin/users"), headers: vec![] };
println!("{:?}", run(&chain, ok).map(|r| r.headers));
println!("{:?}", run(&chain, no_auth).map(|r| r.path));
println!("{:?}", run(&chain, admin).map(|r| r.path));
}Ok([("Authorization", "Bearer x"), ("X-Request-Id", "req-77")])
Err("401 unauthorized")
Err("403 forbidden: /admin/users")23. Realistic example: payment processors chosen at runtime
trait PaymentProcessor {
fn name(&self) -> &'static str;
fn fee_cents(&self, amount_cents: u64) -> u64;
fn charge(&self, amount_cents: u64) -> Result<String, String>;
}
struct Stripe;
struct BankTransfer {
max_cents: u64,
}
impl PaymentProcessor for Stripe {
fn name(&self) -> &'static str {
"stripe"
}
fn fee_cents(&self, amount: u64) -> u64 {
30 + amount * 29 / 1000
}
fn charge(&self, amount: u64) -> Result<String, String> {
Ok(format!("ch_{amount}"))
}
}
impl PaymentProcessor for BankTransfer {
fn name(&self) -> &'static str {
"bank"
}
fn fee_cents(&self, _amount: u64) -> u64 {
0
}
fn charge(&self, amount: u64) -> Result<String, String> {
if amount > self.max_cents { Err(format!("over bank limit of {}", self.max_cents)) } else { Ok(format!("bt_{amount}")) }
}
}
fn processor_for(country: &str) -> Box<dyn PaymentProcessor> {
match country {
"DE" | "NL" => Box::new(BankTransfer { max_cents: 500_000 }),
_ => Box::new(Stripe),
}
}
fn main() {
for (country, amount) in [("US", 10_000), ("DE", 10_000), ("NL", 900_000)] {
let p = processor_for(country);
match p.charge(amount) {
Ok(id) => println!("{country}: {} charged {amount} (fee {}) -> {id}", p.name(), p.fee_cents(amount)),
Err(e) => println!("{country}: {} failed: {e}", p.name()),
}
}
}US: stripe charged 10000 (fee 320) -> ch_10000
DE: bank charged 10000 (fee 0) -> bt_10000
NL: bank failed: over bank limit of 50000024. Realistic example: a report built from pluggable sections
struct Data {
orders: Vec<(u32, u64)>, // (id, cents)
refunds: Vec<u64>,
}
trait Section {
fn title(&self) -> String;
fn body(&self, data: &Data) -> String;
}
struct Totals;
struct LargestOrder;
struct RefundRate;
impl Section for Totals {
fn title(&self) -> String {
String::from("Totals")
}
fn body(&self, d: &Data) -> String {
let revenue: u64 = d.orders.iter().map(|(_, c)| c).sum();
format!("{} orders, revenue {:.2}", d.orders.len(), revenue as f64 / 100.0)
}
}
impl Section for LargestOrder {
fn title(&self) -> String {
String::from("Largest order")
}
fn body(&self, d: &Data) -> String {
match d.orders.iter().max_by_key(|(_, c)| *c) {
Some((id, c)) => format!("#{id} at {:.2}", *c as f64 / 100.0),
None => String::from("none"),
}
}
}
impl Section for RefundRate {
fn title(&self) -> String {
String::from("Refund rate")
}
fn body(&self, d: &Data) -> String {
format!("{:.1}%", d.refunds.len() as f64 * 100.0 / d.orders.len().max(1) as f64)
}
}
fn render(sections: &[Box<dyn Section>], data: &Data) -> String {
sections.iter().map(|s| format!("## {}\n{}", s.title(), s.body(data))).collect::<Vec<_>>().join("\n\n")
}
fn main() {
let data = Data { orders: vec![(1, 4_500), (2, 129_900), (3, 2_000), (4, 9_950)], refunds: vec![2_000] };
let sections: Vec<Box<dyn Section>> = vec![Box::new(Totals), Box::new(LargestOrder), Box::new(RefundRate)];
println!("{}", render(§ions, &data));
}<h2 id="totals">Totals</h2>
4 orders, revenue 1463.50
<h2 id="largest-order">Largest order</h2>
#2 at 1299.00
<h2 id="refund-rate">Refund rate</h2>
25.0%25. Realistic example: a game entity loop
A game loop updates many kinds of entities through one trait. Some entities die and are removed.
trait Entity {
fn update(&mut self, tick: u32);
fn alive(&self) -> bool;
fn describe(&self) -> String;
}
struct Player {
hp: i32,
x: i32,
}
struct Projectile {
x: i32,
range_left: u32,
}
impl Entity for Player {
fn update(&mut self, tick: u32) {
self.x += 1;
if tick == 2 {
self.hp -= 30;
}
}
fn alive(&self) -> bool {
self.hp > 0
}
fn describe(&self) -> String {
format!("player hp={} x={}", self.hp, self.x)
}
}
impl Entity for Projectile {
fn update(&mut self, _tick: u32) {
self.x += 3;
self.range_left = self.range_left.saturating_sub(1);
}
fn alive(&self) -> bool {
self.range_left > 0
}
fn describe(&self) -> String {
format!("projectile x={} range={}", self.x, self.range_left)
}
}
fn main() {
let mut world: Vec<Box<dyn Entity>> = vec![
Box::new(Player { hp: 100, x: 0 }),
Box::new(Projectile { x: 0, range_left: 2 }),
Box::new(Projectile { x: 5, range_left: 4 }),
];
for tick in 1..=3 {
for e in world.iter_mut() {
e.update(tick);
}
world.retain(|e| e.alive());
let states: Vec<String> = world.iter().map(|e| e.describe()).collect();
println!("tick {tick}: {}", states.join(" | "));
}
}tick 1: player hp=100 x=1 | projectile x=3 range=1 | projectile x=8 range=3
tick 2: player hp=70 x=2 | projectile x=11 range=2
tick 3: player hp=70 x=3 | projectile x=14 range=126. Realistic example: storage backends behind one interface
trait BlobStore {
fn put(&mut self, key: &str, data: &[u8]) -> Result<(), String>;
fn get(&self, key: &str) -> Option<Vec<u8>>;
fn describe(&self) -> String;
}
struct InMemory {
blobs: Vec<(String, Vec<u8>)>,
}
struct SizeLimited {
inner: Box<dyn BlobStore>,
max_bytes: usize,
}
impl BlobStore for InMemory {
fn put(&mut self, key: &str, data: &[u8]) -> Result<(), String> {
self.blobs.retain(|(k, _)| k != key);
self.blobs.push((key.to_string(), data.to_vec()));
Ok(())
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
self.blobs.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
}
fn describe(&self) -> String {
format!("in-memory ({} blobs)", self.blobs.len())
}
}
impl BlobStore for SizeLimited {
fn put(&mut self, key: &str, data: &[u8]) -> Result<(), String> {
if data.len() > self.max_bytes {
return Err(format!("{key}: {} bytes exceeds limit of {}", data.len(), self.max_bytes));
}
self.inner.put(key, data)
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
self.inner.get(key)
}
fn describe(&self) -> String {
format!("size-limited({}) over {}", self.max_bytes, self.inner.describe())
}
}
fn main() {
let mut store: Box<dyn BlobStore> = Box::new(SizeLimited { inner: Box::new(InMemory { blobs: Vec::new() }), max_bytes: 8 });
println!("{:?}", store.put("avatar", b"PNG..."));
println!("{:?}", store.put("video", b"way too large for the limit"));
println!("{:?}", store.get("avatar").map(|b| String::from_utf8_lossy(&b).to_string()));
println!("{}", store.describe());
}Ok(())
Err("video: 27 bytes exceeds limit of 8")
Some("PNG...")
size-limited(8) over in-memory (1 blobs)SizeLimited wraps any other store, the decorator pattern, and is itself a BlobStore, so decorators can stack.
27. Realistic example: static and dynamic dispatch together
Generic code accepts trait objects too, because dyn Trait itself implements the trait. A single generic function then serves both fast, specialized calls and flexible runtime ones.
trait Metric {
fn value(&self) -> f64;
}
struct Cpu(f64);
struct Memory { used: f64, total: f64 }
impl Metric for Cpu {
fn value(&self) -> f64 {
self.0
}
}
impl Metric for Memory {
fn value(&self) -> f64 {
self.used / self.total * 100.0
}
}
fn over_threshold<M: Metric + ?Sized>(metric: &M, threshold: f64) -> bool {
metric.value() > threshold
}
fn main() {
// static dispatch: M = Cpu
println!("{}", over_threshold(&Cpu(91.0), 90.0));
// dynamic dispatch: M = dyn Metric
let metrics: Vec<Box<dyn Metric>> = vec![Box::new(Cpu(40.0)), Box::new(Memory { used: 15.0, total: 16.0 })];
for m in &metrics {
println!("{:.1} over 90: {}", m.value(), over_threshold(m.as_ref(), 90.0));
}
}true
40.0 over 90: false
93.8 over 90: trueThe ?Sized bound from Trait Bounds is what allows M = dyn Metric.
Summary
dyn Traitis "some type implementingTrait", resolved at runtime through a vtable. Use it behind a pointer:&dyn Trait,&mut dyn Trait, orBox<dyn Trait>.- Trait objects allow mixed collections (
Vec<Box<dyn Trait>>), runtime choice of implementation, and returning different types from one function. - A trait object pointer is a fat pointer: data pointer plus vtable pointer.
- Only dyn-compatible traits work as
dyn Trait: no generic methods, no methods returningSelf(unless markedwhere Self: Sized). - Clone boxed trait objects with a
clone_boxmethod. Downcast withAnyonly when truly needed. - Prefer enums for closed sets of types and trait objects for open ones. Prefer generics when you don't need mixing, for zero-cost static dispatch.
Next: Advanced Traits.
