Structs & Enums#

Structs group related data. Enums represent values that can be one of several variants. Together with pattern matching, they form Rust’s primary data modelling toolkit — replacing classes, inheritance, and null in other languages.


Structs#

struct User {
    id: u64,
    name: String,
    email: String,
    active: bool,
}

let user = User {
    id: 1,
    name: String::from("Alice"),
    email: String::from("alice@example.com"),
    active: true,
};

println!("{}", user.name);

Field Init Shorthand#

fn new_user(name: String, email: String) -> User {
    User {
        id: generate_id(),
        name,     // shorthand: field name matches variable name
        email,    // same
        active: true,
    }
}

Struct Update Syntax#

let user2 = User {
    email: String::from("bob@example.com"),
    ..user  // take remaining fields from user (MOVES String fields)
};

Tuple Structs#

struct Color(u8, u8, u8);
struct Point(f64, f64);

let red = Color(255, 0, 0);
let origin = Point(0.0, 0.0);
println!("r={}", red.0);

Unit Structs#

struct AlwaysEqual;  // no fields, used as markers or trait implementations

Methods#

Defined in impl blocks:

impl User {
    // Associated function (constructor) — no self
    fn new(name: String, email: String) -> Self {
        Self {
            id: generate_id(),
            name,
            email,
            active: true,
        }
    }

    // Method — borrows self immutably
    fn full_info(&self) -> String {
        format!("{} <{}>", self.name, self.email)
    }

    // Method — borrows self mutably
    fn deactivate(&mut self) {
        self.active = false;
    }

    // Method — consumes self (takes ownership)
    fn into_name(self) -> String {
        self.name
    }
}

let mut user = User::new("Alice".into(), "alice@example.com".into());
println!("{}", user.full_info());
user.deactivate();
let name = user.into_name();  // user is consumed, no longer usable

Receiver Types#

ReceiverMeaningWhen
&selfImmutable borrowRead-only access
&mut selfMutable borrowModify the struct
selfTake ownershipTransform or consume the struct

Enums#

Enums in Rust are algebraic data types (tagged unions). Each variant can hold different data:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

Enums with Named Fields#

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

Methods on Enums#

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle { base, height } => 0.5 * base * height,
        }
    }
}

Pattern Matching#

match is exhaustive — you must handle every variant:

fn describe(shape: &Shape) -> String {
    match shape {
        Shape::Circle { radius } => format!("circle with radius {radius}"),
        Shape::Rectangle { width, height } => format!("{width}x{height} rectangle"),
        Shape::Triangle { .. } => "triangle".to_string(),
    }
}

Match Arms#

match value {
    1 => println!("one"),
    2 | 3 => println!("two or three"),
    4..=10 => println!("four to ten"),
    n if n < 0 => println!("negative: {n}"),  // guard
    _ => println!("something else"),           // wildcard
}

if let and let else#

// if let — handle one variant, ignore the rest
if let Some(value) = optional {
    println!("got {value}");
}

// let else (Rust 1.65+) — handle the happy path, diverge on mismatch
let Some(value) = optional else {
    return Err(anyhow!("missing value"));
};
// value is available here

while let#

while let Some(item) = stack.pop() {
    process(item);
}

Option<T> — Null Replacement#

Rust has no null. Instead, Option<T> represents a value that might be absent:

enum Option<T> {
    Some(T),
    None,
}
fn find_user(id: u64) -> Option<User> {
    if id == 1 {
        Some(User { id: 1, name: "Alice".into(), /* ... */ })
    } else {
        None
    }
}

match find_user(1) {
    Some(user) => println!("found: {}", user.name),
    None => println!("not found"),
}

// Combinators
let name = find_user(1).map(|u| u.name).unwrap_or("unknown".into());
let user = find_user(1).ok_or(anyhow!("user not found"))?;

Common Option Methods#

MethodBehaviour
unwrap()Returns value or panics
expect("msg")Returns value or panics with message
unwrap_or(default)Returns value or default
unwrap_or_else(|| expr)Returns value or computes default
map(f)Transforms the inner value
and_then(f)Chains operations that return Option
ok_or(err)Converts Option to Result
is_some() / is_none()Boolean check

Result<T, E> — Error Handling#

enum Result<T, E> {
    Ok(T),
    Err(E),
}
fn parse_port(s: &str) -> Result<u16, String> {
    s.parse::<u16>().map_err(|e| format!("invalid port: {e}"))
}

match parse_port("8080") {
    Ok(port) => println!("port: {port}"),
    Err(e) => eprintln!("error: {e}"),
}

Result is covered in depth in the Error Handling section.


Deriving Traits#

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
    id: u64,
    name: String,
}

#[derive(Debug, Clone, PartialEq)]
enum Status {
    Active,
    Inactive,
    Suspended,
}
DeriveProvides
Debug{:?} formatting
Clone.clone() method
CopyImplicit copy (stack-only types)
PartialEq== and !=
EqTotal equality (PartialEq + reflexive)
HashHashable (for HashMap/HashSet keys)
PartialOrd<, >, <=, >=
OrdTotal ordering
DefaultDefault::default()
Serialize / Deserializeserde serialisation (external crate)

Key Takeaways#

  • Structs hold related data; enums represent one-of-many variants. Together they replace classes and inheritance.
  • Enums can hold data in each variant — they are algebraic data types, not just integer constants.
  • match is exhaustive — the compiler forces you to handle every case. This eliminates missed branches.
  • Option<T> replaces null. You cannot use a value that might be absent without checking first.
  • Result<T, E> replaces exceptions. Errors are values in the return type, not hidden control flow.
  • if let and let else simplify matching when you only care about one variant.
  • Derive macros (Debug, Clone, PartialEq) generate boilerplate trait implementations automatically.