Types & Traits
Structs, enums, traits, generics, derive macros.
Types & Traits
Rust's type system has three pillars: structs for product types, enums for sum types, and traits for behaviour. Together with generics, these compose into the most expressive type system in mainstream production languages — capable of catching at compile time what other languages defer to runtime tests.
Structs — product types
A struct is a record:
struct Point {
x: f64,
y: f64,
}
struct Color(u8, u8, u8); // tuple struct — positional fields
struct Marker; // unit struct — no fields, used as a type-level tag
let p = Point { x: 1.0, y: 2.0 };
let c = Color(255, 0, 128);
let _m = Marker;
Construction is by name (Point { x, y }) or position (Color(r, g, b)). Field access is .name or .0/.1 for tuple structs. Structs are passed by value (move or copy depending on field types).
Enums — sum types
An enum is a tagged union: a value is exactly one of several variants, each carrying its own data:
enum Shape {
Circle { radius: f64 }, // struct-like variant
Rectangle(f64, f64), // tuple variant
Point, // unit variant
}
let s = Shape::Circle { radius: 1.5 };
The two enums you'll use constantly — both in the standard library, both critical:
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Option replaces null. Result replaces exceptions. We'll cover both in depth in lessons 03 and 05.
Methods & associated functions
Both structs and enums get methods via impl blocks:
impl Point {
// Associated function (no `self`) — called as Point::origin()
fn origin() -> Self {
Point { x: 0.0, y: 0.0 }
}
// Method (takes `&self`) — called as p.distance(&other)
fn distance(&self, other: &Point) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
// Mutating method — takes `&mut self`
fn translate(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
}
Self (capital S) is the type itself. self (lowercase) is the receiver. Three flavours: self (consume), &self (read-only borrow), &mut self (mutable borrow).
Traits — capability declarations
A trait is a contract: "any type implementing this trait provides these methods." Roughly Java's interface, but more flexible:
trait Greet {
fn hello(&self) -> String;
// Default implementation — implementers can override or accept it.
fn shout(&self) -> String {
format!("{}!!!", self.hello())
}
}
struct Cat;
impl Greet for Cat {
fn hello(&self) -> String {
String::from("meow")
}
}
let c = Cat;
println!("{}", c.shout()); // "meow!!!"
You can implement traits on types you don't own (within limits — the orphan rule requires either the trait or the type to be defined in your crate).
Generics + trait bounds
Generics let one definition work for many types. Trait bounds say which types qualify:
use std::fmt::Display;
fn announce<T: Display>(thing: T) {
println!("Behold: {}", thing);
}
// Equivalent with `where` clause — easier to read with multiple bounds:
fn announce_two<T, U>(t: T, u: U)
where
T: Display,
U: Display + Clone,
{
println!("{} and {}", t, u.clone());
}
Generics in Rust are monomorphised — the compiler generates a separate copy per concrete type used. There is zero runtime cost, no boxing, no virtual dispatch. The downside is binary size grows with the number of instantiations.
The big four derive traits
Most of your structs will start with this incantation:
#[derive(Debug, Clone, PartialEq, Eq)]
struct User {
id: u64,
name: String,
}
| Trait | What it gives you |
|---|---|
Debug |
println!("{:?}", x) — developer-facing format |
Clone |
x.clone() — explicit deep copy |
Copy |
implicit copy on assignment (only if all fields are Copy) |
PartialEq / Eq |
== and != |
PartialOrd / Ord |
<, >, .cmp() |
Hash |
usable as a HashMap key |
Default |
User::default() returns a zero-valued instance |
Each #[derive(X)] expands at compile time into a full impl X for User { ... }. Use cargo expand to see the generated code.
Display vs Debug
Two formatting traits, distinct purposes:
use std::fmt;
struct Money { cents: u64 }
// Debug — auto-derivable; for developers / logs.
#[derive(Debug)]
struct DebugMe { x: i32, y: i32 }
// Display — write it by hand; for end users.
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "${}.{:02}", self.cents / 100, self.cents % 100)
}
}
println!("{:?}", DebugMe { x: 1, y: 2 }); // DebugMe { x: 1, y: 2 }
println!("{}", Money { cents: 12345 }); // $123.45
Display being non-derivable is intentional: every type's user-facing representation is a design decision, not a mechanical translation.
Trait objects (dyn) — runtime polymorphism
When you genuinely need a heterogeneous collection of trait implementers, use a trait object:
let shapes: Vec<Box<dyn Greet>> = vec![
Box::new(Cat),
Box::new(Dog),
];
for s in &shapes {
println!("{}", s.hello()); // virtual dispatch via vtable
}
dyn Greet is "some unspecified type implementing Greet". This is dynamic dispatch — slightly slower than monomorphisation, but enables runtime heterogeneity. The trait must be object-safe (no generic methods, no Self returns).
When to reach for what
| You want | Use |
|---|---|
| A record with named fields | struct |
| A value that's one-of-several shapes | enum |
| "These types support this operation" | trait + generics |
| Heterogeneous collection of implementers | Vec<Box<dyn Trait>> |
| Compile-time polymorphism, zero cost | <T: Trait> |
| A field that's optional | Option<T> |
| A function that can fail | Result<T, E> |
Internalise this table and 80% of Rust API design becomes mechanical.
Tools in the wild
4 tools- clirustupfree tier
Install and switch toolchains; ships rustc, cargo, and stdlib documentation.
- clicargofree tier
`cargo doc --open` renders the full standard-library trait reference locally.
- cliclippyfree tier
Suggests when to derive a trait, when to use `Default`, when to prefer `From` over `Into`.
- clicargo-expandfree tier
See what `#[derive(...)]` macros expand to — invaluable when learning the trait machinery.