3. Control Flow
Rust’s control structures will look familiar for about five minutes. The shapes
are the ones you know, with parentheses dropped and braces required. What is new
is match, which does the work that switch, if-chains, destructuring, and
type tests do in other languages, and does it with the compiler checking that you
have not forgotten a case.
3.1 if and else
#![allow(unused)]
fn main() {
let n = 7;
if n % 2 == 0 {
println!("even");
} else if n % 3 == 0 {
println!("divisible by three");
} else {
println!("neither");
}
}
The condition must be a bool. No parentheses, no truthiness, and the braces
are required even for a single statement, which retires the dangling-else
problem and C’s misindented-second-line bug.
Because if is an expression it can produce a value, and both arms must then
agree on the type:
#![allow(unused)]
fn main() {
let n = 7;
let parity = if n % 2 == 0 { "even" } else { "odd" };
}
Leave off the else and the value is (), so an if without an else can
only be used as a statement. That is the rule; the compiler’s phrasing of it,
when you trip, is that the arms have incompatible types.
3.2 match
match compares a value against patterns in order and takes the first that
fits. FizzBuzz, written against the pair of remainders:
fn main() {
for n in 1..=15 {
let line = match (n % 3, n % 5) {
(0, 0) => "FizzBuzz".to_string(),
(0, _) => "Fizz".to_string(),
(_, 0) => "Buzz".to_string(),
_ => n.to_string(),
};
println!("{line}");
}
}
The patterns here are tuples, _ matches anything, and the arms are expressions
separated by commas. Every arm has to produce the same type, which is why the
numbers are converted with .to_string(): three arms offer text, so the fourth
must too.
The property that makes match more than a switch is exhaustiveness. Leave a
case out and the program does not compile:
error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered
--> non_exhaustive.rs:3:22
|
3 | let name = match n {
| ^ patterns `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered
For an integer that is mostly a nuisance and you write _ at the end. For the
types in Chapter 6, where a value is one of a fixed set of alternatives, it is
the feature the language is built around: add a new alternative and the compiler
lists every place in the program that now has a hole in it.
Patterns
A pattern can be a literal, a range, an alternation, a binding, or a structure taken apart. All of these are arms:
0 => ..., // a literal
1..=9 => ..., // a range, both ends included
'a' | 'e' | 'i' | 'o' | 'u' => // alternation
n => ..., // binds anything to the name n
_ => ..., // matches anything, binds nothing
(x, 0) => ..., // a tuple, second element zero
[first, .., last] => ..., // a slice, first and last elements
An arm can carry a guard, an if that runs after the pattern fits:
n if n < 0 => "negative",
and a binding can be attached to a pattern with @ so you can both test a value
and keep it:
n @ 1..=9 => println!("single digit {n}"),
Here is all of it at once. The program reads words and says what each looks like:
// Read words from standard input and say what each one looks like.
use std::io::{self, Read};
fn classify(word: &str) -> String {
match word {
"" => "empty".to_string(),
"yes" | "no" => format!("an answer: {word}"),
w if w.parse::<i64>().is_ok() => format!("the number {w}"),
w if w.parse::<f64>().is_ok() => format!("the float {w}"),
w if w.chars().all(|c| c.is_ascii_alphabetic()) => match w.len() {
1 => "a single letter".to_string(),
2..=4 => "a short word".to_string(),
_ => "a long word".to_string(),
},
w => format!("something else ({} chars)", w.chars().count()),
}
}
fn main() {
let mut text = String::new();
io::stdin().read_to_string(&mut text).unwrap();
for word in text.split_whitespace() {
println!("{word}: {}", classify(word));
}
}
$ printf 'no 42 3.5 x cat elephant hello! 7up\n' | cargo run -q -p ch03 --bin classify
no: an answer: no
42: the number 42
3.5: the float 3.5
x: a single letter
cat: a short word
elephant: a long word
hello!: something else (6 chars)
7up: something else (3 chars)
Order matters: "no" reaches the second arm before the guards get a chance, and
7up fails the numeric parse and the all-alphabetic test and falls to the end.
Arms can nest, as the word-length match does. format! builds a String the
way println! builds a line of output.
w if ... binds the word and then tests it. The closure |c| c.is_ascii_alphabetic()
is Chapter 4’s business; read it for now as a small unnamed function passed to
all.
3.3 Loops
Three loops, and they divide up cleanly. while runs while a condition holds.
for walks a sequence. loop runs until something breaks out of it, and is the
one C spells for (;;).
// How many steps does each starting value take to reach 1?
fn steps(mut n: u64) -> u32 {
let mut count = 0;
while n != 1 {
n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
count += 1;
}
count
}
fn main() {
let mut worst = (0, 0);
for start in 1..1000 {
let s = steps(start);
if s > worst.1 {
worst = (start, s);
}
}
println!("longest under 1000: {} takes {} steps", worst.0, worst.1);
}
longest under 1000: 871 takes 178 steps
Note mut n: u64 in the parameter list. The function gets its own copy of the
number and is free to modify it, and the caller’s value is untouched. That is
the same call-by-value C has, said out loud.
break leaves the innermost loop and continue starts its next round. A loop
can hand a value out through its break, the tidy way to write a search that
has to run at least once:
#![allow(unused)]
fn main() {
let mut n = 1;
let first_big_square = loop {
if n * n > 1000 {
break n * n;
}
n += 1;
};
println!("{first_big_square}");
}
When loops nest and you need to leave more than one, label them:
#![allow(unused)]
fn main() {
let grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
let target = 5;
let mut found = None;
'rows: for (r, row) in grid.iter().enumerate() {
for (c, value) in row.iter().enumerate() {
if *value == target {
found = Some((r, c));
break 'rows;
}
}
}
println!("{found:?}");
}
A label is a name with a leading apostrophe, and break 'rows leaves the loop
wearing it. This replaces the goto that C reaches for here; Rust has no
goto.
.enumerate() pairs each item with its position, and .iter() walks a
collection by reference. Chapter 7 covers both properly.
3.4 if let, let else, and while let
Much of the time a match has one arm you care about and a _ => {} for
everything else. if let is the short form:
#![allow(unused)]
fn main() {
let maybe_port: Option<u16> = "8080".parse().ok();
if let Some(port) = maybe_port {
println!("listening on {port}");
} else {
println!("no port given");
}
}
let ... else is for the other common shape, where a failure means this function
has nothing more to do:
#![allow(unused)]
fn main() {
fn show(text: &str) {
let Ok(n) = text.parse::<i32>() else {
println!("{text} is not a number");
return;
};
println!("{}", n * 2);
}
}
The pattern binds for the rest of the enclosing block, and the else branch has
to leave: return, break, continue, or a panic. It is how you keep the
happy path flat instead of drifting rightwards with every check.
while let loops as long as the pattern keeps fitting, and is how you drain
something:
#![allow(unused)]
fn main() {
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{top}");
}
}
pop hands back Some(value) until the vector is empty and then None, which
ends the loop. Chapter 1’s byte-copying program used the same construction on a
Result.
3.5 What Is Missing
There is no goto, no do ... while (write loop with the test at the
bottom), no fallthrough between match arms, no ternary operator, and no
exceptions. The last of those is the significant one: a Rust function that can
fail says so in its return type, and Chapter 8 is about what you do with that.
Exercise 3-1. Write the FizzBuzz program again using if/else if. Which
version would you rather modify when a third divisor is added?
Exercise 3-2. Print the Collatz sequence itself for a starting value read
from the command line, rather than its length. std::env::args().nth(1) gives
you the first argument as an Option<String>.
Exercise 3-3. Write a function that takes a char and returns its category
as a &'static str: vowel, consonant, digit, white space, or other. Use one
match.
Exercise 3-4. The classify program tests is_ok() on a parse and throws
the result away, then parses again. Rewrite it so each word is parsed at most
once.
Exercise 3-5. Write a loop that finds the smallest positive integer n for
which n * n has at least six digits, using loop and break with a value.