Control flow#

Rust’s control surface is small and expression-based: if, match, loop, while, for, plus if let / while let / let-else for pattern-based binding. Every construct except while and for produces a value. There is no fallthrough, no goto; break accepts a label and a value.

For ? and Result, see Errors.

Conditional#

if / else if / else. Conditions must be bool; no truthiness.

        flowchart TD
    A([start]) --> B{x > 0?}
    B -->|true| C[positive]
    B -->|false| D{x == 0?}
    D -->|true| E[zero]
    D -->|false| F[negative]
    C --> Z([end])
    E --> Z
    F --> Z
    
if x > 0 {
    positive();
} else if x == 0 {
    zero();
} else {
    negative();
}

if is an expression; use it as the right-hand side of let.

let label = if n % 2 == 0 { "even" } else { "odd" };

Both arms must produce the same type; this is how the operator gets a “ternary” in Rust.

let abs = if x < 0 { -x } else { x };

Both arms must produce the same type.

if let and let-else#

if let binds the inner value when a pattern matches.

if let Some(port) = opts.port {
    listen(port);
}

while let Some(item) = stack.pop() {
    handle(item);
}

let-else is the operator’s preferred form for “happy path”; the else block must diverge.

let Some(host) = opts.host else {
    eprintln!("host required");
    return;
};
// host: String in scope

Match#

match is exhaustive pattern matching. Every variant of the matched type must be handled.

        flowchart TD
    A([match cmd]) --> B{"start | go"}
    B -->|yes| C[run]
    B -->|no| D{stop}
    D -->|yes| E[halt]
    D -->|no| F{status}
    F -->|yes| G[status]
    F -->|no| H[_ default]
    C --> Z([end])
    E --> Z
    G --> Z
    H --> Z
    
match cmd {
    "start" | "go"   => run(),
    "stop"           => halt(),
    "status"         => status(),
    _                => eprintln!("unknown"),
}

match s {
    Shape::Circle(r)       => std::f64::consts::PI * r * r,
    Shape::Rect { w, h }   => w * h,
    Shape::Empty           => 0.0,
}

Match arms can carry guards (if cond) and bind sub-patterns.

match (x, y) {
    (0, 0)               => "origin",
    (x, _) if x > 0      => "right half",
    _                    => "elsewhere",
};

match req {
    Request::Get { path } => fetch(path),
    Request::Post { body } if body.is_empty() => empty_post(),
    Request::Post { body } => store(body),
    _ => unreachable!(),
}

match is an expression; every arm must produce the same type.

Loops#

Three forms.

loop#

The infinite loop. Use break value to exit with a value.

let answer = loop {
    let r = poll();
    if r.is_ready() { break r; }
};

while#

        flowchart TD
    A([start]) --> B{cond?}
    B -->|true| C[body]
    C --> B
    B -->|false| D([end])
    

Test-first.

while queue.len() > 0 {
    handle(queue.pop_front().unwrap());
}

for#

        flowchart TD
    A([start]) --> B{more in iter?}
    B -->|yes| C[bind item]
    C --> D[body]
    D --> B
    B -->|no| E([end])
    

Iterates anything implementing IntoIterator. The operator’s default loop form.

Borrowed iteration over a slice.

for x in &xs {
    println!("{x}");
}

Index plus value via enumerate.

for (i, v) in xs.iter().enumerate() {
    println!("{i} {v}");
}

Map iteration; &map borrows so the map is not consumed.

for (k, v) in &map {
    println!("{k} -> {v}");
}

Characters of a string.

for ch in "operator".chars() {
    handle(ch);
}

Half-open integer range (0..10 skips the upper bound).

for i in 0..10 {
    handle(i);
}

Inclusive integer range (0..=10 includes the upper bound).

for i in 0..=10 {
    handle(i);
}

Jumps#

Keyword

Effect

break

Exit the innermost loop. With a label, break out of an outer loop; with a value (in loop), produce the loop’s value.

continue

Skip to the next iteration.

return

Exit the current function with a value (() if omitted).

?

Propagate Err / None to the caller. See Errors.

Labels.

'outer: for row in rows {
    for cell in row {
        if cell == target { break 'outer; }
    }
}

match vs if-else chains#

Reach for match whenever the dispatch covers all variants of an enum (and the compiler should catch a missing arm), or when the patterns are non-trivial (tuples, struct destructuring). For simple boolean tests, if reads better.

Diverging expressions#

Expressions that never produce a value have type ! and can fit anywhere a value is expected.

fn fatal(msg: &str) -> ! {
    eprintln!("{msg}");
    std::process::exit(1);
}

let v: u32 = match opts.port {
    Some(p) => p,
    None    => fatal("port required"),     // ! coerces to u32
};

References#

  • Syntax for the lexical surface if / match live in.

  • Types for bool, Option, Result, enums, and the ! type.

  • Errors for ?, Result, unwrap_or_else.

  • Functions for closures and the Iterator combinators that often replace explicit loops.

  • Rust Book, Control flow.