Structs and Enums
Custom data types in two shapes: structs group related fields, enums describe values that can be one of several variants. Together with pattern matching, they cover most of the type-design work you do in Rust.
For Python developers: structs are like dataclasses. Enums go further than Python's Enum. Each variant can hold its own data.
Structs
Defining Structs
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user = User {
email: String::from("user@example.com"),
username: String::from("someuser"),
active: true,
sign_in_count: 1,
};
println!("User: {}", user.username);
}
Python comparison:
# Using dataclass (Python 3.7+)
from dataclasses import dataclass
@dataclass
class User:
username: str
email: str
sign_in_count: int
active: bool
user = User(
email="user@example.com",
username="someuser",
active=True,
sign_in_count=1
)
# Or traditional class
class User:
def __init__(self, username, email, sign_in_count, active):
self.username = username
self.email = email
self.sign_in_count = sign_in_count
self.active = active
Mutable Structs
The whole struct is either mutable or not. You can't mark individual fields.
fn main() {
let mut user = User {
email: String::from("user@example.com"),
username: String::from("someuser"),
active: true,
sign_in_count: 1,
};
user.email = String::from("new@example.com");
println!("Email: {}", user.email);
}
Field Init Shorthand
When parameter names match field names:
fn build_user(email: String, username: String) -> User {
User {
email, // Shorthand for email: email
username, // Shorthand for username: username
active: true,
sign_in_count: 1,
}
}
Struct Update Syntax
Create a new struct from an existing one:
fn main() {
let user1 = User {
email: String::from("user1@example.com"),
username: String::from("user1"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("user2@example.com"),
..user1 // Copy remaining fields from user1
};
// user1.email still valid (String was not moved)
// user1.username was moved to user2
println!("User2: {}", user2.email);
}
Warning: If the update syntax moves a non-Copy field, the original is partially moved.
Tuple Structs
Named tuples with no field names:
struct Color(i32, i32, i32);
struct Point(f64, f64, f64);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0, 0.0);
println!("Red: {}", black.0);
println!("X: {}", origin.0);
}
Useful when you want type safety but naming fields isn't needed.
Unit-Like Structs
Structs with no fields:
struct AlwaysEqual;
fn main() {
let _subject = AlwaysEqual;
}
Useful for implementing traits on a type with no data.
Deriving Traits for Structs
#[derive(Debug, Clone, PartialEq)]
struct Rectangle {
width: f64,
height: f64,
}
fn main() {
let rect = Rectangle { width: 30.0, height: 50.0 };
// Debug print
println!("{:?}", rect);
println!("{:#?}", rect); // Pretty print
// Clone
let rect2 = rect.clone();
// Compare
println!("Equal: {}", rect == rect2);
}
| Derive | Purpose |
|---|---|
Debug | Enable {:?} formatting |
Clone | Enable .clone() |
Copy | Enable implicit copies (requires Clone) |
PartialEq | Enable == and != |
Eq | Full equality (requires PartialEq) |
Hash | Enable use as HashMap key |
Default | Enable Default::default() |
Implementing Methods
#[derive(Debug)]
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// Associated function (constructor)
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
fn square(size: f64) -> Self {
Rectangle { width: size, height: size }
}
// Methods
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn is_square(&self) -> bool {
(self.width - self.height).abs() < f64::EPSILON
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
}
fn main() {
let mut rect = Rectangle::new(30.0, 50.0);
println!("Area: {}", rect.area());
println!("Perimeter: {}", rect.perimeter());
println!("Is square: {}", rect.is_square());
rect.scale(2.0);
println!("Scaled: {:?}", rect);
let sq = Rectangle::square(10.0);
println!("Can hold square: {}", rect.can_hold(&sq));
}
You can have multiple impl blocks for the same struct.
Enums
Defining Enums
enum Direction {
North,
South,
East,
West,
}
fn main() {
let dir = Direction::North;
match dir {
Direction::North => println!("Going north"),
Direction::South => println!("Going south"),
Direction::East => println!("Going east"),
Direction::West => println!("Going west"),
}
}
Enums with Data
Each variant can hold different types and amounts of data:
enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Named fields (like a struct)
Write(String), // Single String
ChangeColor(i32, i32, i32), // Three i32s
}
fn main() {
let msg = Message::Write(String::from("hello"));
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Text: {}", text),
Message::ChangeColor(r, g, b) => println!("Color: ({}, {}, {})", r, g, b),
}
}
Methods on Enums
impl Message {
fn call(&self) {
match self {
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!("Changing color to ({}, {}, {})", r, g, b);
}
}
}
}
fn main() {
let msg = Message::Move { x: 10, y: 20 };
msg.call();
}
The Option Enum
Rust has no null. Instead it uses Option<T>:
enum Option<T> {
Some(T),
None,
}
fn main() {
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
// You must handle both cases
match some_number {
Some(n) => println!("Got: {}", n),
None => println!("Got nothing"),
}
}
Option Methods
fn main() {
let x: Option<i32> = Some(42);
let y: Option<i32> = None;
// unwrap - panics if None
println!("{}", x.unwrap());
// unwrap_or - default value
println!("{}", y.unwrap_or(0));
// unwrap_or_else - lazy default
println!("{}", y.unwrap_or_else(|| 2 * 21));
// map - transform inner value
let doubled = x.map(|n| n * 2);
println!("{:?}", doubled); // Some(84)
// and_then - chain operations
let result = x
.map(|n| n + 1)
.and_then(|n| if n > 40 { Some(n) } else { None });
println!("{:?}", result); // Some(43)
// is_some / is_none
println!("x is some: {}", x.is_some());
println!("y is none: {}", y.is_none());
}
| Method | Behavior on None | Behavior on Some(v) |
|---|---|---|
unwrap() | Panics | Returns v |
unwrap_or(default) | Returns default | Returns v |
unwrap_or_else(f) | Returns f() | Returns v |
map(f) | Returns None | Returns Some(f(v)) |
and_then(f) | Returns None | Returns f(v) |
is_some() | false | true |
Pattern Matching in Depth
Destructuring Structs
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
// Destructure in let binding
let Point { x, y } = p;
println!("x: {}, y: {}", x, y);
// Destructure in match
match p {
Point { x: 0, y } => println!("On y-axis at {}", y),
Point { x, y: 0 } => println!("On x-axis at {}", x),
Point { x, y } => println!("At ({}, {})", x, y),
}
}
Destructuring Enums
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle { base: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
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 shapes = vec![
Shape::Circle(5.0),
Shape::Rectangle(4.0, 6.0),
Shape::Triangle { base: 3.0, height: 8.0 },
];
for shape in &shapes {
println!("Area: {:.2}", area(shape));
}
}
Nested Patterns
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
ChangeColor(Color),
Move { x: i32, y: i32 },
}
fn main() {
let msg = Message::ChangeColor(Color::Rgb(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("RGB: ({}, {}, {})", r, g, b);
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("HSV: ({}, {}, {})", h, s, v);
}
Message::Move { x, y } => {
println!("Move to ({}, {})", x, y);
}
}
}
Ignoring Values
fn main() {
let numbers = (1, 2, 3, 4, 5);
// Ignore specific fields with _
match numbers {
(first, _, third, _, fifth) => {
println!("{} {} {}", first, third, fifth);
}
}
// Ignore remaining with ..
match numbers {
(first, .., last) => {
println!("first: {}, last: {}", first, last);
}
}
// Ignore entire value with _
let _unused = 42;
}
Binding with @
Bind a value while also testing it:
fn main() {
let num = 5;
match num {
n @ 1..=5 => println!("Got {} (between 1 and 5)", n),
n @ 6..=10 => println!("Got {} (between 6 and 10)", n),
n => println!("Got {} (something else)", n),
}
}
Combining Structs and Enums
State Machine
#[derive(Debug)]
enum OrderStatus {
Pending,
Processing,
Shipped { tracking: String },
Delivered,
Cancelled { reason: String },
}
struct Order {
id: u32,
item: String,
quantity: u32,
status: OrderStatus,
}
impl Order {
fn new(id: u32, item: &str, quantity: u32) -> Self {
Order {
id,
item: item.to_string(),
quantity,
status: OrderStatus::Pending,
}
}
fn ship(&mut self, tracking: String) {
self.status = OrderStatus::Shipped { tracking };
}
fn cancel(&mut self, reason: &str) {
self.status = OrderStatus::Cancelled {
reason: reason.to_string(),
};
}
fn summary(&self) -> String {
let status_msg = match &self.status {
OrderStatus::Pending => "awaiting processing".to_string(),
OrderStatus::Processing => "being processed".to_string(),
OrderStatus::Shipped { tracking } => format!("shipped ({})", tracking),
OrderStatus::Delivered => "delivered".to_string(),
OrderStatus::Cancelled { reason } => format!("cancelled: {}", reason),
};
format!(
"Order #{}: {}x {} - {}",
self.id, self.quantity, self.item, status_msg
)
}
}
fn main() {
let mut order = Order::new(1, "Rust Book", 2);
println!("{}", order.summary());
order.ship("TRACK123".to_string());
println!("{}", order.summary());
}
Enum as Configuration
enum LogLevel {
Error,
Warning,
Info,
Debug,
}
struct Logger {
level: LogLevel,
}
impl Logger {
fn log(&self, level: &LogLevel, message: &str) {
let level_num = |l: &LogLevel| match l {
LogLevel::Error => 0,
LogLevel::Warning => 1,
LogLevel::Info => 2,
LogLevel::Debug => 3,
};
if level_num(level) <= level_num(&self.level) {
let prefix = match level {
LogLevel::Error => "ERROR",
LogLevel::Warning => "WARN ",
LogLevel::Info => "INFO ",
LogLevel::Debug => "DEBUG",
};
println!("[{}] {}", prefix, message);
}
}
}
fn main() {
let logger = Logger { level: LogLevel::Info };
logger.log(&LogLevel::Error, "Something broke");
logger.log(&LogLevel::Info, "Starting up");
logger.log(&LogLevel::Debug, "This won't print");
}
Practice Exercises
Exercise 1: Student Records
Create a Student struct with name, grades (Vec<f64>), and a method to calculate the average.
struct Student {
name: String,
grades: Vec<f64>,
}
impl Student {
fn average(&self) -> f64 {
// Your code
}
}
Exercise 2: Expression Evaluator
Create an enum for mathematical expressions and evaluate them:
enum Expr {
Number(f64),
Add(Box<Expr>, Box<Expr>),
Multiply(Box<Expr>, Box<Expr>),
}
fn eval(expr: &Expr) -> f64 {
// Your code
}
Exercise 3: Traffic Light
Create an enum for traffic light states and a method that returns the duration for each state.
Common Mistakes
1. Forgetting to Handle All Enum Variants
match direction {
Direction::North => println!("north"),
Direction::South => println!("south"),
} // ERROR: non-exhaustive
Fix: Handle all variants or use _ catch-all.
2. Moving Fields in Update Syntax
let user2 = User { email: String::from("new@example.com"), ..user1 };
println!("{}", user1.username); // ERROR: username was moved
Fix: Clone the source or only access non-moved fields.
3. Comparing Enums Without PartialEq
let d = Direction::North;
if d == Direction::North { } // ERROR without #[derive(PartialEq)]
Fix: Add #[derive(PartialEq)] to the enum.
Key Takeaways
- Structs group related data with named fields
- Tuple structs are unnamed-field structs for simple wrappers
- Enums represent types that can be one of several variants
- Each enum variant can hold different types and amounts of data
Option<T>replaces null with compile-time safety- Pattern matching with
matchmust be exhaustive - Use
@bindings to capture values while testing patterns - Structs and enums together model state machines and domain logic
Next Steps
Continue to 07-collections.md to learn the standard collections: Vec, String, and HashMap.
Quick Reference
// Struct
struct Point { x: f64, y: f64 }
let p = Point { x: 1.0, y: 2.0 };
// Tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
// Enum
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
}
// Pattern matching
match shape {
Shape::Circle(r) => r * r * PI,
Shape::Rect { w, h } => w * h,
}
// Option
let x: Option<i32> = Some(5);
let y: Option<i32> = None;
// if let
if let Some(val) = x {
println!("{}", val);
}