Глава 4: Управление потоком

Содержание: Условия: if, else if, else Match: сопоставление с образцом Циклы: loop, while, for Прерывание и продолжение (break, continue) Завершение программы (exit) в Rust Примеры: разбор случаев с match Упражнение: Реализовать конечный автомат с помощью match

Добро пожаловать в четвёртую главу курса по Rust! Сегодня мы разберём управление потоком — ключевой аспект программирования, позволяющий вашему коду принимать решения, повторять действия и обрабатывать различные сценарии. Мы рассмотрим условия (if, else if, else), сопоставление с образцом (match), циклы (loop, while, for), а также операторы break и continue.

Rust сочетает выразительность и безопасность, и управление потоком здесь строго типизировано. Мы разберём каждый элемент с примерами, советами и завершится глава упражнением по созданию конечного автомата с использованием match. Приступим!


1. Условия: if, else if, else

Основы

Условия это как заставить программу выбирать, т.е. позволяют выполнять код в зависимости от истинности выражения. Конструкция if — базовый инструмент ветвления, она как развилка на дороге, помогает программе решать, что делать дальше.:

if условие {
    // код выполняется, если условие истинно
} else {
    // код выполняется, если условие ложно
}

Условие в if должно быть типа bool. Rust не позволяет использовать числа или другие типы напрямую.

Пример простого условия

fn main() {
    let number = 7;

    if number > 0 {
        println!("Число положительное");
    } else {
        println!("Число отрицательное или ноль");
    }
}

Множественные условия с else if

fn main() {
    let number = 42;

    if number > 100 {
        println!("Число больше 100");
    } else if number > 0 {
        println!("Число положительное, но меньше или равно 100");
    } else {
        println!("Число отрицательное или ноль");
    }
}

if как выражение

В Rust if может возвращать значение:

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };
    println!("Значение числа: {}", number); // Вывод: Значение числа: 5
}

Все ветки должны возвращать один тип!

Совет: Используйте if как выражение для компактности. Для сложных случаев переходите к match.

2. match: Сопоставление с образцом

Что такое match?

match — мощный инструмент для сопоставления значения с образцами. Он требует исчерпывающего покрытия случаев т.е. требующий полного охвата всех возможных вариантов:

match значение {
    образец1 => выражение1,
    образец2 => выражение2,
    _ => выражение_по_умолчанию,
}

Пример базового использования

fn main() {
    let number = 2;

    match number {
        1 => println!("Один"),
        2 => println!("Два"),
        3 => println!("Три"),
        _ => println!("Что-то другое"),
    }
}

Сопоставление с условиями

fn main() {
    let number = 42;

    match number {
        n if n > 0 => println!("Положительное: {}", n),
        n if n < 0 => println!("Отрицательное: {}", n),
        _ => println!("Ноль"),
    }
}

Что за n и откуда оно взялось?

n — это имя переменной, которое ты задаёшь прямо в шаблоне match. Оно называется привязкой (binding) в терминологии Rust.

Когда ты пишешь n if n > 0, ты говоришь: "Возьми значение number и назови его n для этой ветки. Затем проверь условие if n > 0. Если оно истинно, выполни действие".

n — это временная переменная, которая существует только внутри конкретной ветки match и принимает значение того, что ты проверяешь (здесь number, то есть 42).

Почему n работает?

В match number ты проверяешь значение number. Каждый шаблон в match должен либо:

Здесь n — это способ захватить значение number и работать с ним в условии if.

Можно ли использовать любую букву?

Да, вместо n ты можешь использовать любое допустимое имя переменной (букву, слово и т.д.), главное, чтобы оно было осмысленным для тебя и не конфликтовало с другими именами в области видимости. Например:

fn main() {
    let number = 42;

    match number {
        x if x > 0 => println!("Положительное: {}", x),
        y if y < 0 => println!("Отрицательное: {}", y),
        _ => println!("Ноль"),
    }
}

Здесь x и y — просто разные имена для одной и той же идеи: они привязывают значение number в своих ветках.

Ты можешь даже назвать их value, num или как угодно ещё.

Но важно: имя должно быть одинаковым внутри одной ветки (нельзя написать n if x > 0, это вызовет ошибку).

Как это работает шаг за шагом?

  1. match number берёт значение number (в данном случае 42).
  2. Проверяет ветки по порядку:

Если бы number было, например, -5, сработала бы вторая ветка. Если бы number было 0, сработала бы третья (_).

Почему нужно _?

match требует исчерпывающего покрытия — ты должен учесть все возможные случаи.
Условия if n > 0 и if n < 0 не покрывают 0, поэтому без _ компилятор выдал бы ошибку. _ — это "ловушка" для всех значений, которые не подошли под предыдущие ветки.

Когда использовать такие условия?

Обычно match используется с точными значениями (например, 1 => ..., 2 => ...), но добавление if позволяет проверять более сложные условия, как в этом примере (положительное, отрицательное или ноль). Это называется guards (охрана) в Rust.

Альтернатива без привязки

Если тебе не нужно имя внутри ветки, можно обойтись без него, но тогда код будет менее гибким:

match number {
    42 => println!("Ровно 42"), // точное значение
    _ if number > 0 => println!("Положительное: {}", number),
    _ if number < 0 => println!("Отрицательное: {}", number),
    _ => println!("Ноль"),
}

Здесь _ используется как "не интересующее нас имя", а значение берётся напрямую из number.

Итог

match как выражение

fn main() {
    let number = 3;
    let result = match number {
        1 => "один",
        2 => "два",
        _ => "другое",
    };
    println!("Результат: {}", result); // Вывод: Результат: другое
}
Совет: Используйте match вместо длинных цепочек else if.

3. Циклы: loop, while, for

loop: Бесконечный цикл

Бесконечный цикл, который выполняется до явного прерывания (обычно с помощью break). Используется, когда нужно что-то повторять "вечно" или до выполнения условия выхода.

Пример: loop { println!("Повтор"); break; }

fn main() {
    let mut count = 0;
    loop {
        println!("Счёт: {}", count);
        count += 1;
        if count == 5 {
            break;
        }
    }
}

Программа запускает бесконечный цикл loop, который выводит значение переменной count (начиная с 0) и увеличивает её на 1 на каждой итерации. Когда count достигает 5, условие if count == 5 срабатывает, и цикл прерывается с помощью break. В итоге выводится:

Счёт: 0
Счёт: 1
Счёт: 2
Счёт: 3
Счёт: 4

Можно возвращать значение через break:

fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!("Результат: {}", result); // Вывод: Результат: 20
}

while: Цикл с условием

Цикл с условием — выполняется, пока условие истинно. Удобен, когда количество итераций заранее неизвестно, но есть проверка.

Пример: while x < 5 { x += 1; }

fn main() {
    let mut number = 3;
    while number != 0 {
        println!("Осталось: {}", number);
        number -= 1;
    }
    println!("Взлёт!");
}

Программа использует цикл while, который выполняется, пока переменная number не равна 0. На каждой итерации выводится текущее значение number (начиная с 3), затем оно уменьшается на 1. Когда number становится 0, цикл завершается, и выводится "Взлёт!". Результат:

Осталось: 3
Осталось: 2
Осталось: 1
Взлёт!

for: Итерация по коллекциям

Цикл для итерации(перебора) по коллекциям или диапазонам. Самый безопасный и удобный для перебора элементов.

Пример: for i in 0..5 { println!("{}", i); } (выводит 0, 1, 2, 3, 4)

fn main() {
    let numbers = [10, 20, 30, 40];
    for num in numbers {
        println!("Число: {}", num);
    }
}

Программа использует цикл for для перебора элементов массива numbers, содержащего значения [10, 20, 30, 40]. На каждой итерации переменная num принимает очередное значение из массива, и оно выводится. Результат:

Число: 10
Число: 20
Число: 30
Число: 40

Для диапазонов:

fn main() {
    for i in 1..4 {  // от 1 до 3
        println!("i = {}", i);
    }
    for i in 1..=4 {  // от 1 до 4 включительно
        println!("i = {}", i);
    }
}
Совет: Предпочитайте for для перебора — он безопасен и читаем.

4. Прерывание и продолжение: break и continue

break

fn main() {
    let mut count = 0;
    while true {
        count += 1;
        if count == 5 {
            break; // Выход из цикла
        }
        println!("Счёт: {}", count);
    }
}

continue

fn main() {
    for i in 1..6 {
        if i % 2 == 0 {
            continue; // Пропускаем чётные числа
        }
        println!("Нечётное: {}", i);
    }
}

5. Завершение программы (exit) в Rust

В Rust для "нормального" завершения программы используется функция std::process::exit из стандартной библиотеки. Она завершает программу немедленно с заданным кодом возврата, не вызывая панику.

use std::process;

fn main() {
    println!("До выхода");
    process::exit(0); // Завершаем программу с кодом 0 (успех)
    println!("Это не выведется");
}

Как работает?

Пример с условием

use std::process;

fn main() {
    let should_stop = true;
    if should_stop {
        println!("Пока!");
        process::exit(0); // Выходим без ошибок
    }
    println!("Дальше не дойдём");
}

Альтернативы

Возврат из main

Если ты просто хочешь завершить программу "по-хорошему", можно использовать return в main. Это не то же самое, что exit (не мгновенно прерывает), но подходит для естественного завершения:

fn main() {
    println!("Работаем...");
    return; // Завершаем с кодом 0
    println!("Это не выведется");
}

Код возврата по умолчанию — 0, если не указано иное через std::process::Termination.

Код возврата через Result

В Rust принято возвращать Result из main, чтобы указать успех или ошибку:

fn main() -> Result<(), i32> {
    println!("Работаем...");
    Ok(()); // Успех, код 0
    // или Err(1) для ошибки с кодом 1
}

Что выбрать?


6. Примеры: Разбор случаев с match

enum Direction {
    North,
    East,
    South,
    West,
}

fn main() {
    let dir = Direction::East;

    match dir {
        Direction::North => println!("Идём на север!"),
        Direction::East => println!("Идём на восток!"),
        Direction::South => println!("Идём на юг!"),
        Direction::West => println!("Идём на запад!"),
    }
}

Выведет "Идём на восток!"

С вложенными данными:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(u32),
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(num) => 25 * num,
    }
}

fn main() {
    let coin = Coin::Quarter(3);
    println!("Стоимость: {} центов", value_in_cents(coin)); // Вывод: 75 центов
}

7. Упражнение: Реализовать конечный автомат с помощью match

Задача: Реализуйте конечный автомат, моделирующий светофор с состояниями Red, Yellow, Green. Переключайте состояния по таймеру.

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn main() {
    let mut state = TrafficLight::Red;
    let mut timer = 0;

    loop {
        match state {
            TrafficLight::Red => {
                println!("Красный: остановитесь!");
                if timer >= 3 {
                    state = TrafficLight::Green;
                    timer = 0;
                }
            }
            TrafficLight::Green => {
                println!("Зелёный: идите!");
                if timer >= 5 {
                    state = TrafficLight::Yellow;
                    timer = 0;
                }
            }
            TrafficLight::Yellow => {
                println!("Жёлтый: приготовьтесь!");
                if timer >= 2 {
                    state = TrafficLight::Red;
                    timer = 0;
                }
            }
        }
        timer += 1;
        println!("Таймер: {}", timer);

        if timer > 10 {
            break; // Для завершения примера
        }
    }
}

Улучшение: Добавьте ввод пользователя для ручного переключения.

Подсказка: Используйте.

use std::io;
....
let mut input = String::new();
...
io::stdin().read_line(&mut input).expect("Ошибка чтения строки");
...


Заключение

Мы изучили основы управления потоком в Rust: условия, сопоставление, циклы и операторы прерывания. Эти инструменты помогут вам строить гибкую логику. Практикуйтесь с примерами и упражнением!