Skip to main content
  1. Posts/

Rust basics: structs and enums

·587 words·3 mins
Author
m58
0xm58.xyz
Table of Contents
Structs and enums are the core of Rust data modeling. Structs group fields together. Enums describe a value that can be one of several well-defined variants.

If you are coming from languages where data modeling is mostly “objects everywhere”, Rust feels different at first. It prefers explicit data shapes plus separate behavior.

Structs group related fields#

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

A struct is just a named collection of fields with fixed types.

Create one with a literal:

let user = User {
    id: 1,
    name: String::from("Ada"),
    active: true,
};

Access fields with dot syntax:

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

Mutable structs
#

If you want to modify fields later, the binding must be mutable:

let mut user = User {
    id: 1,
    name: String::from("Ada"),
    active: true,
};

user.active = false;

This is still ordinary Rust binding behavior. The struct itself is not special here.

Tuple structs and unit structs
#

Rust also supports shorter struct forms:

struct Color(u8, u8, u8);
struct Marker;
  • tuple structs are useful when fields have meaning but do not need names
  • unit structs are rare, but can mark a type with no data

For beginners, named-field structs are the most important form.

Enums model variants
#

Enums are where Rust gets much of its expressive power.

enum Status {
    Active,
    Disabled,
    PendingReview,
}

That says a Status value must be exactly one of those variants.

Variants can also carry data:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}

This is the point where enums become more powerful than the simple “named constants” version many people know from other languages.

Option and Result are enums too
#

Two of Rust’s most important standard-library types are enums:

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

This is why learning enums early pays off. They are not a side feature. They are central to ordinary Rust code.

Pattern matching with enums
#

Enums are usually consumed with match:

let msg = Message::Write(String::from("hi"));

match msg {
    Message::Quit => println!("quit"),
    Message::Move { x, y } => println!("move {x},{y}"),
    Message::Write(text) => println!("write {text}"),
}

Pattern matching lets you destructure the data inside each variant.

Behavior lives in impl blocks
#

Methods and associated functions are defined with impl:

impl User {
    fn new(id: u64, name: String) -> Self {
        Self {
            id,
            name,
            active: true,
        }
    }

    fn is_active(&self) -> bool {
        self.active
    }
}
  • Self refers to the type being implemented
  • new is an associated function
  • is_active is a method because it takes &self

This is Rust’s usual style: data and behavior are connected, but without forcing everything into class inheritance.

A practical design example
#

Imagine a deployment status:

enum DeployState {
    Pending,
    Running { started_at: u64 },
    Failed { reason: String },
    Complete,
}

This is more precise than a struct with several optional fields:

// weaker design
struct DeployStatus {
    running: bool,
    failed: bool,
    reason: Option<String>,
}

The enum version prevents invalid combinations like “running and failed at the same time.”

Summary
#

  • struct is for one value with a fixed set of fields.
  • enum is for one value that can take several distinct shapes.
  • impl adds constructors, methods, and associated functions.
  • Enums become especially powerful when paired with match.

If you are not sure whether something should be a struct or an enum, ask a simple question:

  • “Does this thing always have the same fields?” Use a struct.
  • “Can this thing be one of several distinct cases?” Use an enum.

Related

Rust basics: control flow

·665 words·4 mins
Rust control flow is more than branching and loops. The important part is that many control-flow forms are expressions, which means they produce values. That design shows up everywhere in day-to-day Rust. It makes code concise, but it also forces you to be explicit about the shapes of the values you return.

Rust basics: functions and types

·598 words·3 mins
Rust functions are explicit at the boundary and flexible inside the body. You spell out parameter and return types, then let inference do most of the local work. That balance is one of Rust’s better design choices. Signatures stay readable, but the code inside them does not turn into annotation noise.

Rust basics: let, mut, and shadowing

·481 words·3 mins
Rust variables are really bindings. Once that clicks, let, mut, and shadowing stop feeling like syntax trivia and start feeling like design choices. Rust pushes you toward immutability by default. That is not just style. It reduces accidental state changes and makes data flow easier to follow.