Управление потоком выполнения
Возможность выполнять некоторый код в зависимости от того, является ли условие
true, и возможность многократно выполнять некоторый код, пока условие
остаётся true, — базовые строительные блоки большинства языков
программирования. Самые распространённые конструкции, позволяющие управлять
потоком выполнения кода 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, иногда называют ветвями (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 должен точно знать во время компиляции, какой тип имеет
переменная 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 и continue применяются к самому
внутреннему циклу в этой точке. При желании можно указать для цикла метку
цикла, которую затем можно использовать с 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, цикл остановится до попытки
получить шестое значение из массива.
Однако этот подход подвержен ошибкам: мы можем вызвать panic в программе, если
значение индекса или проверяемое условие неверны. Например, если вы измените
определение массива a, чтобы в нём было четыре элемента, но забудете обновить
условие на while index < 4, код вызовет panic. Кроме того, это медленно,
потому что компилятор добавляет код времени выполнения для условной проверки
того, находится ли индекс в границах массива, на каждой итерации цикла.
В качестве более краткой альтернативы можно использовать цикл 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, большинство разработчиков Rust использовали бы цикл
for. Для этого можно использовать Range, предоставляемый стандартной
библиотекой: он генерирует все числа в последовательности, начиная с одного
числа и заканчивая перед другим, не включая его.
Вот как выглядел бы обратный отсчёт с использованием цикла for и другого
метода, о котором мы ещё не говорили, rev, чтобы развернуть диапазон:
Имя файла: src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
Этот код немного приятнее, не правда ли?
Итоги
Вы справились! Это была объёмная глава: вы узнали о переменных, скалярных
и составных типах данных, функциях, комментариях, выражениях if и циклах!
Чтобы попрактиковаться с концепциями, обсуждёнными в этой главе, попробуйте
написать программы, которые делают следующее:
- Преобразуют температуру между шкалами Фаренгейта и Цельсия.
- Генерируют n-е число Фибоначчи.
- Печатают текст рождественской песни «Двенадцать дней Рождества», используя повторения в песне.
Когда будете готовы двигаться дальше, мы поговорим о концепции Rust, которая обычно не существует в других языках программирования: о владении.