Hedronite Lesson · Polyglot-Dev / Rust · Thu 2026-10-01

OOP / trait objects

Rust keeps objects and encapsulation. It refuses inheritance. Trait objects carry shared behavior at runtime.

Lesson Class: Duha (Rust language track · Beat A · topic T1)
Focus: objects + encapsulation · no inheritance · Box · open collections
Code Blocks: clean blocks from TRPL 18-1..18-5 + tiny Draw proof, explanation in prose
Done-criteria: Can state what Rust will and will not do for OOP, and use a trait object
Grounding: TRPL stable ch18-00 + ch18-01 + ch18-02 · ch18-03 state pattern held · Topics #22 held
The Object Ledger
Objects and encapsulation yes; classical inheritance no.
The Dyn Pointer
Box (pointer + dyn) is a trait object for shared behavior.
The Open Collection
Trait objects mix concrete types in one Vec; generics stay homogeneous.
Rust keeps objects and encapsulation. It refuses inheritance. Trait objects carry shared behavior at runtime.

<!-- hal:authoritative:yaml -->

Rust keeps objects and encapsulation. It refuses inheritance. Trait objects carry shared behavior at runtime.

§I - Frame

Duha session 21. Topics #21, TRPL Chapter 18: what counts as object-oriented, what Rust will and will not do for that style, and how a trait object stands in for many concrete types. Session 20 taught async / .await. This row leaves the runtime and opens polymorphism through traits.

The book opens with a frank map: by some definitions Rust is object oriented; by others it is not. Three characteristics usually appear in the debate: objects, encapsulation, and inheritance. Done-criteria for this row: state what Rust will and will not do for OOP, and use a trait object. The ch18-03 state-pattern walkthrough can wait; the ledger and the dyn pointer are enough for the Done cell.

Three moves land by the end:

  1. The Object Ledger: objects and encapsulation yes; classical inheritance no.
  2. The Dyn Pointer: Box<dyn Trait> (or another pointer + dyn) is a trait object.
  3. The Open Collection: trait objects let one Vec hold many concrete types that share a trait.

Done-criteria: Can state what Rust will and will not do for OOP, and use a trait object.

§II - The Object Ledger

Gang of Four style: an object packages data and the procedures that operate on that data. In Rust, structs and enums hold data; impl blocks hold methods. Under that definition, Rust qualifies.

Encapsulation is the next column. Private fields stay private unless you mark them pub. Listing 18-1 / 18-2 shape keeps the average honest by forcing every mutation through methods:

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

impl AveragedCollection {
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }

    pub fn remove(&mut self) -> Option<i32> {
        let result = self.list.pop();
        match result {
            Some(value) => {
                self.update_average();
                Some(value)
            }
            None => None,
        }
    }

    pub fn average(&self) -> f64 {
        self.average
    }

    fn update_average(&mut self) {
        let total: i32 = self.list.iter().sum();
        self.average = total as f64 / self.list.len() as f64;
    }
}

Callers see add, remove, and average. They never touch list or average directly, so the cache cannot drift. That is encapsulation without a class keyword.

Inheritance is the missing column. There is no parent struct whose fields and method bodies a child struct inherits. Default trait methods give limited reuse; they are not a type hierarchy. If a language must ship inheritance to count as OOP, Rust refuses that test on purpose.

Thus the Object Ledger: data+methods and pub boundaries yes; classical inheritance no.

§III - The Dyn Pointer

Chapter 8 used an enum when the set of types was closed at compile time. Chapter 18 needs an open set: a GUI library that draws components the library author has not named yet. Inheritance would invent a Component base class. Rust defines a trait instead.

Listing 18-3 through 18-5 shape:

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

A trait object is a pointer (Box, &, and friends) plus dyn plus the trait. It points at a concrete value and at a table of method addresses for that trait. Screen::run only needs draw; it does not need the concrete type name. The book stresses the limit: you cannot add fields to a trait object. Its job is shared behavior, not a bag of data like objects in some other languages.

Button and a user-defined SelectBox both implement Draw, then enter the vector as Box::new(...). At runtime, each draw call dispatches through the table for that value's type.

Thus the Dyn Pointer: pointer + dyn Trait abstracts over implementors the library did not list.

§IV - The Open Collection

Generics with a trait bound monomorphize to one concrete type per instantiation. Listing 18-6 shape (Screen<T: Draw> with Vec<T>) forces every component in that screen to be the same T. That is faster when the collection is homogeneous.

Trait objects trade that for openness. One Screen can hold Box<Button> and Box<SelectBox> in the same Vec<Box<dyn Draw>>. The cost is dynamic dispatch and the object-safety rules the book names later in the section (methods that need Self: Sized, or that return Self, cannot go on a trait object). For this row, remember the choice: closed homogeneous set → generic; open heterogeneous set → trait object.

Write a tiny proof without a full GUI:

trait Draw {
    fn draw(&self);
}

struct Label { text: String }
struct Dot;

impl Draw for Label {
    fn draw(&self) {
        println!("label: {}", self.text);
    }
}

impl Draw for Dot {
    fn draw(&self) {
        println!(".");
    }
}

fn main() {
    let screen: Vec<Box<dyn Draw>> = vec![
        Box::new(Label { text: String::from("ready") }),
        Box::new(Dot),
    ];
    for component in screen.iter() {
        component.draw();
    }
}

Two concrete types, one trait, one vector. That is the Open Collection.

§V - Proof and close

  1. Fill the Object Ledger in one sentence each: objects, encapsulation, inheritance (yes / yes / no).
  2. Point at AveragedCollection and name which fields stay private and why.
  3. Write Box<dyn Draw> (or another pointer + dyn) and say what two things the trait object points at.
  4. Contrast Screen<T: Draw> with Vec<Box<dyn Draw>>: which collection can mix concrete types?

Done-criteria: Can state what Rust will and will not do for OOP, and use a trait object.

Next: Patterns (Topics #22, TRPL Ch.19). Keep the Dyn Pointer; #22 leaves trait objects and opens pattern syntax.

Related