thecodex.expert · The Codex Family of Knowledge
Rust

Types and Traits

Structs hold data, enums model variants, traits define behaviour — no classes, no inheritance, no null.

Rust 2024 edition Memory safe Last verified:
Canonical Definition

Rust organises behaviour through structs (named-field data types), enums (sum types with optional data in each variant), impl blocks (methods and associated functions), and traits (shared behaviour contracts) — with generics for type-parameterised code and trait objects (dyn Trait) for runtime polymorphism.

One sentence

Rust's type system uses structs for data, enums for variants, and traits for shared behaviour — there are no classes, no inheritance, and no null values.

Structs and enums

Ruststructs_enums.rs
// Struct — named fields of different types
#[derive(Debug)]   // auto-implement Debug for println!("{:?}", ...)
struct User {
    name:    String,
    email:   String,
    age:     u32,
    active:  bool,
}

impl User {
    // Associated function (like a static method) — constructor pattern
    fn new(name: &str, email: &str, age: u32) -> Self {
        User {
            name:   name.to_string(),
            email:  email.to_string(),
            age,              // shorthand when field = variable name
            active: true,
        }
    }

    // Method — takes &self (immutable borrow of the instance)
    fn greeting(&self) -> String {
        format!("Hello, {}!", self.name)
    }

    // Mutable method — takes &mut self
    fn deactivate(&mut self) {
        self.active = false;
    }
}

// Enum — a type that can be one of several variants
#[derive(Debug)]
enum Direction { North, South, East, West }

// Enum with data — each variant can hold different types
#[derive(Debug)]
enum Message {
    Quit,                          // no data
    Move { x: i32, y: i32 },      // struct-like
    Write(String),                 // tuple-like
    ChangeColor(u8, u8, u8),       // tuple with three values
}

fn main() {
    let mut user = User::new("Priya", "priya@example.com", 28);
    println!("{}", user.greeting());   // "Hello, Priya!"
    user.deactivate();
    println!("{:#?}", user);           // pretty debug print

    let msg = Message::Move { x: 10, y: 20 };
    println!("{:?}", msg);
}

Pattern matching

Rustpattern_matching.rs
fn describe_message(msg: &Message) {
    match msg {
        Message::Quit              => println!("Quit"),
        Message::Move { x, y }    => println!("Move to ({x}, {y})"),
        Message::Write(text)       => println!("Write: {text}"),
        Message::ChangeColor(r,g,b) => println!("Color: #{r:02x}{g:02x}{b:02x}"),
    }
    // match must be exhaustive — all variants covered or use _ wildcard
}

// Option<T> — the Rust null replacement
// Option<T> is either Some(value) or None — no null pointer dereferences
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn main() {
    let result = divide(10.0, 2.0);
    match result {
        Some(v) => println!("Result: {v}"),
        None    => println!("Division by zero"),
    }

    // if let — pattern match for one variant
    if let Some(v) = divide(10.0, 3.0) {
        println!("Got: {v:.2}");
    }

    // unwrap_or — default value if None
    let v = divide(10.0, 0.0).unwrap_or(0.0);

    // ? operator — propagate None (or Err) upward
    // fn double_divide(a: f64, b: f64) -> Option<f64> {
    //     let first = divide(a, b)?;   // returns None if divide returns None
    //     Some(first * 2.0)
    // }
}

Traits

Rusttraits.rs
// Trait — defines shared behaviour (like an interface)
trait Animal {
    fn name(&self) -> &str;
    fn sound(&self) -> String;

    // Default implementation — can be overridden
    fn describe(&self) -> String {
        format!("{} says {}", self.name(), self.sound())
    }
}

struct Dog { name: String }
struct Cat { name: String }

impl Animal for Dog {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> String { "Woof".to_string() }
}

impl Animal for Cat {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> String { "Meow".to_string() }
    // Override default
    fn describe(&self) -> String {
        format!("{} says {} (quietly)", self.name(), self.sound())
    }
}

// Trait as a function parameter — accepts any type implementing Animal
fn make_noise(animal: &impl Animal) {
    println!("{}", animal.describe());
}

// Generic syntax — equivalent to above
fn make_noise_generic<T: Animal>(animal: &T) {
    println!("{}", animal.describe());
}

fn main() {
    let dog = Dog { name: "Rex".to_string() };
    let cat = Cat { name: "Whiskers".to_string() };
    make_noise(&dog);   // "Rex says Woof"
    make_noise(&cat);   // "Whiskers says Meow (quietly)"
}

Generics and trait bounds

Rustgenerics.rs
use std::fmt::Display;

// Generic function with multiple trait bounds
fn print_and_return<T: Display + Clone>(value: T) -> T {
    println!("{}", value);
    value.clone()
}

// Where clause — same as above, cleaner for complex bounds
fn print_and_return2<T>(value: T) -> T
where T: Display + Clone
{
    println!("{}", value);
    value.clone()
}

// Generic struct
#[derive(Debug)]
struct Pair<T> {
    first:  T,
    second: T,
}

impl<T: Display + PartialOrd> Pair<T> {
    fn new(first: T, second: T) -> Self { Pair { first, second } }
    fn larger(&self) -> &T {
        if self.first > self.second { &self.first } else { &self.second }
    }
}

// Dynamic dispatch — trait objects
// dyn Trait — pointer to any type implementing the trait
// Stored as a fat pointer: (data pointer, vtable pointer)
fn make_sounds(animals: &[Box<dyn Animal>]) {
    for a in animals {
        println!("{}", a.describe());   // dispatched via vtable at runtime
    }
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog { name: "Rex".to_string() }),
        Box::new(Cat { name: "Whiskers".to_string() }),
    ];
    make_sounds(&animals);
}

Important standard traits

TraitWhat it enablesHow to get it
Debugprintln!("{:?}", value)#[derive(Debug)]
Displayprintln!("{}", value)implement manually
Clonevalue.clone() — deep copy#[derive(Clone)]
Copysilent copy on assignment (stack types)#[derive(Copy, Clone)]
PartialEq== and !=#[derive(PartialEq)]
Ord<, >, sorting#[derive(PartialOrd, Ord, Eq)]
Hashuse as HashMap key#[derive(Hash)]
DefaultType::default()#[derive(Default)]
Iteratorfor loops, map/filter/collectimplement next()
From / Intotype conversionsimplement From<T>
Commonly confused
Traits are not interfaces with virtual dispatch by default. When you use impl Trait or a generic T: Trait, Rust monomorphises — it generates a separate compiled version of the function for each concrete type. This is zero-cost abstraction: no vtable, no pointer indirection. Only dyn Trait uses a vtable and incurs dynamic dispatch.
Option<T> is the replacement for null — not an addition. Rust has no null keyword and no null pointer. Every value is either present or absent in an explicit, type-checked way. You cannot accidentally dereference a None as if it were a value — the compiler forces you to handle both cases.
Enums in Rust are sum types (algebraic data types), not C-style enums. A Rust enum variant can hold data of any type. Option<T> and Result<T, E> are both enums. This is fundamentally different from Java/C# enums which are just named integers.

Monomorphisation and zero-cost abstractions

When the Rust compiler encounters a generic function fn f<T: Trait>(x: T), it performs monomorphisation at compile time: it generates a separate compiled version of f for each concrete type T that is used. f::<i32> and f::<String> become two different compiled functions. The result is the same machine code as if you had written a non-generic, concrete function — no runtime overhead. This is what Rust means by "zero-cost abstractions": the abstraction compiles away entirely. The trade-off is larger binary size (each monomorphised copy is compiled separately) and longer compile times.

Coherence and the orphan rule

Rust's trait coherence rules prevent conflicting trait implementations. The orphan rule: you can implement a trait for a type only if either the trait or the type is defined in your crate. You cannot implement a foreign trait for a foreign type (e.g., you cannot implement std::fmt::Display for Vec<T> in your crate — both are from std). This prevents two crates from providing conflicting implementations that the compiler cannot choose between. The newtype pattern — wrapping a foreign type in a tuple struct — is the standard workaround when you need to implement a foreign trait for a foreign type.

Sources

1
The Rust Programming Language — Ch 5: Using Structs. doc.rust-lang.org/book/ch05-00-structs.html.
2
The Rust Programming Language — Ch 6: Enums and Pattern Matching. doc.rust-lang.org/book/ch06-00-enums.html.
3
The Rust Programming Language — Ch 10: Generics, Traits, Lifetimes. doc.rust-lang.org/book/ch10-00-generics.html.
4
Rust Reference — Trait Objects. doc.rust-lang.org/reference/types/trait-object.html.
Source confidence: High Last verified: Primary source: The Rust Book · doc.rust-lang.org/book/

Sources

1
The Rust Book — Ch 5: Structs. doc.rust-lang.org/book/ch05-00-structs.html.
2
The Rust Book — Ch 6: Enums. doc.rust-lang.org/book/ch06-00-enums.html.
3
The Rust Book — Ch 10: Generics, Traits. doc.rust-lang.org/book/ch10-00-generics.html.
4
Rust Reference — Trait Objects. doc.rust-lang.org/reference/types/trait-object.html.