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 |
|---|---|
|
Exit the innermost loop. With a label, break out of an
outer loop; with a value (in |
|
Skip to the next iteration. |
|
Exit the current function with a value ( |
|
Propagate |
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
};