Control Flow
Control flow allows you to determine the order in which code executes. Rust provides several control flow constructs that are both powerful and safe, including conditional expressions, various types of loops, and pattern matching.
If Expressions
In Rust, if
is an expression, not a statement, which means it returns a value:
fn main() {
let number = 6;
if number % 4 == 0 {
println!("Number is divisible by 4");
} else if number % 3 == 0 {
println!("Number is divisible by 3");
} else if number % 2 == 0 {
println!("Number is divisible by 2");
} else {
println!("Number is not divisible by 4, 3, or 2");
}
}
If as an Expression
Since if
is an expression, you can use it to assign values:
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {}", number);
// Both arms must return the same type
let x = 5;
let message = if x > 0 {
"positive"
} else if x < 0 {
"negative"
} else {
"zero"
};
println!("Number is {}", message);
}
Type Consistency in If Expressions
fn main() {
let condition = true;
// This works - both arms return i32
let number = if condition { 5 } else { 6 };
// This would cause a compile error - mismatched types
// let number = if condition { 5 } else { "six" };
}
Loops
Rust has three kinds of loops: loop
, while
, and for
.
Infinite Loops with loop
The loop
keyword creates an infinite loop:
fn main() {
let mut counter = 0;
loop {
counter += 1;
if counter == 3 {
println!("three!");
continue; // Skip the rest of this iteration
}
println!("counter: {}", counter);
if counter == 5 {
break; // Exit the loop
}
}
println!("Loop ended");
}
Returning Values from Loops
You can return values from loops using break
:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // Return a value from the loop
}
};
println!("The result is {}", result); // 20
}
Loop Labels
You can label loops to break out of nested loops:
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {}", count);
let mut remaining = 10;
loop {
println!("remaining = {}", remaining);
if remaining == 9 {
break; // Break inner loop only
}
if count == 2 {
break 'counting_up; // Break outer loop
}
remaining -= 1;
}
count += 1;
}
println!("End count = {}", count);
}
While Loops
while
loops continue as long as a condition is true:
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
// Example: Finding an element
let mut index = 0;
let arr = [10, 20, 30, 40, 50];
while index < arr.len() {
if arr[index] == 30 {
println!("Found 30 at index {}", index);
break;
}
index += 1;
}
}
For Loops
for
loops iterate over collections:
fn main() {
let arr = [10, 20, 30, 40, 50];
// Iterate over array elements
for element in arr {
println!("The value is: {}", element);
}
// Iterate over a range
for number in 1..4 {
println!("{}!", number);
}
// Inclusive range
for number in 1..=3 {
println!("Inclusive: {}", number);
}
// Iterate with index
for (index, value) in arr.iter().enumerate() {
println!("Index {}: {}", index, value);
}
}
Iterating Over Collections
fn main() {
let names = vec!["Alice", "Bob", "Charlie"];
// Iterate by reference (doesn't consume the vector)
for name in &names {
println!("Hello, {}!", name);
}
println!("Names still available: {:?}", names);
// Iterate by value (consumes the vector)
for name in names {
println!("Goodbye, {}!", name);
}
// println!("{:?}", names); // Error: names has been moved
// Iterate over mutable references
let mut numbers = vec![1, 2, 3, 4, 5];
for num in &mut numbers {
*num *= 2;
}
println!("Doubled: {:?}", numbers);
}
Match Expressions
match
is a powerful control flow construct that allows pattern matching:
fn main() {
let number = 13;
match number {
1 => println!("One!"),
2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
13..=19 => println!("A teen"),
_ => println!("Ain't special"),
}
}
Match with Values
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let coin = Coin::Penny;
println!("Value: {} cents", value_in_cents(coin));
}
Destructuring with Match
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn process_message(msg: Message) {
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {} and in the y direction {}", x, y);
}
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => {
println!("Change the color to red {}, green {}, and blue {}", r, g, b);
}
}
}
fn main() {
let messages = vec![
Message::Quit,
Message::Move { x: 10, y: 30 },
Message::Write("hello".to_string()),
Message::ChangeColor(200, 255, 255),
];
for msg in messages {
process_message(msg);
}
}
Matching Options
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
fn main() {
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
println!("six: {:?}", six);
println!("none: {:?}", none);
}
Guards in Match
fn main() {
let num = Some(4);
match num {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
If Let
if let
is a shorter way to match against one pattern:
fn main() {
let config_max = Some(3u8);
// Using match
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
// Using if let (more concise)
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
// if let with else
let mut count = 0;
if let Some(max) = config_max {
count = max;
} else {
count = 10;
}
println!("Count: {}", count);
}
While Let
Similar to if let
, but for loops:
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
// Example with iterator
let mut iter = vec![1, 2, 3, 4, 5].into_iter();
while let Some(value) = iter.next() {
if value > 3 {
break;
}
println!("Value: {}", value);
}
}
Pattern Matching in Function Parameters
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
Advanced Pattern Matching
Matching Literals and Variables
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
// Matching named variables
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {:?}", y), // This y shadows the outer y
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {:?}", x, y);
}
Ranges in Patterns
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
Destructuring Structs
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {}", x),
Point { x: 0, y } => println!("On the y axis at {}", y),
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
}
// Destructuring with different names
let Point { x: a, y: b } = p;
println!("a: {}, b: {}", a, b);
}
Ignoring Values with _
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {}, {}, {}", first, third, fifth);
}
}
// Ignoring unused variables
let _x = 5; // No warning about unused variable
// Ignoring parts of a value
let origin = Point { x: 0, y: 0 };
match origin {
Point { x, .. } => println!("x is {}", x),
}
}
Control Flow Best Practices
1. Use if let
for Simple Option/Result Handling
// Good: Simple and clear
if let Some(value) = optional_value {
process(value);
}
// Avoid: Unnecessarily verbose for simple case
// match optional_value {
// Some(value) => process(value),
// None => (),
// }
2. Prefer for
Loops Over Manual Indexing
let items = vec![1, 2, 3, 4, 5];
// Good: Iterator-based
for item in &items {
println!("{}", item);
}
// Avoid: Manual indexing (error-prone)
// for i in 0..items.len() {
// println!("{}", items[i]);
// }
3. Use Early Returns to Reduce Nesting
fn process_data(data: Option<&str>) -> Result<String, &'static str> {
let data = match data {
Some(d) => d,
None => return Err("No data provided"),
};
if data.is_empty() {
return Err("Data is empty");
}
Ok(format!("Processed: {}", data))
}
4. Leverage Pattern Matching for Complex Logic
enum State {
Loading,
Ready(String),
Error(String),
}
fn handle_state(state: State) {
match state {
State::Loading => println!("Please wait..."),
State::Ready(data) => println!("Data: {}", data),
State::Error(msg) => eprintln!("Error: {}", msg),
}
}
Rust's control flow constructs provide powerful and safe ways to direct program execution. The combination of expressions returning values, pattern matching, and iterator-based loops enables you to write clear, efficient, and maintainable code.