Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Управление потоком выполнения

Выполнять код в зависимости от того, истинно ли условие, и повторять код, пока условие истинно, — это базовые строительные блоки большинства языков программирования. Чаще всего управлять потоком выполнения кода на Rust приходится с помощью выражений if и циклов.

Выражения if

Выражение if позволяет разветвить код в зависимости от условий. Вы задаёте условие и говорите: «Если условие выполняется, запусти этот блок кода. Если нет — не запускай».

Чтобы поэкспериментировать с if, создайте в папке projects новый проект branches. В файл src/main.rs введите следующее:

Файл: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Любое выражение if начинается с ключевого слова if, за которым следует условие. Здесь условие проверяет, меньше ли значение переменной number, чем 5. Блок кода, который должен выполниться, если условие истинно (true), мы пишем сразу после условия в фигурных скобках. Блоки кода, связанные с условиями в if, иногда называют ветками — так же, как ветки выражения match, о которых мы говорили в разделе «Сравниваем догадку с загаданным числом» главы 2.

При желании можно добавить и выражение else — что мы здесь и сделали. Оно задаёт альтернативный блок кода, который выполнится, если условие окажется ложным (false). Если else нет, а условие ложно, программа просто пропустит блок if и перейдёт к следующему коду.

Запустите этот код — вывод должен быть таким:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

Давайте поменяем значение number так, чтобы условие стало ложным, и посмотрим, что получится:

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Снова запустите программу и посмотрите на вывод:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

Стоит отметить, что условие в этом коде обязательно должно иметь тип bool. Если это не bool, будет ошибка. Попробуйте, например, запустить такой код:

Файл: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

На этот раз условие if вычисляется в 3, и Rust выдаёт ошибку:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

Ошибка говорит, что Rust ожидал bool, а получил целое число. В отличие от Ruby или JavaScript, Rust не пытается автоматически превратить не-логическое значение в логическое. Нужно писать явно и всегда передавать if в качестве условия логическое значение. Если, например, мы хотим, чтобы блок if выполнялся, только когда число не равно 0, выражение нужно переписать так:

Файл: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

Этот код выведет number was something other than zero.

Несколько условий: else if

Несколько условий можно проверить, соединив if и else в выражение else if. Например:

Файл: src/main.rs

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");
    }
}

У этой программы четыре возможных пути выполнения. Запустив её, вы увидите:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

Программа по очереди проверяет каждое условие if и выполняет тело первого, которое окажется истинным. Обратите внимание: хотя 6 делится на 2, мы не видим ни number is divisible by 2, ни текста number is not divisible by 4, 3, or 2 из блока else. Дело в том, что Rust выполняет блок только для первого истинного условия, а найдя его, остальные даже не проверяет.

Если else if слишком много, код становится запутанным. Так что если их больше одного, возможно, стоит переписать код. Для таких случаев в главе 6 описана мощная конструкция ветвления Rust — match.

if в инструкции let

Раз if — это выражение, его можно поставить справа в инструкции let и присвоить результат переменной, как в листинге 3-2.

Файл: src/main.rs
fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}
Листинг 3-2: Присваиваем переменной результат выражения if

Переменная number получит значение в зависимости от результата выражения if. Запустите код и посмотрите, что получится:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

Напомним, что блок кода вычисляется в своё последнее выражение, а число само по себе — тоже выражение. В нашем случае значение всего выражения if зависит от того, какой блок выполнится. Значит, все значения, которые может вернуть каждая ветка if, должны быть одного типа. В листинге 3-2 и ветка if, и ветка else давали целые числа типа i32. Если типы не совпадают, как в следующем примере, будет ошибка:

Файл: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {number}");
}

При компиляции этого кода мы получим ошибку. Ветки if и else возвращают значения несовместимых типов, и Rust точно указывает, где в программе проблема:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

Выражение в блоке if вычисляется в целое число, а выражение в блоке else — в строку. Так нельзя: у переменной может быть только один тип, и Rust должен ещё при компиляции точно знать, какого типа переменная number. Зная её тип, компилятор может проверить, что везде, где используется number, тип подходит. Если бы тип number становился известен только во время выполнения, такой проверки бы не было. Компилятору пришлось бы держать в уме сразу несколько возможных типов для каждой переменной — он стал бы сложнее, а гарантий о коде давал бы меньше.

Повторение: циклы

Часто бывает нужно выполнить блок кода несколько раз. Для этого в Rust есть несколько видов циклов: цикл выполняет код в своём теле до конца и сразу же начинает сначала. Чтобы поэкспериментировать с циклами, создадим новый проект loops.

В Rust три вида циклов: loop, while и for. Попробуем каждый.

Повторение кода с loop

Ключевое слово loop говорит Rust выполнять блок кода снова и снова — бесконечно или пока вы явно не скажете остановиться.

Для примера замените содержимое src/main.rs в папке loops на такое:

Файл: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

Если запустить эту программу, again! будет выводиться снова и снова, пока мы не остановим её вручную. Большинство терминалов поддерживают сочетание ctrl-C, которое прерывает зациклившуюся программу. Попробуйте:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

^C отмечает момент, когда вы нажали ctrl-C.

После ^C слово again! может появиться, а может и нет — это зависит от того, в каком месте цикла был код, когда пришёл сигнал прерывания.

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

В той же игре мы использовали и continue. В цикле оно говорит программе пропустить весь оставшийся код текущей итерации и перейти к следующей.

Возврат значений из цикла

Одно из применений loop — повторять операцию, которая может не удаться, например проверять, закончил ли поток свою работу. При этом результат операции может понадобиться вынести из цикла в остальной код. Для этого добавьте нужное значение после выражения break, которым вы останавливаете цикл: это значение вернётся из цикла, и его можно будет использовать. Вот так:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Перед циклом мы объявляем переменную counter и инициализируем её значением 0. Затем объявляем переменную result, в которую попадёт значение, возвращённое циклом. На каждой итерации мы прибавляем к counter единицу и проверяем, не равен ли counter десяти. Когда равен, вызываем break со значением counter * 2. После цикла стоит точка с запятой — она завершает инструкцию, которая присваивает значение result. В конце мы выводим значение result — в данном случае 20.

Из цикла можно выйти и через return. Разница в том, что break выходит только из текущего цикла, а return — всегда из текущей функции.

Метки циклов

Если циклы вложены друг в друга, break и continue относятся к самому внутреннему циклу в этом месте. Но циклу можно дать метку (loop label) и затем указать её в break или continue — тогда они будут относиться к помеченному циклу, а не к самому внутреннему. Метка цикла должна начинаться с одинарной кавычки. Вот пример с двумя вложенными циклами:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

У внешнего цикла метка 'counting_up, он считает от 0 до 2. Внутренний цикл без метки считает от 10 до 9. Первый break без метки выходит только из внутреннего цикла, а инструкция break 'counting_up; выходит из внешнего. Этот код выведет:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

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

Программе часто нужно проверять условие внутри цикла: пока оно истинно, цикл работает, а как только перестаёт быть истинным, программа вызывает break и цикл останавливается. Такое поведение можно собрать из loop, if, else и break — если хотите, попробуйте прямо сейчас. Но этот шаблон встречается настолько часто, что в Rust для него есть встроенная конструкция — цикл while. В листинге 3-3 с помощью while программа трижды проходит цикл, каждый раз уменьшая счётчик, а после цикла выводит сообщение и завершается.

Файл: src/main.rs
fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}
Листинг 3-3: Цикл while: код выполняется, пока условие истинно

Эта конструкция избавляет от лишней вложенности, которая понадобилась бы с loop, if, else и break, и читается понятнее. Пока условие истинно, код выполняется; как только станет ложным — цикл завершается.

Обход коллекции с for

Перебрать элементы коллекции, например массива, можно и с помощью while. Скажем, цикл в листинге 3-4 выводит каждый элемент массива a.

Файл: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}
Листинг 3-4: Обход всех элементов коллекции циклом while

Здесь код перебирает элементы массива по индексу: начинает с 0 и крутится, пока не дойдёт до последнего индекса (то есть пока index < 5 не перестанет быть истинным). Код выведет все элементы массива:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

Как и ожидалось, в терминале появились все пять значений. Хотя index в какой-то момент становится равным 5, цикл останавливается раньше, чем программа попытается достать из массива шестое значение.

Но такой подход чреват ошибками: если индекс или условие окажутся неверными, программа может запаниковать. Например, если сократить массив a до четырёх элементов и забыть поменять условие на while index < 4, код запаникует. К тому же он медленный: компилятор добавляет код, который на каждой итерации во время выполнения проверяет, не вышел ли индекс за границы массива.

Более лаконичная альтернатива — цикл for, который выполняет код для каждого элемента коллекции. Он выглядит как в листинге 3-5.

Файл: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}
Листинг 3-5: Обход всех элементов коллекции циклом for

Вывод будет таким же, как в листинге 3-4. Но, что важнее, код стал безопаснее: исчезла возможность ошибиться — выйти за конец массива или, наоборот, не дойти до конца и пропустить элементы. Машинный код для циклов for к тому же может быть эффективнее, ведь индекс не нужно сравнивать с длиной массива на каждой итерации.

С циклом for вам не придётся помнить, что нужно поменять ещё какой-то код, если в массиве изменится количество элементов, — а со способом из листинга 3-4 пришлось бы.

Благодаря безопасности и лаконичности for — самый популярный вид цикла в Rust. Даже когда нужно выполнить код определённое число раз, как в примере с обратным отсчётом через while из листинга 3-3, большинство растацеанов возьмут for. Для этого используют Range (диапазон) из стандартной библиотеки: он по порядку выдаёт числа от начального значения до конечного, не включая само конечное.

Вот как выглядит обратный отсчёт с циклом for и ещё одним методом, о котором мы пока не говорили, — rev, который разворачивает диапазон:

Файл: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

Так ведь приятнее, правда?

Итоги

Вы справились! Глава была немаленькая: вы узнали о переменных, скалярных и составных типах данных, функциях, комментариях, выражениях if и циклах. Чтобы потренироваться, попробуйте написать программы, которые:

  • переводят температуру из градусов Фаренгейта в градусы Цельсия и обратно;
  • вычисляют n-е число Фибоначчи;
  • выводят текст рождественской песни «The Twelve Days of Christmas» («Двенадцать дней Рождества»), используя то, что куплеты в ней повторяются.

Когда будете готовы двигаться дальше, мы поговорим о понятии, которого в других языках программирования обычно нет, — о владении.