Skip to content
Educora
Intermediate20 min5 / 10

Structs, methods, enums and match

Create your own types: structs with fields, methods in an `impl` block, enums with variants, handling them with `match`, and `Option` instead of `null`.

Check yourself
In this lesson you will learn
  • Declare a struct, create an instance and print it with #[derive(Debug)]
  • Write &self and &mut self methods and associated functions in an impl block
  • Create an enum with data-carrying variants and handle it with an exhaustive match
  • Safely handle a value that may be missing with Option, if let and unwrap_or

In an electronic class register, every student has a name, a grade and an average score — keeping these three values in separate variables is awkward; they belong together. A traffic light, on the other hand, is in exactly one state at a time: red, yellow or green. Rust has structs (struct) for the first case and enums (enum) for the second.

Structs

A struct is a type with named fields. The type name is written in PascalCase and the fields in snake_case. When you create an instance, every field must get a value, and a field is accessed with a dot: aysel.score. To change a field, the whole instance must be mut — Rust does not let you mark a single field as mut.

Rust
#[derive(Debug)]
struct Student {
    name: String,
    grade: u8,
    score: f64,
}

fn main() {
    let mut aysel = Student {
        name: String::from("Aysel"),
        grade: 9,
        score: 91.5,
    };
    aysel.score += 2.0;
    println!("{} is in grade {}", aysel.name, aysel.grade);
    println!("{:?}", aysel);
    println!("{:#?}", aysel);
}
Expected output
Aysel is in grade 9
Student { name: "Aysel", grade: 9, score: 93.5 }
Student {
    name: "Aysel",
    grade: 9,
    score: 93.5,
}
The #[derive(Debug)] attribute teaches the compiler to print the struct with {:?}. {:#?} pretty-prints it with every field on its own line.

Methods and impl

Functions that belong to a type are written in an impl block. A function whose first parameter is self is a method and is called with a dot: room.area(). A function without a self parameter is an associated function and is called with ::: Rectangle::new(4, 3) — String::from is exactly this kind. Inside impl, Self stands for the type's own name.

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

impl Rectangle {
    fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }

    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn scale(&mut self, factor: u32) {
        self.width *= factor;
        self.height *= factor;
    }
}

fn main() {
    let mut room = Rectangle::new(4, 3);
    println!("Area: {}", room.area());
    room.scale(2);
    println!("Scaled: {}x{}, area {}", room.width, room.height, room.area());
}
Expected output
Area: 12
Scaled: 8x6, area 48
First parameterMeaningCall
&selfonly reads the instanceroom.area()
&mut selfchanges the instanceroom.scale(2)
selftakes ownership; the instance cannot be used afterwardstext.into_bytes()
no selfassociated function, often a constructorRectangle::new(4, 3)

Enums and match

An enum says that a value is one of several variants. In Rust, variants can carry their own data: nothing, named fields or values in parentheses. Enums are handled with match: each arm has the form pattern => expression. A match must be exhaustive — every possible variant must be covered, or the code will not compile.

Rust
enum Command {
    Quit,
    Move { x: i32, y: i32 },
    Say(String),
}

fn run(cmd: &Command) -> String {
    match cmd {
        Command::Quit => String::from("bye"),
        Command::Move { x, y } => format!("move to ({x}, {y})"),
        Command::Say(text) => format!("say: {text}"),
    }
}

fn main() {
    let commands = [
        Command::Say(String::from("hello")),
        Command::Move { x: 3, y: -1 },
        Command::Quit,
    ];
    for cmd in &commands {
        println!("{}", run(cmd));
    }
}
Expected output
say: hello
move to (3, -1)
bye
format! works like println! but returns the text as a String instead of printing it. In the match arms, x, y and text are bound to the data inside the variant.
Error E0004
fn day_type(day: u8) -> char {
    match day {
        1..=5 => 'W',
        6 | 7 => 'H',
    }
}
Exhaustive with `_`
fn day_type(day: u8) -> char {
    match day {
        1..=5 => 'W',
        6 | 7 => 'H',
        _ => '?',
    }
}
A u8 can be anything from 0 to 255, so the compiler says: non-exhaustive patterns: 0_u8 and 8_u8..=u8::MAX not covered. _ means “everything else”. Patterns may also use ranges (1..=5) and “or” (6 | 7).

Option instead of null

Rust has no null. When a value may or may not be present, the standard library's Option<T> enum is used: Some(value) or None. You cannot use the value inside it directly — the compiler forces you to handle the None case too. If only one variant interests you, write a short if let instead of a full match.

Rust
fn find_index(names: &[&str], target: &str) -> Option<usize> {
    for (i, name) in names.iter().enumerate() {
        if *name == target {
            return Some(i);
        }
    }
    None
}

fn main() {
    let names = ["Elvin", "Leyla", "Murad"];
    match find_index(&names, "Leyla") {
        Some(i) => println!("Leyla is at {i}"),
        None => println!("Leyla not found"),
    }
    if let Some(i) = find_index(&names, "Nigar") {
        println!("Nigar is at {i}");
    } else {
        println!("Nigar not found");
    }
    let murad = find_index(&names, "Murad");
    println!("{:?} {}", murad, murad.unwrap_or(0));
}
Expected output
Leyla is at 1
Nigar not found
Some(2) 2
unwrap_or(0) returns the value inside Some, or the given fallback when it is None. The standard library also has a ready-made method that does the same job: names.iter().position(|n| *n == target).

Key points

  • A struct groups named fields into one type; #[derive(Debug)] lets you print it with {:?}.
  • Methods live in an impl block: &self reads, &mut self changes; a function without self is called as Type::name().
  • enum variants can carry data; match must cover every variant, and _ catches the rest.
  • Rust has no null: a value that may be missing is an Option<T> — Some(x) or None.
  • unwrap() panics on None; match, if let and unwrap_or are safer.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
Which method signature allows changing the struct's fields?