|
| 1 | +--- |
| 2 | +title: Structs and Enums |
| 3 | +--- |
| 4 | + |
| 5 | +### Structuring Data with `struct`s |
| 6 | + |
| 7 | +`struct`s (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. |
| 8 | + |
| 9 | +#### **Defining a `struct`:** |
| 10 | + |
| 11 | +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). |
| 12 | + |
| 13 | +```rust |
| 14 | +// Define a struct named 'User' |
| 15 | +struct User { |
| 16 | + active: bool, |
| 17 | + username: String, |
| 18 | + email: String, |
| 19 | + sign_in_count: u64, |
| 20 | +} |
| 21 | + |
| 22 | +fn main() { |
| 23 | + // Creating an instance of a struct |
| 24 | + let user1 = User { // Order of fields doesn't matter |
| 25 | + active: true, |
| 26 | + username: String::from("alice123"), |
| 27 | + email: String::from("alice@example.com"), |
| 28 | + sign_in_count: 1, |
| 29 | + }; |
| 30 | + |
| 31 | + // Accessing values using dot notation |
| 32 | + println!("User 1 Name: {}", user1.username); |
| 33 | + println!("User 1 Email: {}", user1.email); |
| 34 | + |
| 35 | + // To modify a field, the struct instance itself must be mutable |
| 36 | + let mut user2 = User { |
| 37 | + active: false, |
| 38 | + username: String::from("bob456"), |
| 39 | + email: String::from("bob@example.com"), |
| 40 | + sign_in_count: 5, |
| 41 | + }; |
| 42 | + |
| 43 | + user2.email = String::from("new_bob@example.com"); // This is allowed |
| 44 | + println!("User 2 New Email: {}", user2.email); |
| 45 | + |
| 46 | + // You can also create new instances from existing ones using the struct update syntax |
| 47 | + let user3 = User { |
| 48 | + email: String::from("charlie@example.com"), |
| 49 | + username: String::from("charlie789"), |
| 50 | + ..user1 // Fills remaining fields from user1 (active, sign_in_count) |
| 51 | + }; |
| 52 | + println!("User 3 Name: {}, Active: {}", user3.username, user3.active); |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +#### **Tuple Structs:** |
| 57 | + |
| 58 | +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. |
| 59 | + |
| 60 | +```rust |
| 61 | +struct Color(i32, i32, i32); // RGB values |
| 62 | +struct Point(i32, i32, i32); // X, Y, Z coordinates |
| 63 | + |
| 64 | +fn main() { |
| 65 | + let black = Color(0, 0, 0); |
| 66 | + let origin = Point(0, 0, 0); |
| 67 | + |
| 68 | + println!("Black RGB: ({}, {}, {})", black.0, black.1, black.2); |
| 69 | + // Note: black.0 is the first element, black.1 the second, etc. |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +#### **Unit-Like Structs:** |
| 74 | + |
| 75 | +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. |
| 76 | + |
| 77 | +```rust |
| 78 | +struct AlwaysEqual; // No fields |
| 79 | + |
| 80 | +fn main() { |
| 81 | + let subject = AlwaysEqual; |
| 82 | + // You can use it as a type, but it holds no data. |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +#### **Printing Structs with `Debug` Trait:** |
| 87 | + |
| 88 | +By default, `println!` cannot directly print structs in a readable format. You need to derive the `Debug` trait for your struct using `#[derive(Debug)]`. |
| 89 | + |
| 90 | +```rust |
| 91 | +#[derive(Debug)] // Add this line above your struct definition |
| 92 | +struct User { |
| 93 | + active: bool, |
| 94 | + username: String, |
| 95 | + email: String, |
| 96 | + sign_in_count: u64, |
| 97 | +} |
| 98 | + |
| 99 | +fn main() { |
| 100 | + let user1 = User { |
| 101 | + active: true, |
| 102 | + username: String::from("alice123"), |
| 103 | + email: String::from("alice@example.com"), |
| 104 | + sign_in_count: 1, |
| 105 | + }; |
| 106 | + |
| 107 | + println!("User 1: {:?}", user1); // Use {:?} for debug printing |
| 108 | + println!("User 1 (pretty print): {:#?}", user1); // Use {:#?} for pretty printing |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +### Modeling Data with `enum`s |
| 115 | + |
| 116 | +`enum`s (enumerations) allow you to define a type by enumerating its possible variants. In Rust, `enum`s 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. |
| 117 | + |
| 118 | +#### **Simple `enum`s:** |
| 119 | + |
| 120 | +You've already seen `Ordering` in the guessing game, which is a simple enum. |
| 121 | + |
| 122 | +```rust |
| 123 | +enum TrafficLight { |
| 124 | + Red, |
| 125 | + Yellow, |
| 126 | + Green, |
| 127 | +} |
| 128 | + |
| 129 | +fn main() { |
| 130 | + let current_light = TrafficLight::Red; |
| 131 | + |
| 132 | + match current_light { // Often used with 'match' for exhaustive handling |
| 133 | + TrafficLight::Red => println!("Stop!"), |
| 134 | + TrafficLight::Yellow => println!("Prepare to stop!"), |
| 135 | + TrafficLight::Green => println!("Go!"), |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | + |
| 140 | +#### **`enum`s with Associated Data:** |
| 141 | + |
| 142 | +This is where Rust's enums become extremely powerful. Each variant of an enum can hold its own specific data. |
| 143 | + |
| 144 | +```rust |
| 145 | +enum Message { |
| 146 | + Quit, // No data |
| 147 | + Move { x: i32, y: i32 }, // Anonymous struct-like data |
| 148 | + Write(String), // Single String data |
| 149 | + ChangeColor(i32, i32, i32), // Tuple-like data (RGB values) |
| 150 | +} |
| 151 | + |
| 152 | +fn main() { |
| 153 | + let m1 = Message::Quit; |
| 154 | + let m2 = Message::Move { x: 10, y: 20 }; |
| 155 | + let m3 = Message::Write(String::from("hello")); |
| 156 | + let m4 = Message::ChangeColor(255, 0, 128); |
| 157 | + |
| 158 | + // Using match to destructure and handle different enum variants |
| 159 | + match m2 { |
| 160 | + Message::Quit => println!("The Quit message has no data."), |
| 161 | + Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y), |
| 162 | + Message::Write(text) => println!("Write message: {}", text), |
| 163 | + Message::ChangeColor(r, g, b) => println!("Change color to R:{}, G:{}, B:{}", r, g, b), |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +#### **The `Option<T>` Enum (Handling Absence of a Value):** |
| 169 | + |
| 170 | +`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. |
| 171 | + |
| 172 | +```rust |
| 173 | +enum Option<T> { // Conceptual definition |
| 174 | + None, // Represents no value |
| 175 | + Some(T), // Represents a value of type T |
| 176 | +} |
| 177 | + |
| 178 | +fn main() { |
| 179 | + let some_number = Some(5); // A value is present |
| 180 | + let no_number: Option<i32> = None; // No value is present |
| 181 | + |
| 182 | + // You MUST use match (or other Option methods) to safely get the value out |
| 183 | + match some_number { |
| 184 | + Some(value) => println!("We have a number: {}", value), |
| 185 | + None => println!("No number here."), |
| 186 | + } |
| 187 | + |
| 188 | + match no_number { |
| 189 | + Some(value) => println!("We have a number: {}", value), |
| 190 | + None => println!("No number here."), |
| 191 | + } |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +#### **The `Result<T, E>` Enum (Handling Recoverable Errors):** |
| 196 | + |
| 197 | +`Result<T, E>` is another fundamental enum for handling operations that can succeed or fail. You saw it with `read_line()` in Lesson 1. |
| 198 | + |
| 199 | +```rust |
| 200 | +enum Result<T, E> { // Conceptual definition |
| 201 | + Ok(T), // Represents success, holding a value of type T |
| 202 | + Err(E), // Represents failure, holding an error of type E |
| 203 | +} |
| 204 | + |
| 205 | +fn main() { |
| 206 | + // Example: A function that might fail |
| 207 | + fn divide(numerator: f64, denominator: f64) -> Result<f64, String> { |
| 208 | + if denominator == 0.0 { |
| 209 | + Err(String::from("Cannot divide by zero!")) |
| 210 | + } else { |
| 211 | + Ok(numerator / denominator) |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + let division_result = divide(10.0, 2.0); |
| 216 | + match division_result { |
| 217 | + Ok(value) => println!("Division successful: {}", value), |
| 218 | + Err(error) => println!("Division failed: {}", error), |
| 219 | + } |
| 220 | + |
| 221 | + let division_by_zero = divide(10.0, 0.0); |
| 222 | + match division_by_zero { |
| 223 | + Ok(value) => println!("Division successful: {}", value), |
| 224 | + Err(error) => println!("Division failed: {}", error), |
| 225 | + } |
| 226 | +} |
| 227 | +``` |
| 228 | + |
| 229 | +- **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. |
| 230 | + |
| 231 | +--- |
0 commit comments