Structs and Enums
Structs and enums are the primary ways to create custom data types in Rust. Together with pattern matching, they form the backbone of Rust's type system.
For Python developers: Structs are similar to dataclasses or simple classes. Enums are much more powerful than Python's Enum - they can hold 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 entire struct must be mutable - 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
In the next chapter, we'll explore Rust's built-in 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);
}