Skip to editor content
learningrust.orglesson 6 of 26

Structs & Enums

Structs and enums are Rust's primary tools for creating custom data types. They allow you to group related data together and create meaningful types for your domain.

Defining Structs

A struct (short for "structure") is a custom data type that groups related values together:

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        email: String::from("user@example.com"),
        username: String::from("rustacean"),
        active: true,
        sign_in_count: 1,
    };

    println!("Username: {}", user1.username);
}

Mutable Structs

To modify a struct, the entire instance must be mutable:

struct User {
    username: String,
    email: String,
    active: bool,
}

fn main() {
    let mut user1 = User {
        email: String::from("user@example.com"),
        username: String::from("rustacean"),
        active: true,
    };

    user1.email = String::from("new@example.com");
    println!("New email: {}", user1.email);
}

Struct Update Syntax

Create a new instance from an existing one:

struct User {
    username: String,
    email: String,
    active: bool,
}

fn main() {
    let user1 = User {
        email: String::from("user@example.com"),
        username: String::from("rustacean"),
        active: true,
    };

    // Create user2 with some values from user1
    let user2 = User {
        email: String::from("another@example.com"),
        ..user1  // Take remaining fields from user1
    };

    println!("User2: {}", user2.username);
}

..user1 reads as "fill the rest in from user1", which sounds like copying. It is not — it is the move rule from the ownership lesson applied one field at a time, and that has a consequence most people do not expect the first time they hit it. The program below builds a second account from a first, then reads two of the original's fields; only one of those two reads is legal, which is why the other is commented out. Work out which and why before running.

Predict

After ..base fills in the remaining fields, one of the two reads of base below still compiles and the other does not — which is why one is commented out. What does this program print, and why is the commented line the one that had to go?

struct Account {
  email: String,
  plan: String,
  seats: u32,
}

fn main() {
  let base = Account {
      email: String::from("first@example.com"),
      plan: String::from("pro"),
      seats: 3,
  };

  let upgraded = Account {
      email: String::from("second@example.com"),
      ..base
  };

  println!("{} {}", upgraded.email, upgraded.plan);
  println!("{}", base.seats);
  // println!("{}", base.plan); // this line would stop the program compiling
}

The program prints second@example.com pro and then 3, and the line that had to be commented out is the one reading base.plan. ..base is not a copy — it is a partial move, applying the ordinary move rule one field at a time. plan is a String, so it moved into upgraded and reading base.plan afterwards is error[E0382]: borrow of moved value. seats is a u32, which implements Copy, so it was duplicated and base.seats still works. The struct as a whole is not dead; the compiler tracks exactly which fields left. Whenever a struct-update line breaks a later read, ask which of the filled-in fields owned heap memory.

Tuple Structs

Structs that look like tuples, useful for giving meaning to tuples:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

    println!("Red value: {}", black.0);
    println!("X coordinate: {}", origin.0);
}

Methods on Structs

Use impl blocks to define methods:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // Method that borrows self
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // Method with additional parameters
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }

    // Associated function (no self) - often used as constructors
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let square = Rectangle::square(25);

    println!("Area of rect1: {}", rect1.area());
    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Square area: {}", square.area());
}

Defining Enums

Enums allow you to define a type by enumerating its possible variants:

enum Direction {
    North,
    South,
    East,
    West,
}

fn main() {
    let heading = Direction::North;

    match heading {
        Direction::North => println!("Going north!"),
        Direction::South => println!("Going south!"),
        Direction::East => println!("Going east!"),
        Direction::West => println!("Going west!"),
    }
}

Enums with Data

Enum variants can hold data of different types:

enum Message {
    Quit,                       // No data
    Move { x: i32, y: i32 },   // Named fields (like struct)
    Write(String),              // Single value
    ChangeColor(i32, i32, i32), // Multiple values
}

fn main() {
    let msg1 = Message::Quit;
    let msg2 = Message::Move { x: 10, y: 20 };
    let msg3 = Message::Write(String::from("Hello"));
    let msg4 = Message::ChangeColor(255, 0, 0);

    process_message(msg3);
}

fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("Quitting"),
        Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
        Message::Write(text) => println!("Writing: {}", text),
        Message::ChangeColor(r, g, b) => println!("Color: RGB({}, {}, {})", r, g, b),
    }
}

The Option Enum

Rust's Option type handles the absence of a value (no null!):

fn main() {
    let some_number: Option<i32> = Some(5);
    let no_number: Option<i32> = None;

    // Using match
    match some_number {
        Some(n) => println!("Got number: {}", n),
        None => println!("No number"),
    }

    // Using if let for simpler cases
    if let Some(n) = some_number {
        println!("The number is: {}", n);
    }
}

Methods on Enums

Enums can have methods too:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

impl Coin {
    fn value_in_cents(&self) -> u32 {
        match self {
            Coin::Penny => 1,
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter => 25,
        }
    }
}

fn main() {
    let coin = Coin::Quarter;
    println!("Value: {} cents", coin.value_in_cents());
}

Notice that value_in_cents lists all four variants and has no _ arm. That is not verbosity — it is the safety net. match is exhaustive, so if someone adds a fifth variant to Coin, the compiler refuses to build until this method decides what the new coin is worth. Write _ instead, and you trade that guarantee for a silent default. The program below did exactly that and now reports the wrong total, with no error and no warning to point at. Commit a hypothesis about which coin is being mispriced before you change anything:

Debug

This program adds up the coins in a till. It compiles cleanly, prints no warning, and produces a total that looks entirely plausible — but the assert says it is wrong. Work out which coin is being priced incorrectly and why the compiler said nothing, then fix it.

enum Coin {
  Penny,
  Nickel,
  Dime,
  Quarter,
  HalfDollar,
}

impl Coin {
  fn value_in_cents(&self) -> u32 {
      match self {
          Coin::Penny => 1,
          Coin::Nickel => 5,
          Coin::Dime => 10,
          _ => 25,
      }
  }
}

fn main() {
  let till = vec![
      Coin::Quarter,
      Coin::HalfDollar,
      Coin::Dime,
      Coin::Nickel,
      Coin::Penny,
  ];

  let mut total = 0;
  for coin in &till {
      total += coin.value_in_cents();
  }

  assert_eq!(
      total, 91,
      "a quarter, a half dollar, a dime, a nickel and a penny total 91 cents, got {}",
      total
  );
  println!("Till total: {} cents", total);
}

Expected output: Till total: 91 cents

The bug is the _ => 25 arm. Coin has five variants but the match names only three, so the catch-all is silently handling Quarter and HalfDollar — pricing a fifty-cent piece at twenty-five and returning 66 instead of 91. Nothing warns you, because _ is a perfectly legal pattern that makes the match exhaustive by construction; there is nothing left for the compiler to object to. Naming both arms explicitly (Coin::Quarter => 25, Coin::HalfDollar => 50) fixes the total and restores the guarantee: add a sixth variant later and this method stops compiling with error[E0004]: non-exhaustive patterns, pointing straight at the code that needs a decision. Use _ when the leftovers genuinely share a meaning — not to save typing.

Recall

Without scrolling up: in Ownership & Borrowing you learned that passing a value to a function by value MOVES it, while passing &value only lends it. Now apply that to methods. Rectangle::area above is declared fn area(&self) -> u32. What would change if it were declared fn area(self) -> u32 instead?

A method's receiver is nothing more than its first parameter written in shorthand, and it follows the ownership rules exactly: &self borrows, so the caller keeps the value and can call the method again; &mut self borrows mutably, so the method may change the value in place — which is why a have_birthday-style method needs it and why the caller's binding needs mut; and a bare self consumes the value, right only when the method's job is to convert or destroy it. Reading methods take &self almost always. Choosing a receiver is choosing your API's ownership contract, so choose it deliberately.

Practice Exercise

Try this in the playground:

struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn new(name: &str, age: u32) -> Person {
        Person {
            name: String::from(name),
            age,
        }
    }

    fn greet(&self) {
        println!("Hi, I'm {} and I'm {} years old!", self.name, self.age);
    }

    fn have_birthday(&mut self) {
        self.age += 1;
        println!("Happy birthday! Now I'm {}!", self.age);
    }
}

fn main() {
    let mut person = Person::new("Alice", 30);
    person.greet();
    person.have_birthday();
}

Try It Yourself

Reading about structs and enums is not the same as modelling a domain with them. This is a build task: a small program that reports its own pass/fail. You write an associated function that constructs a struct, a method that computes from its fields, and a method on an enum that branches over every variant. Run it as-is and it fails immediately, naming the first check that did not pass. Implement each one until every check passes and it prints All checks passed.

The three pieces are the lesson in miniature. Item::new is an associated function — no self, called as Item::new(...), the constructor idiom from the Rectangle::square example. line_total is a method taking &self, so the caller keeps its Item and can ask again. And Discount::apply matches on self across every variant with no _ arm, for exactly the reason the debug block above made painful. One detail worth knowing before you start: matching on &self gives you references to the payloads, so a u32 payload arrives as &u32 and needs a * to use it as a number.

Build

Finish the build. One associated function and two methods are stubbed out, and the checks below them fail until each one behaves. Run it as-is to see which check fails first, decide what that piece is missing, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

// A tiny inventory model: line items and the discounts you can apply to them.

struct Item {
  name: String,
  unit_price: u32,
  quantity: u32,
}

impl Item {
  // TODO 1: an ASSOCIATED FUNCTION (no self) that builds an Item from a
  //   &str name plus a price and a quantity. Note the field is a String
  //   while the parameter is a &str, so it needs String::from.
  //   Item::new("bolt", 30, 4) -> an Item named "bolt", priced 30, quantity 4
  fn new(name: &str, unit_price: u32, quantity: u32) -> Item {
      let _ = name;
      let _ = unit_price;
      let _ = quantity;
      Item {
          name: String::new(),
          unit_price: 0,
          quantity: 0,
      }
  }

  // TODO 2: a METHOD that borrows self and returns unit_price * quantity.
  //   Item::new("bolt", 30, 4).line_total() -> 120
  fn line_total(&self) -> u32 {
      0
  }
}

enum Discount {
  None,
  FlatCents(u32),
  Percent(u32),
}

impl Discount {
  // TODO 3: apply the discount to a total, in cents. The match skeleton
  //   is written for you with every variant named and no '_' arm - fill in
  //   what each one produces.
  //     None leaves the total alone.
  //     FlatCents(c) subtracts c but never goes below 0. total.saturating_sub(*c)
  //       does exactly that; note the * because matching on &self hands you a &u32.
  //     Percent(p) removes p percent: total - total * *p / 100.
  //   Discount::None.apply(120) -> 120
  //   Discount::FlatCents(30).apply(120) -> 90
  //   Discount::FlatCents(500).apply(120) -> 0
  //   Discount::Percent(25).apply(120) -> 90
  fn apply(&self, total: u32) -> u32 {
      let _ = total;
      match self {
          Discount::None => {}
          Discount::FlatCents(cents) => {
              let _ = cents;
          }
          Discount::Percent(percent) => {
              let _ = percent;
          }
      }
      0
  }
}

// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
  let bolts = Item::new("bolt", 30, 4);
  assert_eq!(bolts.name, "bolt", "Item::new should store the name it was given");
  assert_eq!(bolts.unit_price, 30, "Item::new should store the unit price it was given");
  assert_eq!(bolts.quantity, 4, "Item::new should store the quantity it was given");

  assert_eq!(bolts.line_total(), 120, "line_total is unit_price * quantity");
  assert_eq!(Item::new("washer", 7, 0).line_total(), 0, "a zero quantity costs nothing");

  assert_eq!(Discount::None.apply(120), 120, "None must leave the total untouched");
  assert_eq!(Discount::FlatCents(30).apply(120), 90, "FlatCents(30) takes 30 off 120");
  assert_eq!(Discount::FlatCents(500).apply(120), 0, "a flat discount must not go below zero");
  assert_eq!(Discount::Percent(25).apply(120), 90, "Percent(25) removes a quarter of 120");

  assert_eq!(
      Discount::Percent(10).apply(Item::new("hinge", 250, 2).line_total()),
      450,
      "a 10 percent discount on 2 hinges at 250 each is 450"
  );

  println!("All checks passed.");
  println!("Line total for 4 bolts: {}", bolts.line_total());
  println!("After a 25 percent discount: {}", Discount::Percent(25).apply(bolts.line_total()));
}

Expected output: All checks passed. Line total for 4 bolts: 120 After a 25 percent discount: 90

Once it passes, try two variations and predict each before running:

  1. Swallow a variant with a catch-all. In Discount::apply, replace the Percent arm with _ => total. Predict which check fails before running. Compilation succeeds — the _ arm makes the match exhaustive, so there is no error about the missing variant — and the Discount::Percent(25) check then fails with left: 120, right: 90, because percentage discounts are now silently doing nothing. (You do get a field 0 is never read warning about Percent's payload, which is a hint but not the diagnosis; it fires because nothing reads that number any more, not because the variant is unhandled.) This is the debug block's bug reintroduced by your own hand, which is the fastest way to feel why the explicit arms are worth the typing.
  2. Take the receiver by value. Change line_total's signature to fn line_total(self) -> u32. Predict what the compiler says before running. It refuses to compile: error[E0382]: use of moved value: bolts, with the note Item::line_total takes ownership of the receiver `self` . The first call consumes bolts and drops it, so the log line at the bottom has nothing left to measure. One ampersand decides whether a method is a question you can ask twice.

Key Takeaways

  • Structs group related data with named fields
  • Use impl blocks to add methods to structs
  • Enums define types with multiple variants
  • Enum variants can hold different types of data
  • Option<T> replaces null with Some(T) or None
  • match exhaustively handles all enum variants
  • if let provides a simpler syntax for single-variant matching

Structs and enums are foundational to writing idiomatic Rust code!

Next Steps

With structs and enums in your toolkit, you're ready for pattern matching — Rust's powerful way to destructure those types and branch on their variants. match and if let turn the enums you just defined into expressive, exhaustive control flow.

Next lesson

Pattern Matching

Master Rust's powerful pattern matching with match expressions, if let, and destructuring

25 min