Управление потоком выполнения
Возможность выполнять некоторый код в зависимости от того, является ли условие true, и выполнять некоторый код повторно, пока условие остаётся true, — это базовые строительные блоки в большинстве языков программирования. Наиболее распространённые конструкции, позволяющие управлять потоком выполнения кода Rust, — это выражения if и циклы.
Выражения if
Выражение if позволяет ветвить ваш код в зависимости от условий. Вы указываете условие и затем говорите: «Если это условие выполняется, выполнить этот блок кода. Если условие не выполняется, не выполнять этот блок кода».
Создайте новый проект с именем branches в вашем каталоге projects, чтобы поэкспериментировать с выражением if. В файле 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, иногда называются ветвями (arms), подобно ветвям в выражениях match, которые мы обсуждали в разделе «Сравнение догадки с секретным числом» главы 2.
Опционально мы также можем включить выражение else, что мы и сделали здесь, чтобы дать программе альтернативный блок кода для выполнения, если условие оценивается как false. Если вы не предоставите выражение else и условие будет false, программа просто пропустит блок 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 на значение, которое делает условие false, чтобы увидеть, что произойдёт:
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, например, мы можем изменить выражение if на следующее:
Имя файла: 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 по очереди и выполняет первое тело, для которого условие оценивается как true. Обратите внимание, что хотя 6 делится на 2, мы не видим вывод number is divisible by 2, и мы также не видим текст number is not divisible by 4, 3, or 2 из блока else. Это потому, что Rust выполняет только блок для первого true условия, и как только находит одно, он даже не проверяет остальные.
Использование слишком большого количества выражений else if может загромождать ваш код, поэтому если у вас их больше одного, вы можете захотеть рефакторить ваш код. Глава 6 описывает мощную конструкцию ветвления Rust под названием match для таких случаев.
Использование if в инструкции let
Поскольку if является выражением, мы можем использовать его в правой части инструкции let, чтобы присвоить результат переменной, как в Листинге 3-2.
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); }
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 должен знать на этапе компиляции, каким типом definitively является переменная number. Знание типа number позволяет компилятору проверить, что тип действителен везде, где мы используем number. Rust не смог бы этого сделать, если бы тип 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.
Вы можете или не увидеть слово again! напечатанным после ^C, в зависимости от того, где находился код в цикле, когда он получил сигнал прерывания.
К счастью, Rust также предоставляет способ выйти из цикла с помощью кода. Вы можете разместить ключевое слово 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 для хранения значения, возвращаемого из цикла. На каждой итерации цикла мы добавляем 1 к переменной counter, а затем проверяем, равен ли counter 10. Когда это так, мы используем ключевое слово break со значением counter * 2. После цикла мы используем точку с запятой, чтобы завершить оператор, который присваивает значение result. Наконец, мы печатаем значение в result, которое в данном случае равно 20.
Вы также можете использовать return внутри цикла. Хотя break выходит только из текущего цикла, return всегда выходит из текущей функции.
Примечание: точка с запятой после
break counter * 2технически необязательна.breakочень похож наreturnв том смысле, что оба могут необязательно принимать выражение в качестве аргумента, оба вызывают изменение потока управления. Код послеbreakилиreturnникогда не выполняется, поэтому компилятор Rust рассматривает выражение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
Программе часто нужно оценивать условие внутри цикла. Пока условие true, цикл выполняется. Когда условие перестаёт быть true, программа вызывает break, останавливая цикл. Возможно реализовать поведение, подобное этому, используя комбинацию loop, if, else и break; вы можете попробовать это сейчас в программе, если хотите. Однако этот шаблон настолько распространён, что Rust имеет встроенную языковую конструкцию для него, называемую циклом while. В Листинге 3-3 мы используем while, чтобы цикл программы выполнился три раза, считая вниз каждый раз, а затем, после цикла, напечатать сообщение и выйти.
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); }
while для выполнения кода, пока условие оценивается как trueЭта конструкция устраняет много вложенности, которая была бы необходимой, если бы вы использовали loop, if, else и break, и она яснее. Пока условие оценивается как true, код выполняется; в противном случае он выходит из цикла.
Цикл по коллекции с помощью for
Вы также можете использовать конструкцию while для перебора элементов коллекции, такой как массив. Например, цикл в Листинге 3-4 печатает каждый элемент в массиве a.
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("the value is: {}", a[index]); index += 1; } }
whileЗдесь код считает вверх по элементам в массиве. Он начинается с индекса 0, а затем циклится, пока не достигнет конечного индекса в массиве (то есть, когда index < 5 больше не true). Запуск этого кода выведет каждый элемент в массиве:
$ 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 и выполнять некоторый код для каждого элемента в коллекции. Цикл for выглядит как код в Листинге 3-5.
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } }
forКогда мы запускаем этот код, мы увидим тот же вывод, что и в Листинге 3-4. Что более важно, мы теперь повысили безопасность кода и устранили шанс ошибок, которые могли бы возникнуть из-за выхода за конец массива или недостаточного прохода и пропуска некоторых элементов. Машинный код, сгенерированный из циклов for, также может быть более эффективным, потому что индекс не нужно сравнивать с длиной массива на каждой итерации.
Используя цикл for, вам не нужно было бы помнить изменять любой другой код, если вы изменили количество значений в массиве, как это было бы с методом, использованным в Листинге 3-4.
Безопасность и краткость циклов for делают их наиболее часто используемой конструкцией циклов в Rust. Даже в ситуациях, когда вы хотите выполнить некоторый код определённое количество раз, как в примере обратного отсчёта, который использовал цикл while в Листинге 3-3, большинство Rustaceans использовали бы цикл for. Способ сделать это — использовать Range, предоставляемый стандартной библиотекой, который генерирует все числа в последовательности, начиная с одного числа и заканчивая перед другим числом.
Вот как будет выглядеть обратный отсчёт с использованием цикла for и другого метода, о котором мы ещё не говорили, rev, для обращения диапазона:
Имя файла: src/main.rs
fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }
Этот код немного приятнее, не так ли?
Резюме
Вы справились! Эта глава была объёмной: вы узнали о переменных, скалярных и составных типах данных, функциях, комментариях, выражениях if и циклах! Чтобы попрактиковаться в концепциях, обсуждённых в этой главе, попробуйте создать программы для выполнения следующих задач:
- Преобразование температур между Фаренгейтом и Цельсием.
- Генерация n-го числа Фибоначчи.
- Печать текста рождественской колядки «Двенадцать дней Рождества», используя повторение в песне.
Когда вы будете готовы двигаться дальше, мы поговорим о концепции в Rust, которая не обычно существует в других языках программирования: владении.