You've already seen if and for in passing.
Now we'll slow down and look at them on purpose, along with the other two loop forms (while and loop) and the keywords that control them (break and continue).
if / else / else ifThe usage is unsurprising:
if x > 0 {
println!("positive");
} else if x < 0 {
println!("negative");
} else {
println!("zero");
}
You might notice two things:
The condition is a bool.
No truthy strings, no zero-as-false, no parentheses required around the condition.
The whole if is itself an expression.
You can use it on the right-hand side of a let binding:
let label = if x >= 0 { "non-negative" } else { "negative" };
Both branches have to produce the same type, and there's no trailing semicolon on the value-producing expression in each branch (just like a function body, see the functions chapter).
for loopsA for loop can walk through a range of numbers, the elements of an array, or the items in a collection.
Rust supports all of these through iterators, but you do not need to understand iterators yet to use the loop.
For example:
for i in 0..5 { // 0, 1, 2, 3, 4
println!("{i}");
}
for word in ["hi", "rust"] {
println!("{word}");
}
0..5 is a range: a value that produces the integers from 0 up to (but not including) 5.
The inclusive form is 0..=5, which also yields 5.
Ranges also work as patterns in match, such as 1..=10 => ....
For larger collections, you'll usually iterate over a Vec, a slice, a HashMap, or the result of s.chars().
For now, "anything you can put on the right of for x in ..." is enough.
while and loopwhile runs as long as a condition is true:
let mut n = 10;
while n > 0 {
println!("{n}");
n -= 1;
}
loop runs forever, until you break out of it.
It is useful when the exit condition is not a simple boolean check at the top, or when you only know whether to stop after doing some work:
let mut attempts = 0;
loop {
attempts += 1;
if try_connect() { break; }
if attempts > 10 { break; }
}
loop can also produce a value: pass an expression to break and the whole loop evaluates to it.
let answer = loop {
let guess = read_guess();
if guess == 42 { break guess; }
};
break and continueBoth keywords control the innermost loop:
break exits the loop immediately.continue skips the rest of the current iteration and starts the next one.for n in 0..10 {
if n % 2 == 1 { continue; } // skip odd
if n > 6 { break; } // stop at 8
println!("{n}"); // 0, 2, 4, 6
}
When you need to pick one:
for when you know what you're iterating over (a range, a slice, a map, the chars of a string).while when the exit condition is a simple "keep going while X is true".loop only when neither of the above fits, usually because the exit condition is in the middle of the body.If you are unsure, start with for when there is already a collection or range to walk through.
Ranges, slices, and collections all fit this syntax because each can produce an iterator.
Ferris the crab is a creature of simple needs.
Two things determine his mood on any given day: how hungry he is (on a 0..=10 scale) and how many naps he's managed to fit in.
Implement ferris_mood(hunger, naps) returning a &'static str, following these rules:
| Condition | Mood |
|---|---|
hunger >= 8 | "Hangry" |
hunger >= 5 and naps == 0 | "Grumpy" |
naps >= 3 | "Sleepy" |
| anything else | "Content" |
&'static str just means "a borrowed string slice that lives for the whole program".
String literals like "Hangry" are baked into your compiled binary, so the text is around for as long as the program is running.
The 'static lifetime is just the compiler's way of saying "this reference will never dangle."
If you've written C, it's the same intuition as a const char * pointing at a string literal.
For now, the only thing to take away is "string literals are always safe to return as &'static str."
Combining conditions. The "Grumpy" rule needs both parts to be true.
Rust spells this && (logical AND).
Its sibling || is logical OR.
Both short-circuit: if the left side already decides the answer, the right side isn't evaluated.
Order matters. An if/else if/else chain is checked top-to-bottom and stops at the first match.
If you put the naps check before the hunger check, a hungry crab who happens to have napped a lot will get classified as "Sleepy" instead of "Hangry".
The tests deliberately include cases (like ferris_mood(9, 5)) that only pass with the right ordering.
if/else if chain decides everything: top to bottom, first match wins.
Translate the rule table line by line and the order falls out for you."Grumpy" rule needs both conditions to be true.
Combine them with && (logical AND).
/// Ferris the crab has moods. Decide which one based on how
/// hungry he is (a `0..=10` scale) and how many naps he's had today.
///
/// The rules, in plain English:
///
/// - If Ferris is **very** hungry (8 or more), he's `"Hangry"`,
/// no matter how many naps he's had.
/// - Otherwise, if he's also a bit hungry (5 or more) **and** has
/// had no naps, he's `"Grumpy"`.
/// - Otherwise, if he's had three or more naps, he's `"Sleepy"`.
/// - Otherwise, he's `"Content"`.
///
/// One `if`/`else if`/`else` chain, returning a `&'static str`.
/// The order of the branches matters; the tests will catch you if
/// you get it wrong.
fn ferris_mood(hunger: u32, naps: u32) -> &'static str {
if hunger >= 8 {
"Hangry"
} else if hunger >= 5 && naps == 0 {
"Grumpy"
} else if naps >= 3 {
"Sleepy"
} else {
"Content"
}
}
#[test]
fn content_by_default() {
assert_eq!(ferris_mood(3, 1), "Content");
assert_eq!(ferris_mood(0, 2), "Content");
}
#[test]
fn grumpy_when_hungry_and_napless() {
assert_eq!(ferris_mood(5, 0), "Grumpy");
assert_eq!(ferris_mood(7, 0), "Grumpy");
}
#[test]
fn sleepy_after_too_many_naps() {
assert_eq!(ferris_mood(2, 3), "Sleepy");
assert_eq!(ferris_mood(0, 10), "Sleepy");
}
#[test]
fn hangry_overrides_everything() {
// Ferris is too hungry to care about anything else.
// If you check naps before hunger, these will fail.
assert_eq!(ferris_mood(8, 0), "Hangry");
assert_eq!(ferris_mood(9, 5), "Hangry");
assert_eq!(ferris_mood(10, 100), "Hangry");
}
n! is 1 * 2 * 3 * ... * n.
By convention, 0! == 1.
Build it up with a running accumulator and a for loop over the inclusive range 1..=n.
Start the accumulator at 1, then multiply each number into it as the loop goes along.
There is a nice side effect at the boundary.
When n is 0, the range 1..=n is empty, so the loop does not run and the initial 1 comes back unchanged.
That gives you 0! == 1 without a special case.
The accumulator pattern shows up everywhere once you start writing loops: begin with one value, update it for every item, then return what you ended up with.
The binding needs mut because Rust bindings are immutable unless you explicitly make them mutable.
let mut acc: u32 = 1; outside the loop, for i in 1..=n { ... } inside.
Return acc at the end.acc *= i;.
Both mut on the binding and *= for the compound assignment are needed.
/// Returns `n!` (n factorial). By convention, `factorial(0) == 1`.
///
/// Build it up with a `mut` accumulator and a `for` loop over the
/// inclusive range `1..=n`. For `n == 0` the loop body never runs,
/// so the initial value carries through unchanged.
fn factorial(n: u32) -> u32 {
let mut result = 1;
for i in 1..=n {
result *= i;
}
result
}
#[test]
fn test_factorial() {
assert_eq!(factorial(0), 1);
assert_eq!(factorial(1), 1);
assert_eq!(factorial(2), 2);
assert_eq!(factorial(5), 120);
assert_eq!(factorial(10), 3_628_800);
}
The parameter here is a &[i32], a slice: a borrowed view over a sequence of i32 values that live somewhere else.
For now, the only thing you need is that a for loop walks a slice one element at a time, handing you each number in turn.
A for loop gives you each number in turn.
Keep a counter and bump it whenever the number is even.
When the current number is odd, you can use continue to skip straight to the next one.
You can then increment the counter without nesting that line inside another if.
let mut count = 0u32; plus a for n in numbers loop.
The suffix 0u32 pins the integer type so you don't need a separate annotation.for n in numbers over a &[i32] yields &i32.
The % operator works through the reference, so n % 2 Just Works.
continue skips the rest of the current iteration.
/// Counts how many numbers in the slice are even.
///
/// A `for` loop over the slice plus a `mut` counter is a simple way to do this.
/// Use `continue` to skip the odd numbers if you like; it's not required, just
/// easier to read.
fn count_evens(numbers: &[i32]) -> u32 {
let mut count = 0;
for &number in numbers {
if number % 2 == 0 {
count += 1;
}
}
count
}
#[test]
fn test_count_evens() {
assert_eq!(count_evens(&[]), 0);
assert_eq!(count_evens(&[1, 3, 5]), 0);
assert_eq!(count_evens(&[2, 4, 6, 8]), 4);
assert_eq!(count_evens(&[1, 2, 3, 4, 5, 6]), 3);
assert_eq!(count_evens(&[0, -2, -3, 7]), 2);
}
How many digits does a number have?
0 has one digit; everything else is "divide by 10 and count how many times you can do it before hitting zero".
That's a natural while loop: keep going as long as the number is non-zero, dividing it down each step.
This is the inverse of a for loop (like the one you wrote for factorial).
With factorial, you knew up front how many times to loop.
Here, you don't: you have to keep dividing until the number runs out.
That's exactly what while is for.
n == 0 returning 1.
Otherwise, divide by 10 in a while loop and count the iterations.let mut n = n; so you can mutate it without changing the signature.
Loop while n > 0, dividing by 10 and bumping a counter.
/// Returns the number of decimal digits in `n`. `digit_count(0)` is `1`.
///
/// Use a `while` loop. The "divide by 10 until you hit zero" pattern is the
/// classic solution for this kind of "I don't know how many iterations up
/// front" problem.
fn digit_count(n: u32) -> u32 {
let mut count = 1;
let mut remaining = n;
while remaining >= 10 {
remaining /= 10;
count += 1;
}
count
}
#[test]
fn test_digit_count() {
assert_eq!(digit_count(0), 1);
// Boundary check: 10 has two digits, not one.
assert_eq!(digit_count(10), 2);
assert_eq!(digit_count(99), 2);
assert_eq!(digit_count(100), 3);
assert_eq!(digit_count(1_000_000), 7);
assert_eq!(digit_count(u32::MAX), 10);
}
You used each kind of control flow for a slightly different job.
The mood classifier chose one branch, the two for loops walked through values you already had, and the while loop kept going until there was nothing left to divide.
What we learned
if/elseis an expression, not just a statement. It can sit on the right oflet, be returned from a function, or appear anywhere a value is expected. Both branches must have the same type.- Conditions are
boolexpressions written without surrounding parentheses. Rust does not quietly treat an integer or a string as true or false.for x in iteris the type of loop to use when you already have something to walk through. Ranges (0..n,0..=n), slices, vectors, and most other collections all work here.while condruns as long as the condition is true. Reach for it when the iteration count depends on values computed inside the loop (like "divide until zero").loopruns forever until youbreak. It can also produce a value:let x = loop { ...; break value; };.breakexits the innermost loop;continueskips to the next iteration. Incount_evens,continuehandled the odd numbers first and kept the counter outside a nestedif.- An accumulator lets you compute one value from many inputs. You start with a value, update it once per loop iteration, and return it when the loop is done. Iterator methods such as
sum,count, andfoldcover many of the same jobs.