TechLead
Lesson 6 of 28
5 min read
Rust

Structs & Enums

Define Rust structs, implement methods with impl blocks, use enums with data, and master Option<T> and Result<T,E> types.

Defining Structs

Structs are custom data types that group related fields together. They are similar to classes in other languages but without inheritance. Rust uses composition and traits instead.

// Named-field struct
#[derive(Debug)]
struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

fn main() {
    // Creating an instance
    let user1 = User {
        username: String::from("ferris"),
        email: String::from("ferris@rust-lang.org"),
        active: true,
        sign_in_count: 1,
    };
    println!("{:?}", user1);

    // Mutable struct (entire struct must be mutable)
    let mut user2 = User {
        username: String::from("crab"),
        email: String::from("crab@sea.com"),
        active: true,
        sign_in_count: 0,
    };
    user2.sign_in_count += 1;

    // Struct update syntax (like spread in JS)
    let user3 = User {
        email: String::from("new@email.com"),
        ..user2  // remaining fields from user2 (moves String fields!)
    };
    println!("{:?}", user3);
}

Methods with impl Blocks

#[derive(Debug)]
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // Associated function (like a static method) — no self
    fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }
    }

    fn square(size: f64) -> Self {
        Rectangle { width: size, height: size }
    }

    // Method — takes &self (immutable borrow)
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    // Method that takes &mut self
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    // Method that takes self (consumes the struct)
    fn into_description(self) -> String {
        format!("{}x{} rectangle", self.width, self.height)
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let mut rect = Rectangle::new(30.0, 50.0);
    println!("Area: {}", rect.area());
    println!("Perimeter: {}", rect.perimeter());

    rect.scale(2.0);
    println!("Scaled: {:?}", rect);

    let small = Rectangle::square(10.0);
    println!("Can hold? {}", rect.can_hold(&small));

    let desc = rect.into_description();
    println!("{desc}");
    // rect is consumed — can't use it anymore
}

Tuple Structs and Unit Structs

// Tuple structs — named tuples
struct Color(u8, u8, u8);
struct Point(f64, f64, f64);

// Even though both have similar fields, they're different types
// let p: Point = Color(255, 0, 0); // ERROR: type mismatch

// Unit struct — zero-sized, useful for traits
struct Marker;

fn main() {
    let red = Color(255, 0, 0);
    let origin = Point(0.0, 0.0, 0.0);
    println!("Red: ({}, {}, {})", red.0, red.1, red.2);
    println!("Origin: ({}, {}, {})", origin.0, origin.1, origin.2);
}

Enums

Rust enums are algebraic data types — each variant can hold different types and amounts of data. They're far more powerful than enums in C or Java.

#[derive(Debug)]
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

#[derive(Debug)]
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

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

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));
    println!("{:?} {:?}", home, loopback);

    let shapes = vec![
        Shape::Circle { radius: 5.0 },
        Shape::Rectangle { width: 4.0, height: 6.0 },
        Shape::Triangle { base: 3.0, height: 8.0 },
    ];
    for shape in &shapes {
        println!("{:?} area = {:.2}", shape, shape.area());
    }
}

Option<T> and Result<T, E>

No Null in Rust

Rust has no null. Instead, it uses Option<T> to represent a value that might be absent, and Result<T, E> for operations that might fail. The compiler forces you to handle both cases.

fn main() {
    // Option = Some(T) | None
    let some_number: Option = Some(42);
    let no_number: Option = None;

    // You MUST handle the None case
    match some_number {
        Some(n) => println!("Got: {n}"),
        None => println!("Nothing"),
    }

    // Useful Option methods
    let val = some_number.unwrap_or(0);       // 42
    let val2 = no_number.unwrap_or_default();  // 0
    let mapped = some_number.map(|n| n * 2);   // Some(84)
    let filtered = some_number.filter(|&n| n > 100); // None

    println!("{val} {val2} {mapped:?} {filtered:?}");

    // Result = Ok(T) | Err(E)
    let result: Result = Ok(42);
    let error: Result = Err("oops".to_string());

    match result {
        Ok(n) => println!("Success: {n}"),
        Err(e) => println!("Error: {e}"),
    }

    // Practical example
    let parsed: Result = "42".parse();
    let failed: Result = "abc".parse();
    println!("{:?} {:?}", parsed, failed);
}

Key Takeaways

  • ✅ Structs group data; impl blocks add methods and associated functions
  • ✅ Methods take &self, &mut self, or self depending on access needs
  • ✅ Enums can hold data in each variant — they're algebraic data types
  • Option<T> replaces null; Result<T, E> replaces exceptions
  • ✅ The compiler forces you to handle all cases, eliminating null pointer errors

Continue Learning