Управление потоком выполнения
Выполнять код в зависимости от того, истинно ли условие, и повторять код,
пока условие истинно, — это базовые строительные блоки большинства языков
программирования. Чаще всего управлять потоком выполнения кода на 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.
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
должен ещё при компиляции точно знать, какого типа переменная 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 программа трижды проходит цикл,
каждый раз уменьшая счётчик, а после цикла выводит сообщение и завершается.
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
while: код выполняется, пока условие истинноЭта конструкция избавляет от лишней вложенности, которая понадобилась бы с
loop, if, else и break, и читается понятнее. Пока условие истинно, код
выполняется; как только станет ложным — цикл завершается.
Обход коллекции с 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 не перестанет
быть истинным). Код выведет все элементы массива:
$ 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.
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, большинство растацеанов
возьмут 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» («Двенадцать дней Рождества»), используя то, что куплеты в ней повторяются.
Когда будете готовы двигаться дальше, мы поговорим о понятии, которого в других языках программирования обычно нет, — о владении.