Lesson 6 of 22ยท15 min

Control Flow

A program that runs every line exactly once, top to bottom, can't do much. Control flow is how code decides which lines run, and how many times: choosing between branches, repeating until something is done, or walking through a list of values.

Rust has one way to branch, if, and three loops: loop, while and for. Most of it looks like other languages, with one twist from the previous lesson: if and loop are expressions, so they can produce values.

if and else

An if runs its block only when its condition is true. An optional else block runs otherwise:

fn main() {
    let battery = 15;
 
    if battery < 20 {
        println!("Battery low, plug in soon");
    } else {
        println!("Battery fine");
    }
}
Battery low, plug in soon

There are no parentheses around the condition, but the braces around each block are always required, even for a single line.

To check several conditions in turn, chain them with else if. The first condition that is true wins, and the rest are skipped:

fn main() {
    let wind_kmh = 48;
 
    if wind_kmh >= 75 {
        println!("Storm warning");
    } else if wind_kmh >= 40 {
        println!("Strong wind");
    } else if wind_kmh >= 20 {
        println!("Breezy");
    } else {
        println!("Calm");
    }
}
Strong wind

48 is also at least 20, but the >= 40 branch comes first, so only Strong wind is printed. Order the conditions from most to least specific.

Conditions must be bool

The condition must be a bool. Rust never treats a number, an empty string or anything else as "truthy", so compare explicitly: if count != 0, not if count. You can combine conditions with &&, || and ! from the primitive types lesson:

fn main() {
    let age = 34;
    let has_ticket = true;
 
    if age >= 18 && has_ticket {
        println!("Welcome in");
    }
    if !has_ticket || age < 18 {
        println!("Sorry, not tonight");
    }
}
Welcome in

if is an expression

In Rust, if produces a value: the value of whichever block ran. That means it can go on the right side of a let, where other languages would need a ternary operator:

fn main() {
    let items = 3;
    let label = if items == 1 { "item" } else { "items" };
    println!("{items} {label} in your cart");
 
    let delivery = if items >= 5 { 0.0 } else { 4.99 };
    println!("delivery: {delivery}");
}
3 items in your cart
delivery: 4.99

Each block ends in an expression with no semicolon, just like a function body. Because the result is bound to one variable with one type, every branch has to produce a value of the same type, and there has to be an else: without one, the if would have no value when the condition is false.

This works anywhere a value is expected, including as the last expression of a function, which is a common way to return one of several results:

fn ticket_price(age: u32) -> u32 {
    if age < 12 {
        8
    } else if age >= 65 {
        10
    } else {
        15
    }
}
 
fn main() {
    for age in [7, 40, 70] {
        println!("age {age}: {}", ticket_price(age));
    }
}
age 7: 8
age 40: 15
age 70: 10

Repeating with loop

loop runs its body forever, until you stop it with break:

fn main() {
    let mut attempts = 0;
 
    loop {
        attempts += 1;
        println!("connecting, attempt {attempts}");
        if attempts == 3 {
            break;
        }
    }
    println!("connected");
}
connecting, attempt 1
connecting, attempt 2
connecting, attempt 3
connected

continue skips the rest of the current iteration and starts the next one.

loop is an expression too. Give break a value and the loop evaluates to it, which is handy when you repeat something until it produces a result:

fn main() {
    let mut n = 27;
    let mut steps = 0;
 
    let steps_to_one = loop {
        if n == 1 {
            break steps;
        }
        n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
        steps += 1;
    };
    println!("27 reaches 1 in {steps_to_one} steps");
}
27 reaches 1 in 111 steps

Loop labels

break and continue apply to the innermost loop. To break out of an outer loop from inside a nested one, give the outer loop a label, written with a leading apostrophe, and name that label in the break:

fn main() {
    let grid = [[3, 8, 1], [4, 0, 7], [9, 2, 5]];
 
    'rows: for row in grid {
        for cell in row {
            if cell == 0 {
                println!("found an empty cell, stopping");
                break 'rows;
            }
            println!("cell {cell}");
        }
    }
}
cell 3
cell 8
cell 1
cell 4
found an empty cell, stopping

A plain break there would only end the inner loop, and the outer one would carry on with the next row.

while

A while loop checks a condition before every iteration and stops as soon as it is false. It is loop plus an if and a break, written more directly:

fn main() {
    let mut balance = 1000.0;
    let mut years = 0;
 
    while balance < 2000.0 {
        balance *= 1.07;
        years += 1;
    }
    println!("doubled after {years} years: {balance:.2}");
}
doubled after 11 years: 2104.85

Use while when you don't know in advance how many times to loop, only when to stop. Unlike loop, a while loop can't break with a value: it can also end because the condition turned false, and then there would be no value.

for and ranges

for runs its body once for every item in a sequence. You have already used it on arrays. The other thing you will loop over constantly is a range:

  • a..b counts from a up to, but not including, b;
  • a..=b includes b.
fn main() {
    for i in 0..3 {
        print!("[{i}]");
    }
    println!();
 
    for i in 1..=3 {
        print!("[{i}]");
    }
    println!();
}
[0][1][2]
[1][2][3]

The exclusive form fits indexes: 0..array.len() covers every valid index and stops before the length. The inclusive form fits human counting, like "from 1 to 10".

Ranges have methods to change how they are walked. rev() goes backwards and step_by(n) takes every nth value:

fn main() {
    for i in (1..=3).rev() {
        print!("{i}... ");
    }
    println!("go!");
 
    for minute in (0..60).step_by(15) {
        println!("10:{minute:02}");
    }
}
3... 2... 1... go!
10:00
10:15
10:30
10:45

The parentheses around the range are needed so the method applies to the whole range, not just its end.

A range is also a value you can store and ask questions of, for example with contains:

fn main() {
    let working_hours = 9..17;
    let hour = 18;
    println!("open at {hour}:00? {}", working_hours.contains(&hour));
}
open at 18:00? false

Which loop to use

  • for when you have something to walk through: a range, an array or, later, a Vec or an iterator. It is the most common loop in Rust, and it can't go out of bounds.
  • while when you loop until a condition changes.
  • loop when the exit is somewhere in the middle of the body, or when the loop should produce a value.

A for over a collection is also safer and clearer than a while that counts an index by hand: there is no counter to forget to increment and no index to get wrong.

Later lessons add one more way to branch: match, which compares a value against a list of patterns. It gets its own lesson with enums.

Common mistakes

A condition that isn't a bool

fn main() {
    let unread = 4;
    if unread {
        println!("You have mail");
    }
}

Write the comparison you mean: if unread > 0.

Branches with different types

fn main() {
    let stock = 0;
    let status = if stock > 0 { stock } else { "sold out" };
    println!("{status}");
}

The compiler reports the first branch's type as the expected one. Make every branch produce the same type, here for example by formatting the number into a String in one branch and turning the text into a String in the other.

Using if without else as a value

fn main() {
    let points = 120;
    let bonus = if points > 100 {
        10
    };
    println!("{bonus}");
}

Without else, the if evaluates to () when the condition is false, which doesn't match 10. Add an else branch with a value of the same type.

Breaking out of while with a value

fn main() {
    let mut n = 1;
    let first_big = while n < 1000 {
        n *= 3;
        if n > 100 {
            break n;
        }
    };
    println!("{first_big:?}");
}

Only loop can return a value from break. Rewrite it as a loop, or keep the while and read n after it ends.

Summary

  • if condition { ... } else if other { ... } else { ... } picks the first branch whose condition is true. Conditions must be bool.
  • if is an expression: let x = if c { a } else { b };. Every branch needs the same type, and a value-producing if needs an else.
  • loop repeats until break; break value makes the loop evaluate to value. continue jumps to the next iteration.
  • Labels like 'outer let break and continue target an outer loop.
  • while loops while a condition holds.
  • for x in range walks a range: a..b excludes b, a..=b includes it. rev() and step_by(n) change the direction and step.
  • Prefer for over hand-counted indexes.

Practise what you read

Challenges that use the ideas from this lesson.