Control Flow
Control flow determines which code runs and how many times. Rust has the usual suspects -- if, while, for -- but
with a twist: most control flow constructs are expressions that return values. This is one of the things that makes
Rust code concise and elegant.
if / else
The basics look like most C-family languages:
fn main() {
let temperature = 22;
if temperature > 30 {
println!("It's hot!");
} else if temperature > 15 {
println!("It's nice.");
} else {
println!("It's cold.");
}
}
Key differences from other languages:
- No parentheses around the condition --
if temperature > 30, notif (temperature > 30) - The condition must be a
bool. Rust does not treat integers, strings, or other types as truthy/falsy.
This will not compile:
fn main() {
let number = 1;
if number { // Error: expected bool, found integer
println!("truthy");
}
}
You must be explicit: if number != 0 { ... }.
if as an expression
In Rust, if is an expression -- it returns a value. This means you can use it on the right side of a let
binding:
fn main() {
let temperature = 22;
let description = if temperature > 30 {
"hot"
} else if temperature > 15 {
"nice"
} else {
"cold"
};
println!("It's {description}.");
}
This is like a ternary operator in other languages (condition ? a : b), but more readable and scalable.
Important: When using
ifas an expression, both branches must return the same type. This will not compile:let value = if true { 5 } else { "nope" };
// Error: `if` and `else` have incompatible types
Also note that the branches do not end with a semicolon -- the last expression in a block is the return value. We will explore this more in the next chapter on functions.
Loops
Rust has three kinds of loops: loop, while, and for.
loop -- infinite loops
loop runs forever until you explicitly break:
fn main() {
let mut count = 0;
loop {
count += 1;
if count == 5 {
break;
}
}
println!("Counted to {count}");
}
Returning values from loop
Since loop is an expression, you can return a value from it via break:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("Result: {result}"); // Result: 20
}
This is a pattern you will see when searching for a value -- loop until you find it, then break with the result.
while -- conditional loops
while loops run as long as a condition is true:
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("Liftoff!");
}
3!
2!
1!
Liftoff!
for -- iterating over collections
for is the most common loop in Rust. It iterates over anything that implements the Iterator trait:
fn main() {
let fruits = ["apple", "banana", "cherry"];
for fruit in fruits {
println!("I like {fruit}");
}
}
Ranges
Use ranges to generate sequences of numbers:
fn main() {
// 1 to 5 (inclusive start, exclusive end)
for i in 1..6 {
println!("{i}");
}
// 1 to 5 (inclusive on both ends)
for i in 1..=5 {
println!("{i}");
}
// Countdown with rev()
for i in (1..=5).rev() {
println!("{i}");
}
}
| Range syntax | Meaning | Example |
|---|---|---|
1..6 | 1, 2, 3, 4, 5 | Exclusive end |
1..=5 | 1, 2, 3, 4, 5 | Inclusive end |
(1..6).rev() | 5, 4, 3, 2, 1 | Reversed |
Iterating with index
Use enumerate() when you need both the index and the value:
fn main() {
let colors = ["red", "green", "blue"];
for (index, color) in colors.iter().enumerate() {
println!("{index}: {color}");
}
}
0: red
1: green
2: blue
for vs while -- which to use
Prefer for over while when iterating over a collection. for is:
- Safer -- no risk of off-by-one errors or out-of-bounds access
- Clearer -- the intent is obvious
- Faster -- the compiler can eliminate bounds checks
fn main() {
let numbers = [10, 20, 30, 40, 50];
// Prefer this (for)
for number in numbers {
println!("{number}");
}
// Over this (while with index)
let mut i = 0;
while i < numbers.len() {
println!("{}", numbers[i]);
i += 1;
}
}
break and continue
break exits a loop. continue skips to the next iteration:
fn main() {
for i in 1..=10 {
if i % 3 == 0 {
continue; // Skip multiples of 3
}
if i > 7 {
break; // Stop at 7
}
println!("{i}");
}
}
1
2
4
5
7
Loop labels
When you have nested loops, break and continue apply to the innermost loop by default. Use labels to target an
outer loop:
fn main() {
'outer: for x in 0..5 {
for y in 0..5 {
if x + y > 4 {
continue 'outer; // Skip to the next iteration of the outer loop
}
println!("({x}, {y})");
}
}
}
Labels start with a single quote and a name: 'outer, 'inner, 'search, etc.
match -- a first look
match is Rust's version of a switch statement, but much more powerful. We will cover it in depth in chapter 7 --
here is just enough to get started:
fn main() {
let number = 3;
match number {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("something else"),
}
}
Key points:
- Each arm has a pattern (
1,2,3) and a body (the code after=>) _is a wildcard that matches anything -- likedefaultin a switchmatchis exhaustive -- you must cover every possible value. The_arm typically handles "everything else"matchis an expression -- it returns a value
match as an expression
fn main() {
let number = 2;
let word = match number {
1 => "one",
2 => "two",
3 => "three",
_ => "other",
};
println!("{word}");
}
Matching ranges and multiple values
fn main() {
let score = 85;
let grade = match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F",
};
println!("Grade: {grade}");
}
fn main() {
let day = "Wednesday";
let kind = match day {
"Saturday" | "Sunday" => "weekend",
_ => "weekday",
};
println!("{day} is a {kind}");
}
match becomes incredibly powerful when combined with enums and pattern matching -- we will explore this in chapters 6
and 7.
Blocks as expressions
In Rust, a block ({ ... }) is an expression. The last expression in a block (without a semicolon) becomes the
block's value:
fn main() {
let y = {
let x = 3;
x + 1
};
println!("y is {y}"); // y is 4
}
Notice x + 1 has no semicolon. If you add one, the block returns () (the unit type) instead:
fn main() {
let y = {
let x = 3;
x + 1; // <- semicolon makes this a statement, block returns ()
};
println!("y is {y:?}"); // y is ()
}
This distinction between expressions (produce a value) and statements (do not produce a value) is fundamental
to Rust and carries into functions, if, match, and loops.
Tip: If the compiler tells you "expected
i32, found()", you probably have an extra semicolon on the last line of a block.
Putting it together
Here is a small program that combines everything from this chapter:
fn main() {
let target = 42;
let mut guess = 0;
let attempts = loop {
guess += 7;
if guess >= target {
break guess / 7;
}
};
let status = if guess == target { "exact" } else { "overshoot" };
let message = match attempts {
1..=3 => "Fast!",
4..=6 => "Not bad.",
_ => "Took a while.",
};
println!("Found {guess} in {attempts} attempts ({status}). {message}");
}
Summary
if/elsedoes not need parentheses and is an expression (returns a value)- Conditions must be
bool-- no truthy/falsy loopruns forever untilbreak; can return a value viabreak valuewhileloops on a conditionforiterates over ranges and collections -- prefer it overwhilefor iterationbreakandcontinuework with loop labels for nested loopsmatchis an exhaustive, expression-based pattern matcher (much more in chapter 7)- Blocks are expressions -- the last line (without semicolon) is the return value
Next up: Functions -- defining functions, parameters, return types, the difference between expressions and statements, and documentation comments.