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

Debug

{:?} formatter (the operator’s main tool)

Clone / Copy

deep / shallow value duplication

PartialEq / Eq

== / != (Eq adds the reflexive guarantee)

PartialOrd / Ord

ordering operators

Hash

hashability (required for HashMap keys)

Default

Default::default() constructor

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)
    }
}

References#