Control Flow, Functions and Pattern Matching
Almost everything in Rust is an expression, including if and match, so
control flow produces values rather than assigning to them.
fn main() {
let scores = [88, 42, 95, 61];
for score in scores {
println!("{score}: {}", grade(score));
}
for (i, score) in scores.iter().enumerate() {
if *score == 42 {
continue;
}
if *score > 90 {
println!(" stopping at index {i}");
break;
}
}
let mut countdown = 3;
while countdown > 0 {
println!("t-minus {countdown}");
countdown -= 1;
}
// `loop` is the infinite one, and it can return a value with break.
let mut n = 0;
let doubled = loop {
n += 1;
if n > 2 {
break n * 2;
}
};
println!("loop produced {doubled}");
// if is an expression, so this is Rust's ternary.
let label = if doubled > 5 { "big" } else { "small" };
println!("{label}");
}
fn grade(score: i32) -> &'static str {
if score >= 90 {
"excellent"
} else if score >= 60 {
"pass" // no semicolon: this is the value of the block
} else {
"resit"
}
}
A block's last expression, written without a semicolon, is its value. Adding a
semicolon turns it into a statement and the block produces () - which is the
single most common beginner error, and the compiler explains it well.
match is exhaustive, and that is the point
fn main() {
for code in [200, 302, 404, 500] {
println!("{code} -> {}", describe(code));
}
for n in [-1, 0, 5, 500] {
println!("{n} -> {}", categorise(n));
}
}
fn describe(code: u16) -> &'static str {
match code {
200 => "ok",
301 | 302 => "redirected", // several patterns, one arm
404 => "not found",
500..=599 => "server error", // an inclusive range
_ => "something else", // the catch-all
}
}
fn categorise(n: i32) -> String {
match n {
n if n < 0 => format!("{n} is negative"), // a guard
0 => "zero".to_string(),
1..=9 => "small".to_string(),
10..=99 => "medium".to_string(),
_ => "large".to_string(),
}
}
match must cover every possible value. Remove the _ arm from describe and
it will not compile - which is a feature, because it means adding a variant to
an enum tells you every place that needs updating.
Destructuring
struct Point {
x: i32,
y: i32,
}
enum Shape {
Circle { radius: f64 },
Rect { width: f64, height: f64 },
Unit,
}
fn main() {
let (a, b) = (1, "two");
println!("{a} {b}");
let point = Point { x: 3, y: 4 };
let Point { x, y } = point; // destructure a struct
println!("{x},{y}");
for shape in [
Shape::Circle { radius: 1.0 },
Shape::Rect { width: 2.0, height: 3.0 },
Shape::Unit,
] {
println!("{}", area(&shape));
}
// if let: one pattern, when a full match would be noise.
let maybe: Option<i32> = Some(7);
if let Some(n) = maybe {
println!("got {n}");
}
// let else: bind or leave, with no nesting.
let Some(value) = maybe else {
println!("nothing");
return;
};
println!("value is {value}");
// while let: keep going until the pattern stops matching.
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
print!("{top} ");
}
println!();
}
fn area(shape: &Shape) -> String {
match shape {
Shape::Circle { radius } => format!("circle: {:.2}", std::f64::consts::PI * radius * radius),
Shape::Rect { width, height } => format!("rect: {:.2}", width * height),
Shape::Unit => "unit: 1.00".to_string(),
}
}
Functions, and the expression at the end
fn main() {
println!("{}", add(2, 3));
println!("{:?}", min_max(&[5, 2, 9, 1]));
println!("{}", apply_twice(|n| n * 2, 3));
let mut counter = make_counter();
println!("{} {} {}", counter(), counter(), counter());
// A closure that borrows, and one that takes ownership with `move`.
let name = String::from("Ada");
let borrowing = || println!("borrowed {name}");
borrowing();
println!("{name} is still usable");
let owned = String::from("Grace");
let taking = move || println!("owned {owned}");
taking();
// println!("{owned}"); // moved into the closure
}
fn add(a: i32, b: i32) -> i32 {
a + b // no `return`, no semicolon
}
fn min_max(values: &[i32]) -> (i32, i32) {
let mut lo = values[0];
let mut hi = values[0];
for &v in values {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
(lo, hi)
}
fn apply_twice<F: Fn(i32) -> i32>(f: F, start: i32) -> i32 {
f(f(start))
}
fn make_counter() -> impl FnMut() -> i32 {
let mut count = 0;
move || {
count += 1;
count
}
}
The three closure traits say how the closure uses what it captures: Fn reads,
FnMut mutates, FnOnce consumes. You rarely write them by hand - the compiler
infers which one a closure is - but they appear in signatures constantly.
What to take away
- Almost everything is an expression; a block's last expression, without a semicolon, is its value.
matchis exhaustive, so adding an enum variant finds every place to update.- Guards (
n if n < 0), ranges (1..=9) and|alternatives compose in patterns. if let,let elseandwhile lethandle the single-pattern cases without nesting.- Closures capture by reference by default;
movetakes ownership. Fn,FnMutandFnOncedescribe how a closure uses what it captured.