OOP#
Rust has no classes and no inheritance. Object orientation is
struct (state), enum (sum types), impl (methods),
and trait (shared behaviour, structurally satisfied).
Polymorphism is by generics + trait bounds (static
dispatch) or trait objects (dyn Trait, dynamic
dispatch).
/types`. For
function signatures (Self, &self, &mut self), see
Functions.
Methods#
Methods live in an impl block attached to a type. self,
&self, &mut self describe the receiver.
struct Counter { n: u32 }
impl Counter {
fn new() -> Self { Self { n: 0 } } // associated function (no self)
fn get(&self) -> u32 { self.n }
fn inc(&mut self) { self.n += 1; }
fn into_total(self) -> u32 { self.n } // consumes self
}
let mut c = Counter::new();
c.inc();
println!("{}", c.get()); // 1
Associated functions without self are constructors and
type-level helpers; call them with Type::name (no
instance needed).
Constructors#
Rust has no special constructor syntax. Convention: a function
named new returns Self; functions named with_* /
from_* /try_new cover variants and fallible cases.
impl Server {
fn new(addr: &str) -> Self { Self { addr: addr.into() } }
fn with_timeout(addr: &str, t: Duration) -> Self { /* … */ }
fn try_open(path: &str) -> std::io::Result<Self> { /* … */ }
}
For builder-style construction, see Patterns.
Traits#
A trait declares a set of methods. Implementations are written
in separate impl Trait for Type blocks.
trait Greet {
fn greet(&self) -> String;
fn shout(&self) -> String { self.greet().to_uppercase() } // default
}
struct User { name: String }
impl Greet for User {
fn greet(&self) -> String { format!("hello, {}", self.name) }
}
let u = User { name: "rk".into() };
println!("{}", u.greet());
Default method bodies let the operator override only what differs.
Associated types#
Traits can declare type aliases the implementor fills in.
Used heavily by Iterator, Future, IntoIterator.
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter { n: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
self.n += 1;
Some(self.n)
}
}
Generic + trait bound#
Static-dispatch polymorphism. The compiler monomorphises a copy per concrete type.
trait Listener { fn on_msg(&self, msg: &str); }
fn broadcast<L: Listener>(listeners: &[L], msg: &str) {
for l in listeners { l.on_msg(msg); }
}
Trait objects (dyn)#
Dynamic-dispatch polymorphism. Box<dyn Trait> /
&dyn Trait is a fat pointer (data + vtable).
fn dispatch(handlers: &[Box<dyn Listener>], msg: &str) {
for h in handlers { h.on_msg(msg); }
}
let mixed: Vec<Box<dyn Listener>> = vec![
Box::new(EmailListener),
Box::new(LogListener),
];
dispatch(&mixed, "hello");
Trait must be object-safe (no associated types depending on
Self, no generic methods); the compiler tells the operator
when a trait fails to qualify.
Enums + impl#
Enums with methods are go-tos for sum-type
algebra. match exhausts variants; the compiler complains
if the operator misses one.
enum Shape { Circle(f64), Rect { w: f64, h: f64 } }
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect { w, h } => w * h,
}
}
}
Derives#
#[derive(...)] auto-implements the listed traits.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
struct Config { host: String, port: u16 }
Common derives.
Trait |
What it gives |
|---|---|
|
|
|
deep / shallow value duplication |
|
|
|
ordering operators |
|
hashability (required for |
|
|
Blanket impls#
An impl<T: Bound> Trait for T writes the trait for every
type satisfying the bound. The stdlib uses this to make every
Display value also implement ToString.
impl<T: std::fmt::Display> ToString for T {
fn to_string(&self) -> String { format!("{self}") }
}
Write blanket impls cautiously; they affect inference across the whole crate graph.
self vs Self#
Inside an impl or trait block, Self is the receiver
type; self (lowercase) is the receiver value.
impl Counter {
fn new() -> Self { Self { n: 0 } }
fn double(&self) -> Self { Self { n: self.n * 2 } }
}
Newtype + trait#
The newtype pattern (single-field tuple struct) plus a trait impl gives a distinct type with a focused API.
struct UserId(u64);
impl std::fmt::Display for UserId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "user-{}", self.0)
}
}