Control Flow
Programs aren't very useful if they just follow a linear path forward. To do useful things, we need conditional statements and loops! Rust can do this, of course.
This section ostensibly is about control flow. It's also about Rust as an expression-oriented language; this is one of the things that makes Rust really ergonomic, and is also one of the things most confusing if you haven't used a language like it before! Basically, almost everything in Rust returns a value! This is super helpful, and different/confusing if you're not used to it.
Also, a note: we'll use println quite a bit through this section in a few forms.
The fmt docs have thorough explanations of all the different ways you can format output with println.
Please refer there if you need any help understanding the println!
usage on this page.
Blocks
Blocks aren't really control flow per se, but since every control flow mechanism takes a block, here we are.
A block is just a section of code surrounded by curly braces:
#![allow(unused)] fn main() { { println!("I'm in a block!"); } }
There are two main notable things about blocks:
- Blocks delimit a scope, so any variables you declare inside a block are out of scope outside of it; this is super handy for temporary variables.
- Blocks return a value, which is precisely the value of the last expression in the block. If the last expression of the block ends with a semicolon, then it's a statement, which returns
()
, the unit value, so it functionally has no return value.
Let's see a couple of examples of this and how you'd use it.
#![allow(unused)] fn main() { let msg = "Hello, world"; { // we're in the pirate block now let msg = "Ahoy, matey!"; println!("{msg}"); } println!("{msg}"); }
Exercise: What do you think this will print when you run it? Try to figure it out before executing it to test your understanding and intuition!
Since blocks declare a new scope, the msg
variable inside the block shadows the msg
variable on the outside, and does not change its value.
Note: Examples here are going to be rather contrived because we're avoiding things like structs and methods for now, trying to stick to (largely) just the syntax introduced so far.
Okay, so we saw scope delimiting. Here's an example of a block returning a value:
#![allow(unused)] fn main() { let parrots = 5; let shipmates = 10; let legs_on_ship = { let parrot_legs = 2 * parrots; let human_legs = 2 * shipmates; parrot_legs + human_legs }; }
Here we end up with legs_on_ship
having the value 30
, and the temporary variables (parrot_legs
and human_legs
) are freed when the block ends.
What's the real-world use for this usage of blocks? A common use is to do what we saw in the previous example and use it to constrain the scope of temporary variables. Doing this inside a block allows you to set things up in a readable manner without polluting the outer scope. Another common use is to release resources; you can lock a mutex at the beginning of a block, and when the block ends it will be released (like with lock:
in Python).
Ifs
Blocks are neat, but if-else is how we really get stuff done.
It works how you'd expect from other languages, with a few notable things:
- Parentheses around the condition are optional (and usually considered un-idiomatic)
- if-else-expressions return a value! Just like with blocks, this is the value of the last expression in each branch1.
- The branches must be surrounded by curly braces (yes, even if it's just a single statement2)
Here's a simple one:
#![allow(unused)] fn main() { if plunder > 5 { println!("A good haul"); } else { println!("Just jetsam"); } }
And then using it to get back a value. Since Rust doesn't have the conditional/ternary operator, this is the way you set a value conditionally:
#![allow(unused)] fn main() { let is_crew_member = true; let greeting = if is_crew_member { "Ahoy!!! Welcome aboard!" } else { "Yarrrrr get off me ship" }; println!("{}", greeting); }
This block will print a different greeting depending on whether or not you're a crew member.
Loops
The final basic control flow constructs for getting stuff done are loops. There are a few kinds of loops, so we'll just whirlwind through them.
There are three kinds of loops:
- loop-expressions
- while-expressions
- for-expressions
After those, we'll go through an extra: returning values from a loop.
loop expressions
These are the basic infinite loops. You have to exit out of them manually with the break
keyword. Otherwise, they keep going forever.
#![allow(unused)] fn main() { // Don't do this, it will run forever loop { println!("Wheeeee"); } }
To have the loop terminate, you have to break. Usually you want that on a condition:
#![allow(unused)] fn main() { let mut count = 0; loop { count += 1; println!("iteration {count}"); if count >= 10 { break; } } }
while expressions
These work like loop-expressions with the added bonus of having a condition to halt, so you don't have to manually break out of them. Otherwise, they're the same: you give a body and it gets run each time until the condition evaluates to false
.
#![allow(unused)] fn main() { let mut count = 0; while count < 10 { count += 1; println!("iteration {count}"); } }
This should behave the same as the loop above. Note that since we run while the condition is true, we don't have to do an awkward inversion of it to decide when to break. It's much more convenient.
Of course, you can also use break
statements in these.
There just usually isn't as much need to.
for expressions
The for loop in Rust is one of the ways you iterate over an iterable, which is typically a collection of things or a range ("from 0 to 10"). We'll see an example of both. There will be some syntax you're not familiar with, but we'll come back to the collections later on.
#![allow(unused)] fn main() { for count in 0..10 { println!("iteration {}", count+1); } }
This behaves the same as above. 0..10
gives us the range from 0 to 10, excluding the 10. That means we have to do the awkward +1
to get count the same. We can specify the range as an inclusive range instead to avoid that, using 1..=10
, which ranges from 1 to 10, including the 10.
#![allow(unused)] fn main() { for count in 1..=10 { println!("iteration {count}"); } }
Collections of things are similar. Let's see a basic example with an array. We'll see examples from other collections later on, when we talk about the standard library.
#![allow(unused)] fn main() { for prime in [2, 3, 5, 7, 11] { println!("{prime} is prime."); } }
And like with the other types of loops, you can use break
in for loops!
You usually won't need it, but it comes in handy occasionally.
Loop values
We mentioned before that in Rust, most expressions return values. But if you try to do that with a loop, you're going to see a compiler error:
#![allow(unused)] fn main() { let x = for count in 0..3 { count * 2 }; }
error[E0308]: mismatched types
--> src/main.rs:4:5
|
4 | count * 2
| ^^^^^^^^^ expected `()`, found integer
|
help: you might have meant to break the loop with this value
|
4 | break count * 2;
| +++++ +
For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error
One of the really nice things with Rust is that the compiler messages are often helpful. (Sometimes, they try to be helpful... Trust yourself over the compiler, it cannot know your intent!) In this case, it tells us precisely what we need to do: use the break
keyword.
There are two things to unpack here:
- The loop body must result in
()
, so the last statement needs to end in a semicolon. - If you want the loop to result in a value, you have to emit that value using the
break
keyword.
Here's the previous example, but modified to work (and made a little more interesting):
#![allow(unused)] fn main() { let x = for count in 0..3 { if count > 1 { break count * 2; } }; }
Oops, another compiler error!
Compiling playground v0.0.1 (/playground)
error[E0571]: `break` with value from a `for` loop
--> src/main.rs:5:9
|
3 | let x = for count in 0..3 {
| ----------------- you can't `break` with a value in a `for` loop
4 | if count > 1 {
5 | break count * 2;
| ^^^^^^^^^^^^^^^ can only break with a value inside `loop` or breakable block
|
help: use `break` on its own without a value inside this `for` loop
|
5 | break;
| ~~~~~
For more information about this error, try `rustc --explain E0571`.
error: could not compile `playground` due to previous error
See, the problem is that while- and for-expressions are not going to be guaranteed to have a value, because they may hit their termination before they hit the break statement.
We can do it with loop
though. This one will compile, I promise:
#![allow(unused)] fn main() { let mut count = 0; let x = loop { if count > 1 { break count * 2; } count += 1; }; }
And there you have it: control flow! The first of many things Rust provides to do useful things.
Exercises:
- Write fizz buzz using a while loop.
- Now write fizz buzz using a for loop.
We'll see you in the next section for functions.
If you use it this way, the branches must have matching types. You can't, say, return a string from one branch and an integer from the other.
And thank goodness, too. This helps prevent some infamous bugs with major security implications, like goto fail.