That design shows up everywhere in day-to-day Rust. It makes code concise, but it also forces you to be explicit about the shapes of the values you return.
if returns a value#
In Rust, if is an expression, not just a statement.
let n = 7;
let parity = if n % 2 == 0 { "even" } else { "odd" };This works because both branches return the same type: &str.
If the branch types do not match, the compiler complains:
let flag = true;
// invalid: one branch is i32, the other is &str
// let value = if flag { 10 } else { "ten" };
That restriction is useful. It keeps conditionals predictable and prevents “anything goes” branches.
Blocks are expressions too#
Any block can evaluate to its last expression:
let score = {
let raw = 40;
raw + 2
};score becomes 42 because the final line has no semicolon. Add a semicolon and the block returns (), the unit type.
match is Rust’s main branching tool#
match is exhaustive, which means every possible case must be handled.
let code = 404;
let message = match code {
200 => "ok",
404 => "not found",
500 => "server error",
_ => "unknown",
};That exhaustiveness matters most with enums:
enum State {
Draft,
Published,
Archived,
}
fn label(state: State) -> &'static str {
match state {
State::Draft => "draft",
State::Published => "published",
State::Archived => "archived",
}
}If you later add a new enum variant, the compiler points out every match that now needs updating.
if let for one interesting case#
Sometimes full match is more than you need. if let is the compact form when you only care about one pattern.
let maybe_name = Some(String::from("Ada"));
if let Some(name) = maybe_name {
println!("hello {name}");
}Use match when all variants matter. Use if let when one branch is the whole point.
loop for repeated work until break#
loop is an explicit infinite loop:
let mut retries = 0;
loop {
retries += 1;
if retries == 3 {
break;
}
}Like if, loop can also return a value:
let answer = loop {
break 42;
};That pattern is handy when a loop is searching for something and should yield the result directly.
while for condition-based looping#
Use while when you want to repeat work as long as a condition stays true.
let mut n = 3;
while n > 0 {
println!("{n}");
n -= 1;
}This is straightforward, but in idiomatic Rust you often end up preferring for over while when iterating through collections.
for is the default loop for iteration#
When you want to walk over a range or collection, for is usually the right tool.
for i in 0..3 {
println!("{i}");
}You can iterate arrays, vectors, and iterators the same way:
let names = ["Ada", "Grace", "Linus"];
for name in names {
println!("{name}");
}This avoids manual indexing and usually reads better than while.
A practical example#
Here is one small example that uses several of these forms together:
fn first_even(values: &[i32]) -> Option<i32> {
for value in values {
if value % 2 == 0 {
return Some(*value);
}
}
None
}This function:
- iterates with
for - branches with
if - returns early when it finds a match
- falls back to
Nonewhen nothing matches
That is typical Rust: small, explicit control flow with types that make outcomes clear.
Summary#
if,match, and blocks are expressions that can produce values.matchis exhaustive and becomes especially valuable with enums.if letis a compact way to handle one pattern.foris the usual choice for iteration, whileloopandwhilecover lower-level cases.
Once you get used to control flow producing values, Rust code starts to feel much more consistent.