1. rust
  2. /basics
  3. /control-flow

Control Flow

Control flow allows you to determine the order in which code executes. Rust provides several control flow constructs that are both powerful and safe, including conditional expressions, various types of loops, and pattern matching.

If Expressions

In Rust, if is an expression, not a statement, which means it returns a value:

fn main() {
    let number = 6;
    
    if number % 4 == 0 {
        println!("Number is divisible by 4");
    } else if number % 3 == 0 {
        println!("Number is divisible by 3");
    } else if number % 2 == 0 {
        println!("Number is divisible by 2");
    } else {
        println!("Number is not divisible by 4, 3, or 2");
    }
}

If as an Expression

Since if is an expression, you can use it to assign values:

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };
    
    println!("The value of number is: {}", number);
    
    // Both arms must return the same type
    let x = 5;
    let message = if x > 0 {
        "positive"
    } else if x < 0 {
        "negative"
    } else {
        "zero"
    };
    
    println!("Number is {}", message);
}

Type Consistency in If Expressions

fn main() {
    let condition = true;
    
    // This works - both arms return i32
    let number = if condition { 5 } else { 6 };
    
    // This would cause a compile error - mismatched types
    // let number = if condition { 5 } else { "six" };
}

Loops

Rust has three kinds of loops: loop, while, and for.

Infinite Loops with loop

The loop keyword creates an infinite loop:

fn main() {
    let mut counter = 0;
    
    loop {
        counter += 1;
        
        if counter == 3 {
            println!("three!");
            continue; // Skip the rest of this iteration
        }
        
        println!("counter: {}", counter);
        
        if counter == 5 {
            break; // Exit the loop
        }
    }
    
    println!("Loop ended");
}

Returning Values from Loops

You can return values from loops using break:

fn main() {
    let mut counter = 0;
    
    let result = loop {
        counter += 1;
        
        if counter == 10 {
            break counter * 2; // Return a value from the loop
        }
    };
    
    println!("The result is {}", result); // 20
}

Loop Labels

You can label loops to break out of nested loops:

fn main() {
    let mut count = 0;
    
    'counting_up: loop {
        println!("count = {}", count);
        let mut remaining = 10;
        
        loop {
            println!("remaining = {}", remaining);
            if remaining == 9 {
                break; // Break inner loop only
            }
            if count == 2 {
                break 'counting_up; // Break outer loop
            }
            remaining -= 1;
        }
        
        count += 1;
    }
    
    println!("End count = {}", count);
}

While Loops

while loops continue as long as a condition is true:

fn main() {
    let mut number = 3;
    
    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
    
    println!("LIFTOFF!!!");
    
    // Example: Finding an element
    let mut index = 0;
    let arr = [10, 20, 30, 40, 50];
    
    while index < arr.len() {
        if arr[index] == 30 {
            println!("Found 30 at index {}", index);
            break;
        }
        index += 1;
    }
}

For Loops

for loops iterate over collections:

fn main() {
    let arr = [10, 20, 30, 40, 50];
    
    // Iterate over array elements
    for element in arr {
        println!("The value is: {}", element);
    }
    
    // Iterate over a range
    for number in 1..4 {
        println!("{}!", number);
    }
    
    // Inclusive range
    for number in 1..=3 {
        println!("Inclusive: {}", number);
    }
    
    // Iterate with index
    for (index, value) in arr.iter().enumerate() {
        println!("Index {}: {}", index, value);
    }
}

Iterating Over Collections

fn main() {
    let names = vec!["Alice", "Bob", "Charlie"];
    
    // Iterate by reference (doesn't consume the vector)
    for name in &names {
        println!("Hello, {}!", name);
    }
    println!("Names still available: {:?}", names);
    
    // Iterate by value (consumes the vector)
    for name in names {
        println!("Goodbye, {}!", name);
    }
    // println!("{:?}", names); // Error: names has been moved
    
    // Iterate over mutable references
    let mut numbers = vec![1, 2, 3, 4, 5];
    for num in &mut numbers {
        *num *= 2;
    }
    println!("Doubled: {:?}", numbers);
}

Match Expressions

match is a powerful control flow construct that allows pattern matching:

fn main() {
    let number = 13;
    
    match number {
        1 => println!("One!"),
        2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
        13..=19 => println!("A teen"),
        _ => println!("Ain't special"),
    }
}

Match with Values

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {
    let coin = Coin::Penny;
    println!("Value: {} cents", value_in_cents(coin));
}

Destructuring with Match

#[derive(Debug)]
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn process_message(msg: Message) {
    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!("Move in the x direction {} and in the y direction {}", x, y);
        }
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => {
            println!("Change the color to red {}, green {}, and blue {}", r, g, b);
        }
    }
}

fn main() {
    let messages = vec![
        Message::Quit,
        Message::Move { x: 10, y: 30 },
        Message::Write("hello".to_string()),
        Message::ChangeColor(200, 255, 255),
    ];
    
    for msg in messages {
        process_message(msg);
    }
}

Matching Options

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
    
    println!("six: {:?}", six);
    println!("none: {:?}", none);
}

Guards in Match

fn main() {
    let num = Some(4);
    
    match num {
        Some(x) if x < 5 => println!("less than five: {}", x),
        Some(x) => println!("{}", x),
        None => (),
    }
    
    let x = 4;
    let y = false;
    
    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}

If Let

if let is a shorter way to match against one pattern:

fn main() {
    let config_max = Some(3u8);
    
    // Using match
    match config_max {
        Some(max) => println!("The maximum is configured to be {}", max),
        _ => (),
    }
    
    // Using if let (more concise)
    if let Some(max) = config_max {
        println!("The maximum is configured to be {}", max);
    }
    
    // if let with else
    let mut count = 0;
    if let Some(max) = config_max {
        count = max;
    } else {
        count = 10;
    }
    
    println!("Count: {}", count);
}

While Let

Similar to if let, but for loops:

fn main() {
    let mut stack = Vec::new();
    
    stack.push(1);
    stack.push(2);
    stack.push(3);
    
    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
    
    // Example with iterator
    let mut iter = vec![1, 2, 3, 4, 5].into_iter();
    
    while let Some(value) = iter.next() {
        if value > 3 {
            break;
        }
        println!("Value: {}", value);
    }
}

Pattern Matching in Function Parameters

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

Advanced Pattern Matching

Matching Literals and Variables

fn main() {
    let x = 1;
    
    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
    
    // Matching named variables
    let x = Some(5);
    let y = 10;
    
    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {:?}", y), // This y shadows the outer y
        _ => println!("Default case, x = {:?}", x),
    }
    
    println!("at the end: x = {:?}, y = {:?}", x, y);
}

Ranges in Patterns

fn main() {
    let x = 5;
    
    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }
    
    let x = 'c';
    
    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}

Destructuring Structs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };
    
    match p {
        Point { x, y: 0 } => println!("On the x axis at {}", x),
        Point { x: 0, y } => println!("On the y axis at {}", y),
        Point { x, y } => println!("On neither axis: ({}, {})", x, y),
    }
    
    // Destructuring with different names
    let Point { x: a, y: b } = p;
    println!("a: {}, b: {}", a, b);
}

Ignoring Values with _

fn main() {
    let numbers = (2, 4, 8, 16, 32);
    
    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {}, {}, {}", first, third, fifth);
        }
    }
    
    // Ignoring unused variables
    let _x = 5; // No warning about unused variable
    
    // Ignoring parts of a value
    let origin = Point { x: 0, y: 0 };
    
    match origin {
        Point { x, .. } => println!("x is {}", x),
    }
}

Control Flow Best Practices

1. Use if let for Simple Option/Result Handling

// Good: Simple and clear
if let Some(value) = optional_value {
    process(value);
}

// Avoid: Unnecessarily verbose for simple case
// match optional_value {
//     Some(value) => process(value),
//     None => (),
// }

2. Prefer for Loops Over Manual Indexing

let items = vec![1, 2, 3, 4, 5];

// Good: Iterator-based
for item in &items {
    println!("{}", item);
}

// Avoid: Manual indexing (error-prone)
// for i in 0..items.len() {
//     println!("{}", items[i]);
// }

3. Use Early Returns to Reduce Nesting

fn process_data(data: Option<&str>) -> Result<String, &'static str> {
    let data = match data {
        Some(d) => d,
        None => return Err("No data provided"),
    };
    
    if data.is_empty() {
        return Err("Data is empty");
    }
    
    Ok(format!("Processed: {}", data))
}

4. Leverage Pattern Matching for Complex Logic

enum State {
    Loading,
    Ready(String),
    Error(String),
}

fn handle_state(state: State) {
    match state {
        State::Loading => println!("Please wait..."),
        State::Ready(data) => println!("Data: {}", data),
        State::Error(msg) => eprintln!("Error: {}", msg),
    }
}

Rust's control flow constructs provide powerful and safe ways to direct program execution. The combination of expressions returning values, pattern matching, and iterator-based loops enables you to write clear, efficient, and maintainable code.