|
1 | 1 | --- |
2 | 2 | title: Control Flow & Functions |
3 | 3 | sidebar_position: 2 |
4 | | ---- |
| 4 | +--- |
| 5 | + |
| 6 | +----- |
| 7 | + |
| 8 | +# Lesson 2: Rust Fundamentals - Control Flow & Functions |
| 9 | + |
| 10 | + We'll learn how to make decisions based on conditions, repeat actions, and organize our code into reusable blocks. By the end, we'll even build a game\! |
| 11 | + |
| 12 | +----- |
| 13 | + |
| 14 | +### 2.1 Conditional Execution: `if` Statements (15 min) |
| 15 | + |
| 16 | +Just like in other programming languages, `if` statements in Rust let your program execute different code blocks based on whether a condition is true or false. |
| 17 | + |
| 18 | +#### **Basic `if`, `else if`, `else`:** |
| 19 | + |
| 20 | +You'll find this structure very familiar: |
| 21 | + |
| 22 | +```rust |
| 23 | +fn main() { |
| 24 | + let number = 7; |
| 25 | + |
| 26 | + if number < 5 { // If this condition is true |
| 27 | + println!("Condition was true: number is less than 5"); |
| 28 | + } else if number == 5 { // Otherwise, if this condition is true |
| 29 | + println!("Condition was true: number is exactly 5"); |
| 30 | + } else { // If none of the above conditions are true |
| 31 | + println!("Condition was false: number is greater than 5"); |
| 32 | + } |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | + * **Conditions Must Be `bool`:** In Rust, the condition inside an `if` statement *must* evaluate to a **boolean** (`true` or `false`). You can't just use a number like in some other languages. |
| 37 | + ```rust |
| 38 | + // This would be an ERROR: `if number` is not allowed in Rust |
| 39 | + // if number { |
| 40 | + // println!("Number was something!"); |
| 41 | + // } |
| 42 | + ``` |
| 43 | + |
| 44 | +#### **`if` as an Expression:** |
| 45 | + |
| 46 | +A cool feature in Rust is that `if` statements are **expressions**, meaning they can return a value. This is super handy for assigning values conditionally. |
| 47 | + |
| 48 | +```rust |
| 49 | +fn main() { |
| 50 | + let condition = true; |
| 51 | + let number = if condition { // 'if' expression returns a value |
| 52 | + 5 // This value is returned if 'condition' is true |
| 53 | + } else { |
| 54 | + 6 // This value is returned if 'condition' is false |
| 55 | + }; // Note the semicolon here, as it's a statement assigning a value |
| 56 | + |
| 57 | + println!("The value of number is: {}", number); // Output: The value of number is: 5 |
| 58 | + |
| 59 | + let message = if number > 5 { |
| 60 | + "Number is greater than 5" |
| 61 | + } else { |
| 62 | + "Number is 5 or less" |
| 63 | + }; // Both branches must return the SAME TYPE! |
| 64 | + |
| 65 | + println!("Message: {}", message); |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | + * **Important:** All branches of an `if` expression **must return the same type**. If one branch returns an integer and another returns a string, Rust won't compile because it can't determine the final type of the variable. |
| 70 | + |
| 71 | +----- |
| 72 | + |
| 73 | +### 2.2 Advanced Pattern Matching: The `match` Expression (10 min) |
| 74 | + |
| 75 | +The `match` expression is one of Rust's most powerful control flow constructs. It allows you to compare a value against a series of patterns and then execute code based on which pattern matches. It's often a more robust and readable alternative to long `if-else if` chains. |
| 76 | + |
| 77 | + * **Exhaustiveness:** A key feature of `match` is that it must be **exhaustive**. This means you have to cover *every possible value* that the data could take. If you don't, Rust's compiler will give you an error, which helps prevent bugs\! |
| 78 | + |
| 79 | +Let's look at a simple example with numbers: |
| 80 | + |
| 81 | +```rust |
| 82 | +fn main() { |
| 83 | + let number = 3; |
| 84 | + |
| 85 | + match number { // Match the 'number' against these patterns |
| 86 | + 1 => println!("One!"), // If number is 1, do this |
| 87 | + 2 => println!("Two!"), // If number is 2, do this |
| 88 | + 3 | 4 => println!("Three or Four!"), // If number is 3 OR 4, do this (multiple patterns) |
| 89 | + 5..=10 => println!("Between 5 and 10, inclusive!"), // If number is in this range |
| 90 | + _ => println!("Something else!"), // The underscore '_' is a catch-all pattern (like 'default' in switch) |
| 91 | + } |
| 92 | + |
| 93 | + let result = match number { // 'match' can also be an expression, returning a value |
| 94 | + 1 => "It's one", |
| 95 | + _ => "It's not one", // All branches must return the same type! |
| 96 | + }; |
| 97 | + println!("Result: {}", result); |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | + * **When to use `match` vs. `if`:** |
| 102 | + * Use `if` for simple true/false conditions or a few distinct branches. |
| 103 | + * Use `match` when you have many possible values or complex patterns to handle, especially when working with `enum`s (which we'll cover in Lesson 3) or `Result` types (which you saw in Lesson 1's I/O). |
| 104 | + |
| 105 | +----- |
| 106 | + |
| 107 | +### 2.3 Iterative Control Structures (30 min) |
| 108 | + |
| 109 | +Repeating actions is a fundamental part of programming. Rust provides several ways to create loops. |
| 110 | + |
| 111 | +#### **`loop` (Infinite Loop with `break`):** |
| 112 | + |
| 113 | +The `loop` keyword creates an infinite loop. You'll typically use `break` to exit it based on a condition, and `continue` to skip to the next iteration. |
| 114 | + |
| 115 | +```rust |
| 116 | +fn main() { |
| 117 | + let mut counter = 0; |
| 118 | + |
| 119 | + let result = loop { // 'loop' can also return a value! |
| 120 | + counter += 1; |
| 121 | + println!("Loop count: {}", counter); |
| 122 | + |
| 123 | + if counter == 10 { |
| 124 | + break counter * 2; // Break the loop and return this value |
| 125 | + } |
| 126 | + }; // Semicolon here, as it's an expression |
| 127 | + |
| 128 | + println!("Loop finished. Result: {}", result); // Output: Loop finished. Result: 20 |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +#### **`while` Loop:** |
| 133 | + |
| 134 | +A `while` loop executes a block of code repeatedly as long as a specified condition remains true. |
| 135 | + |
| 136 | +```rust |
| 137 | +fn main() { |
| 138 | + let mut number = 3; |
| 139 | + |
| 140 | + while number != 0 { |
| 141 | + println!("{}!", number); |
| 142 | + number -= 1; // Decrement number |
| 143 | + } |
| 144 | + println!("LIFTOFF!!!"); |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +#### **`for` Loop (Iterating over Collections):** |
| 149 | + |
| 150 | +The `for` loop is the most common loop in Rust. It's used to iterate over elements in a collection (like arrays, vectors, or ranges). This is often safer and more concise than `while` loops for iterating. |
| 151 | + |
| 152 | + * **Iterating over a Range:** |
| 153 | + |
| 154 | + ```rust |
| 155 | + fn main() { |
| 156 | + // Iterate from 1 up to (but not including) 5 |
| 157 | + for number in 1..5 { |
| 158 | + println!("Number in range: {}", number); |
| 159 | + } |
| 160 | + // Iterate from 1 up to AND including 5 |
| 161 | + for number in 1..=5 { |
| 162 | + println!("Number in range (inclusive): {}", number); |
| 163 | + } |
| 164 | + } |
| 165 | + ``` |
| 166 | + |
| 167 | + * **Iterating over an Array/Vector:** |
| 168 | + |
| 169 | + ```rust |
| 170 | + fn main() { |
| 171 | + let a = [10, 20, 30, 40, 50]; |
| 172 | + |
| 173 | + for element in a.iter() { // .iter() creates an iterator over the elements |
| 174 | + println!("The value is: {}", element); |
| 175 | + } |
| 176 | + |
| 177 | + // You can also iterate with an index if needed (less common in idiomatic Rust) |
| 178 | + for (index, element) in a.iter().enumerate() { |
| 179 | + println!("Element at index {}: {}", index, element); |
| 180 | + } |
| 181 | + } |
| 182 | + ``` |
| 183 | + |
| 184 | +----- |
| 185 | + |
| 186 | +### 2.4 Defining and Utilizing Functions (15 min) |
| 187 | + |
| 188 | +Functions are blocks of code that perform a specific task and can be reused. |
| 189 | + |
| 190 | +#### **Basic Function Syntax:** |
| 191 | + |
| 192 | + * Functions are declared using the `fn` keyword. |
| 193 | + * Parameters are type-annotated. |
| 194 | + * The return type is specified after an arrow `->`. |
| 195 | + * The last expression in a function (without a semicolon) is implicitly returned. You can also use the `return` keyword explicitly. |
| 196 | + |
| 197 | +<!-- end list --> |
| 198 | + |
| 199 | +```rust |
| 200 | +// A function that doesn't take parameters and doesn't return a value |
| 201 | +fn greet() { |
| 202 | + println!("Hello from the greet function!"); |
| 203 | +} |
| 204 | + |
| 205 | +// A function that takes parameters and returns a value |
| 206 | +fn add_numbers(x: i32, y: i32) -> i32 { // Takes two i32s, returns an i32 |
| 207 | + x + y // This is an expression, implicitly returned |
| 208 | +} |
| 209 | + |
| 210 | +// A function with an explicit return |
| 211 | +fn subtract_numbers(a: i32, b: i32) -> i32 { |
| 212 | + return a - b; // Explicit return |
| 213 | +} |
| 214 | + |
| 215 | +fn main() { |
| 216 | + greet(); // Call the greet function |
| 217 | + |
| 218 | + let sum = add_numbers(5, 7); // Call add_numbers and store the result |
| 219 | + println!("The sum is: {}", sum); // Output: The sum is: 12 |
| 220 | + |
| 221 | + let difference = subtract_numbers(10, 3); |
| 222 | + println!("The difference is: {}", difference); // Output: The difference is: 7 |
| 223 | +} |
| 224 | +``` |
| 225 | + |
| 226 | +----- |
| 227 | + |
| 228 | +### 2.5 Practical Application: Developing a Console-Based Guessing Game (20 min) |
| 229 | + |
| 230 | +Let's put everything we've learned so far into practice by building a simple "Guess the Number" game in the console\! |
| 231 | + |
| 232 | +**Game Logic:** |
| 233 | + |
| 234 | +1. Generate a random secret number. |
| 235 | +2. Prompt the user to guess. |
| 236 | +3. Read the user's input. |
| 237 | +4. Compare the guess to the secret number. |
| 238 | +5. Tell the user if they guessed too high, too low, or correctly. |
| 239 | +6. Keep looping until the user guesses correctly. |
| 240 | + |
| 241 | +**New Concepts/Tools:** |
| 242 | + |
| 243 | + * **`rand` crate:** We'll need a library to generate random numbers. Add `rand = "0.8.5"` (or a recent version) to your `Cargo.toml` under `[dependencies]`. |
| 244 | + * **`use rand::Rng;`:** To bring the random number generator trait into scope. |
| 245 | + * **`parse()` method:** To convert the user's input string to a number. This also returns a `Result`, so we'll handle it. |
| 246 | + |
| 247 | +**Steps to Build:** |
| 248 | + |
| 249 | +1. **Create a new Cargo project:** `cargo new guessing_game` |
| 250 | +2. **Add `rand` dependency:** Open `Cargo.toml` and add `rand = "0.8.5"` under `[dependencies]`. |
| 251 | +3. **Open `src/main.rs`** and replace its content with the following: |
| 252 | + |
| 253 | +<!-- end list --> |
| 254 | + |
| 255 | +```rust |
| 256 | +// src/main.rs |
| 257 | +use std::io; // For input/output operations |
| 258 | +use rand::Rng; // For generating random numbers |
| 259 | +use std::cmp::Ordering; // For comparing numbers (used with match) |
| 260 | + |
| 261 | +fn main() { |
| 262 | + println!("Guess the number!"); |
| 263 | + |
| 264 | + // Generate a random number between 1 and 100 (inclusive) |
| 265 | + // thread_rng() gives us a random number generator local to the current thread. |
| 266 | + // gen_range(1..=100) generates a number in the specified range. |
| 267 | + let secret_number = rand::thread_rng().gen_range(1..=100); |
| 268 | + |
| 269 | + // For debugging, you can uncomment this line: |
| 270 | + // println!("The secret number is: {}", secret_number); |
| 271 | + |
| 272 | + loop { // Start an infinite loop for the game |
| 273 | + println!("Please input your guess:"); |
| 274 | + |
| 275 | + let mut guess = String::new(); // Create a mutable string to store user input |
| 276 | + |
| 277 | + // Read the user's guess from the console |
| 278 | + io::stdin() |
| 279 | + .read_line(&mut guess) |
| 280 | + .expect("Failed to read line"); // Handle potential errors |
| 281 | + |
| 282 | + // Convert the guess from String to a number (u32). |
| 283 | + // .trim() removes any whitespace (like the newline character) |
| 284 | + // .parse() attempts to convert the string to a number. It returns a Result. |
| 285 | + // We use a 'match' expression to handle the Result: |
| 286 | + // - If Ok, we get the number. |
| 287 | + // - If Err, it means the input wasn't a valid number, so we print an error |
| 288 | + // and 'continue' to the next loop iteration (ask for guess again). |
| 289 | + let guess: u32 = match guess.trim().parse() { |
| 290 | + Ok(num) => num, // If parsing was successful, use the number |
| 291 | + Err(_) => { // If parsing failed (e.g., user typed text) |
| 292 | + println!("Please type a number!"); |
| 293 | + continue; // Skip to the next iteration of the loop |
| 294 | + } |
| 295 | + }; |
| 296 | + |
| 297 | + println!("You guessed: {}", guess); |
| 298 | + |
| 299 | + // Compare the guess to the secret number using 'match' and 'Ordering' enum |
| 300 | + // Ordering is an enum with variants Less, Greater, and Equal. |
| 301 | + match guess.cmp(&secret_number) { |
| 302 | + Ordering::Less => println!("Too small!"), // If guess is less than secret |
| 303 | + Ordering::Greater => println!("Too big!"), // If guess is greater than secret |
| 304 | + Ordering::Equal => { // If guess is equal to secret |
| 305 | + println!("You win!"); |
| 306 | + break; // Exit the loop (and the game) |
| 307 | + } |
| 308 | + } |
| 309 | + } |
| 310 | +} |
| 311 | +``` |
| 312 | + |
| 313 | +**Run your game:** |
| 314 | +In your terminal, navigate into the `guessing_game` folder and run: |
| 315 | +`cargo run` |
| 316 | + |
| 317 | +Now, play the game\! Try typing text instead of numbers to see the error handling. |
| 318 | + |
| 319 | +----- |
| 320 | + |
| 321 | +**End of Lesson 2.** You've now mastered Rust's core control flow, functions, and even built a complete interactive game\! This is a huge step in your Rust journey. Next, we'll tackle Rust's most unique and powerful concept: Ownership. |
0 commit comments