Skip to content

03 - Decisions and Loops

What this session is

About an hour. You'll learn if (an expression in Rust - produces a value), three loop forms (loop, while, for), and the basics of match (Rust's powerful pattern-matching switch).

if is an expression

In most languages, if is a statement (does something, returns nothing). In Rust, if is an expression - it produces a value:

let age = 18;
let label = if age >= 18 { "adult" } else { "minor" };
println!("{label}");        // adult

Notice no semicolons inside the { } for the branches - the last expression is the value of the branch. Both branches must produce the same type. Then let label = ... captures that value.

The "old" form still works:

let age = 18;
if age >= 18 {
    println!("adult");
} else {
    println!("minor");
}

Braces are required even for one-line bodies. (Rust prevents the C-style "forgot the braces" bug.)

No parentheses around the condition required:

if age >= 18 { ... }       // idiomatic
if (age >= 18) { ... }     // works but feels noisy

Comparison operators

Op Meaning
== equal
!= not equal
< <= > >= numeric

Strings: == works for content comparison (&str == &str, String == &str). Unlike Java, you don't need .equals - Rust does the right thing.

Chaining: else if

let score = 75;
let grade = if score >= 90 { "A" }
            else if score >= 80 { "B" }
            else if score >= 70 { "C" }
            else { "F" };
println!("{grade}");

Combining: &&, ||, !

Same as C/Java/Go. Short-circuiting: && skips the right if left is false; || skips if left is true.

let age = 25;
let has_license = true;
if age >= 18 && has_license {
    println!("can drive");
}

Loop 1: loop (infinite)

let mut n = 0;
loop {
    n += 1;
    if n == 5 { break; }
    println!("{n}");
}

loop runs forever until break. Like while true in other languages but more explicit.

Cool trick: break can return a value:

let result = loop {
    // ... compute ...
    if done { break 42; }
};
println!("{result}");    // 42

Loop 2: while

let mut n = 10;
while n > 0 {
    println!("{n}");
    n -= 1;
}

Standard "while condition" loop.

Loop 3: for ... in

By far the most common. Iterates over anything that produces a sequence:

for i in 1..=5 {
    println!("{i}");
}
// prints 1, 2, 3, 4, 5

1..=5 is a range - 1 to 5 inclusive. 1..5 is exclusive (1, 2, 3, 4). You'll see both.

Iterate a collection:

let fruits = ["apple", "banana", "cherry"];
for fruit in fruits.iter() {
    println!("{fruit}");
}

// Or simpler:
for fruit in &fruits {
    println!("{fruit}");
}

The &fruits is a reference to the array (so for doesn't consume it). We'll explain references properly in page 06. For now, the pattern for x in &collection is idiomatic.

break and continue

for i in 1..=10 {
    if i == 5 { break; }
    if i % 2 == 0 { continue; }
    println!("{i}");
}
// prints 1, 3

You can also label loops to break out of nested ones:

'outer: for i in 0..5 {
    for j in 0..5 {
        if i + j == 6 { break 'outer; }
    }
}

Rare; nice when you need it.

match: pattern-matching switch

Rust's switch, called match. More powerful than C-style switch - matches on patterns, not just constants. Always exhaustive - the compiler verifies you covered every case.

let day = 2;
let name = match day {
    1 => "Mon",
    2 => "Tue",
    3 => "Wed",
    _ => "?",         // catch-all (underscore)
};
println!("{name}");    // Tue

The arms (pattern => value) are comma-separated. The whole match is an expression - its value is whatever arm matched.

Multiple values per arm:

let kind = match day {
    1 | 2 | 3 | 4 | 5 => "weekday",
    6 | 7 => "weekend",
    _ => "invalid",
};

Ranges:

let category = match age {
    0..=12 => "child",
    13..=17 => "teen",
    18..=64 => "adult",
    _ => "senior",
};

_ is the catch-all pattern - matches anything. Required when the patterns don't cover everything.

match gets much more powerful when matching on enums and structs (page 05).

Exercise

In your hello project, replace src/main.rs with FizzBuzz:

For each number 1 to 20: - Divisible by 3 → print Fizz. - Divisible by 5 → print Buzz. - Divisible by both → print FizzBuzz. - Otherwise → print the number.

Hint: check "both 3 and 5" first.

Then rewrite using match:

match (n % 3, n % 5) {
    (0, 0) => println!("FizzBuzz"),
    (0, _) => println!("Fizz"),
    (_, 0) => println!("Buzz"),
    _ => println!("{n}"),
}
That's matching on a tuple of (n%3, n%5). (0, 0) means "both zero." (0, _) means "first is zero, second is anything." Powerful pattern.

What you might wonder

"Why is if an expression?" Lets you write let x = if cond { a } else { b }; instead of needing a ternary operator or a separate temp variable. Cleaner; consistent with the "everything is an expression" trend.

"Why does match require _ or full coverage?" Exhaustiveness checking. If you switch on an enum and forget a variant, the compiler tells you exactly which one. This catches a huge class of "I forgot to handle that case" bugs.

"What about a do-while?" Use loop with a conditional break at the end:

loop {
    do_stuff();
    if !condition { break; }
}

Done

  • if/else as expressions.
  • Three loop forms.
  • break/continue (and label-break).
  • match with multi-value arms, ranges, tuple patterns.

Next: Functions →

Comments