Skip to main content

Structs and Enums

Structuring Data with structs

structs (short for "structures") allow you to create custom data types by grouping related data together. They are similar to classes in object-oriented languages or objects/dictionaries in JavaScript/Python, but without built-in methods initially.

Defining a struct:

You define a struct using the struct keyword, followed by its name (typically PascalCase), and then curly braces containing its fields (each with a name and a type).

// Define a struct named 'User'
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

fn main() {
// Creating an instance of a struct
let user1 = User { // Order of fields doesn't matter
active: true,
username: String::from("alice123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};

// Accessing values using dot notation
println!("User 1 Name: {}", user1.username);
println!("User 1 Email: {}", user1.email);

// To modify a field, the struct instance itself must be mutable
let mut user2 = User {
active: false,
username: String::from("bob456"),
email: String::from("[email protected]"),
sign_in_count: 5,
};

user2.email = String::from("[email protected]"); // This is allowed
println!("User 2 New Email: {}", user2.email);

// You can also create new instances from existing ones using the struct update syntax
let user3 = User {
email: String::from("[email protected]"),
username: String::from("charlie789"),
..user1 // Fills remaining fields from user1 (active, sign_in_count)
};
println!("User 3 Name: {}, Active: {}", user3.username, user3.active);
}

Tuple Structs:

Tuple structs are like tuples but have a name. They are useful when you want to give a name to a tuple but don't need named fields.

struct Color(i32, i32, i32); // RGB values
struct Point(i32, i32, i32); // X, Y, Z coordinates

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

println!("Black RGB: ({}, {}, {})", black.0, black.1, black.2);
// Note: black.0 is the first element, black.1 the second, etc.
}

Unit-Like Structs:

These are useful when you need to implement a trait on some type but don't have any data that you want to store inside the type itself.

struct AlwaysEqual; // No fields

fn main() {
let subject = AlwaysEqual;
// You can use it as a type, but it holds no data.
}

Printing Structs with Debug Trait:

By default, println! cannot directly print structs in a readable format. You need to derive the Debug trait for your struct using #[derive(Debug)].

#[derive(Debug)] // Add this line above your struct definition
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

fn main() {
let user1 = User {
active: true,
username: String::from("alice123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};

println!("User 1: {:?}", user1); // Use {:?} for debug printing
println!("User 1 (pretty print): {:#?}", user1); // Use {:#?} for pretty printing
}

Modeling Data with enums

enums (enumerations) allow you to define a type by enumerating its possible variants. In Rust, enums are much more powerful than in many other languages; they are "sum types," meaning a value of an enum can be one of a set of defined possibilities.

Simple enums:

You've already seen Ordering in the guessing game, which is a simple enum.

enum TrafficLight {
Red,
Yellow,
Green,
}

fn main() {
let current_light = TrafficLight::Red;

match current_light { // Often used with 'match' for exhaustive handling
TrafficLight::Red => println!("Stop!"),
TrafficLight::Yellow => println!("Prepare to stop!"),
TrafficLight::Green => println!("Go!"),
}
}

enums with Associated Data:

This is where Rust's enums become extremely powerful. Each variant of an enum can hold its own specific data.

enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Anonymous struct-like data
Write(String), // Single String data
ChangeColor(i32, i32, i32), // Tuple-like data (RGB values)
}

fn main() {
let m1 = Message::Quit;
let m2 = Message::Move { x: 10, y: 20 };
let m3 = Message::Write(String::from("hello"));
let m4 = Message::ChangeColor(255, 0, 128);

// Using match to destructure and handle different enum variants
match m2 {
Message::Quit => println!("The Quit message has no data."),
Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
Message::Write(text) => println!("Write message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to R:{}, G:{}, B:{}", r, g, b),
}
}

The Option<T> Enum (Handling Absence of a Value):

Option<T> is a standard library enum that represents a value that might or might not be present. It's Rust's way of handling null/nil without null pointer exceptions.

enum Option<T> { // Conceptual definition
None, // Represents no value
Some(T), // Represents a value of type T
}

fn main() {
let some_number = Some(5); // A value is present
let no_number: Option<i32> = None; // No value is present

// You MUST use match (or other Option methods) to safely get the value out
match some_number {
Some(value) => println!("We have a number: {}", value),
None => println!("No number here."),
}

match no_number {
Some(value) => println!("We have a number: {}", value),
None => println!("No number here."),
}
}

The Result<T, E> Enum (Handling Recoverable Errors):

Result<T, E> is another fundamental enum for handling operations that can succeed or fail. You saw it with read_line() in Lesson 1.

enum Result<T, E> { // Conceptual definition
Ok(T), // Represents success, holding a value of type T
Err(E), // Represents failure, holding an error of type E
}

fn main() {
// Example: A function that might fail
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0.0 {
Err(String::from("Cannot divide by zero!"))
} else {
Ok(numerator / denominator)
}
}

let division_result = divide(10.0, 2.0);
match division_result {
Ok(value) => println!("Division successful: {}", value),
Err(error) => println!("Division failed: {}", error),
}

let division_by_zero = divide(10.0, 0.0);
match division_by_zero {
Ok(value) => println!("Division successful: {}", value),
Err(error) => println!("Division failed: {}", error),
}
}
  • Key takeaway for Option and Result: Rust forces you to explicitly handle the possibility of a value being absent or an operation failing, leading to more robust code.