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
}
}Selfrefers to the type being implementednewis an associated functionis_activeis 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#
structis for one value with a fixed set of fields.enumis for one value that can take several distinct shapes.impladds 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.