Lesson 7: Structs and Enums: Basic Data Structures
Introduction
So far, you’ve worked with individual values and references. Now it’s time to group related data together and model real-world concepts in your programs. Rust gives you two powerful tools for this: structs and enums.
- A struct lets you bundle multiple named fields into one type — perfect for representing objects like a user, a point, or a book.
- An enum lets you define a type that can be one of several variants — perfect for representing states, options, or categories.
Declaring and Using Structs
A struct is defined with the struct keyword followed by named fields and their types.
struct Point {
x: f64,
y: f64,
}
fn main() {
let p = Point { x: 3.0, y: 4.5 };
println!("Point is at ({}, {})", p.x, p.y);
}- Fields are accessed using dot notation:
p.x,p.y. - To modify fields, the instance must be declared
mut.
fn main() {
let mut p = Point { x: 1.0, y: 2.0 };
p.x = 10.0;
println!("Updated x: {}", p.x);
}Structs with Functions: impl Blocks
You can associate functions and methods directly with a struct using an impl block. Methods that take &self as their first parameter can be called on an instance.
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// Associated function (constructor)
fn new(width: f64, height: f64) -> Rectangle {
Rectangle { width, height }
}
// Method: takes a reference to self
fn area(&self) -> f64 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let rect = Rectangle::new(5.0, 3.0);
println!("Area: {}", rect.area());
println!("Is square? {}", rect.is_square());
}Rectangle::new()is an associated function (called on the type, not an instance — similar to a constructor).rect.area()is a method (called on an instance via&self).
Declaring and Using Enums
An enum defines a type that can take on one of a fixed set of values, called variants.
enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
match heading {
Direction::North => println!("Heading North!"),
Direction::South => println!("Heading South!"),
Direction::East => println!("Heading East!"),
Direction::West => println!("Heading West!"),
}
}- Enum variants are namespaced:
Direction::North. - The
matchexpression is the standard way to handle enum values — and Rust requires you to cover all variants (exhaustive matching).
Enums with Data
Rust enums can carry data inside their variants, making them far more powerful than enums in many other languages.
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle(f64, f64, f64), // three sides
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
fn main() {
let c = Shape::Circle(5.0);
let r = Shape::Rectangle(4.0, 6.0);
println!("Circle area: {:.2}", area(&c));
println!("Rectangle area: {:.2}", area(&r));
}Pattern Matching with match
match is Rust’s powerful control flow construct for pattern matching. It works like a switch statement but is much more expressive.
fn describe_number(n: i32) {
match n {
0 => println!("Zero"),
1..=9 => println!("Single digit"),
10..=99 => println!("Two digits"),
_ => println!("Large number"), // _ is a catch-all
}
}
fn main() {
describe_number(0);
describe_number(7);
describe_number(42);
describe_number(1000);
}1..=9is an inclusive range pattern._is the wildcard — it matches anything not covered above.- Every
matcharm ends with=>followed by the result expression.
Interactive Task 1: Model a Student
Create a struct called Student with fields name (String), age (u32), and grade (f64). Add an impl block with:
- A
new()associated function to create a student. - A
is_passing()method that returnstrueif the grade is 50.0 or above.
struct Student {
// Your fields here
}
impl Student {
// Your new() and is_passing() here
}
fn main() {
let s = Student::new(String::from("Alice"), 20, 72.5);
println!("Passing: {}", s.is_passing());
}Interactive Task 2: Traffic Light Enum
Define an enum TrafficLight with variants Red, Yellow, and Green. Write a function action that takes a TrafficLight and prints what a driver should do.
enum TrafficLight {
// Your variants here
}
fn action(light: TrafficLight) {
// Use match here
}
fn main() {
action(TrafficLight::Red);
action(TrafficLight::Green);
action(TrafficLight::Yellow);
}Mini-Challenge
Define an enum Coin with variants Penny, Nickel, Dime, and Quarter. Write a function value_in_cents that returns the integer value of each coin using match. Then write a main that prints the value of each coin type.
- Call your function from
mainand print each result.
enum Coin {
// Your code here
}
fn value_in_cents(coin: Coin) -> u32 {
// Your match here
}
fn main() {
// Test all four coins
}Recap
- Structs group named fields into a single custom type.
- impl blocks let you attach methods and associated functions to structs.
- Enums define a type with a fixed set of variants — which can optionally carry data.
- match is Rust’s go-to for pattern matching: exhaustive, expressive, and safe.
- Together, structs and enums form the foundation of idiomatic Rust data modeling.
Questions about structs or enums? Drop a comment!