Управление потоком
The ability to run some code depending on whether a condition is true and the ability to run some code repeatedly while a condition is true are basic building blocks in most programming languages. The most common constructs that let you control the flow of execution of Rust code are if expressions and loops.
Выражения if
Выражение if позволяет вам исполнять код в зависимости от истинности условий. Вы определяете условие исполнения, а потом используете if, чтобы указать программе: “Исполни этот код, если условие истинно; иначе — ничего не делай”.
Создайте новый проект в своей директории projects и назовите его branches. В нём мы будем изучать выражение if. Заполните файл src/main.rs этим кодом:
Файл: src/main.rs
fn main() {
let number = 3;
if number < 5 {
println!("условие оказалось истинно");
} else {
println!("условие оказалось ложно");
}
}
Все выражения if состоят с ключевого слова if и следующего за ним условия. В нашем случае, условие проверяет, меньше ли, чем 5, переменная number. В фигурных скобках, сразу после условия, мы размещаем код, который надо исполнить, если условие истинно. Блоки кода, связанные с условиями в выражениях if, иногда называются ветвями — аналогично ветвям в выражении match, которое мы обсуждали в разделе [“Сравнение догадки с загаданным числом”] (ch02-00-guessing-game-tutorial.html#Сравнение-догадки-с-загаданным-числом) Главы 2.
Это не обязательно, но мы можем добавить выражение else, которое указывает на код, который нужно запустить в случае, если условие оказалось ложным. Если вы не напишете выражение 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`
условие оказалось истинно
Изменим значение number на значение, которое сделает условие ложным:
fn main() {
let number = 7;
if number < 5 {
println!("условие оказалось истинно");
} else {
println!("условие оказалось ложно");
}
}
Снова запустите программу и взгляните на вывод:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
условие оказалось ложным
Стоит также отметить, что условие всегда должно иметь тип bool, иначе мы получим ошибку компиляции. Например, попробуем запустить следующий код:
Файл: src/main.rs
fn main() {
let number = 3;
if number {
println!("number было тройкой");
}
}
Условие при 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 условие, являющееся выражением, которое вычисляется в логическое значение. Если вы хотите запустить блок кода только если number не равно 0, нужно предоставить выражению if вот такое условие:
Файл: src/main.rs
fn main() {
let number = 3;
if number != 0 {
println!("number оказалось не равно нулю");
}
}
Запустив этот код, вы увидите текст number оказалось не равно нулю.
Обработка нескольких условий с помощью else if
Вы можете проверять несколько условий, объединив if и else в одно выражение else if. Например:
Файл: src/main.rs
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number делится на 4");
} else if number % 3 == 0 {
println!("number делится на 3");
} else if number % 2 == 0 {
println!("number делится на 2");
} else {
println!("number не делится ни на 4, ни на 3, ни на 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 делится 3
В этой программе происходит поочерёдная проверка каждого выражения if. Как только программа встретит условие, вычисляющееся в true, исполнится соответствующий блок кода. Обратите внимание, что хотя 6 делится на 2, мы не видим ни number делится на 2 (сообщение предпоследней ветви), ни number не делится ни на 4, ни на 3, ни на 2 (сообщение ветви else). Причина в том, что на первом же истинном условии проверка и останавливается — идущие далее условия не проверяются.
Использование большого количества выражений else if — верный способ сделать свой код запутанным и непонятным. Если вы используете больше одного такого оператора, возможно, вашей программе нужен рефакторинг. Глава 6 расскажет вам об операторе ветвления match, который отлично подойдёт для подобных случаев.
Использование if в инструкции let
Поскольку if — это выражение, мы можем использовать его в правой части инструкции let, чтобы условием управлять тем, что присваивается переменной. Посмотрите на Листинг 3-2.
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("Значение number: {number}");
}
if expression to a variableПеременная 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`
Значение number: 5
Помните, что 1) блоки кода вычисляются в значение последнего их выражения и 2) числа сами по себе тоже являются выражениями. В нашем случае, значение всего выражения if зависит от того, какой исполнится блок кода. Из этого следует, что значение каждого блока кода должно иметь один и тот же тип. В Листинге 3-2 всё имеенно так: обе ветви if и ветвь else вычисляются в целое число типа i32. Если ветви будут вычисляться в значения разных типов (как показано в примере ниже), вы получите ошибку:
Файл: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "шесть" };
println!("Значение number: {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 { "шесть" };
| - ^^^^^^^ 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
The expression in the if block evaluates to an integer, and the expression in the else block evaluates to a string. This won’t work, because variables must have a single type, and Rust needs to know definitively at compile time what type the number variable is. Knowing the type of number lets the compiler verify the type is valid everywhere we use number. Rust wouldn’t be able to do that if the type of number was only determined at runtime; the compiler would be more complex and would make fewer guarantees about the code if it had to keep track of multiple hypothetical types for any variable.
Повторное исполнение кода с помощью циклов
Часто нужно исполнить некоторый объём кода больше, чем единожды. Для этого в Rust существуют несколько видов циклов, которые позволяют исполнить блок кода и затем вернуться к его началу. Чтобы опробовать циклы, создайте новый проект и назовите его loops.
В Rust есть три вида циклов: loop, while и for. Попробуем каждый из них.
Повторение кода с помощью loop
The loop keyword tells Rust to execute a block of code over and over again either forever or until you explicitly tell it to stop.
Замените код в файле src/main.rs в директории loops на код из примера ниже:
Файл: src/main.rs
fn main() {
loop {
println!("и ещё раз,");
}
}
When we run this program, we’ll see again! printed over and over continuously until we stop the program manually. Most terminals support the keyboard shortcut ctrl-C to interrupt a program that is stuck in a continual loop. Give it a try:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
и ещё раз,
и ещё раз,
и ещё раз,
и ещё раз,
^Cи ещё раз,
The symbol ^C represents where you pressed ctrl-C.
You may or may not see the word again! printed after the ^C, depending on where the code was in the loop when it received the interrupt signal.
Естественно, в Rust есть способ программно выйти из цикла. Если вы напишете ключевое слово break внутри цикла, то когда исполнение до него дойдёт, цикл завершится. К слову, мы уже его использовали: вспомните, как мы реализовали “Завершение игры после правильной догадки” в нашей игре в угадайку из Главы 2.
Мы также тогда использовали ключевое слово continue — оно указывает циклу пропустить исполнение оставшегося кода и сразу продолжить с новой итерации цикла.
Вычисление циклов в значения
One of the uses of a loop is to retry an operation you know might fail, such as checking whether a thread has completed its job. You might also need to pass the result of that operation out of the loop to the rest of your code. To do this, you can add the value you want returned after the break expression you use to stop the loop; that value will be returned out of the loop so that you can use it, as shown here:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("result равна {result}");
}
Before the loop, we declare a variable named counter and initialize it to 0. Then, we declare a variable named result to hold the value returned from the loop. On every iteration of the loop, we add 1 to the counter variable, and then check whether the counter is equal to 10. When it is, we use the break keyword with the value counter * 2. After the loop, we use a semicolon to end the statement that assigns the value to result. Finally, we print the value in result, which in this case is 20.
Вы также можете использовать в цикле ключевое слово return. В отличие от break (которое завершит исполнение только своего цикла), return завершит исполнение сразу всей функции.
Disambiguating with Loop Labels
Если вы работаете во вложенных циклах, 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!("Окончательное значение 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
Окончательное значение count = 2
Streamlining Conditional Loops with while
A program will often need to evaluate a condition within a loop. While the condition is true, the loop runs. When the condition ceases to be true, the program calls break, stopping the loop. It’s possible to implement behavior like this using a combination of loop, if, else, and break; you could try that now in a program, if you’d like. However, this pattern is so common that Rust has a built-in language construct for it, called a while loop. In Listing 3-3, we use while to loop the program three times, counting down each time, and then, after the loop, to print a message and exit.
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("ПОЕХАЛИ!!!");
}
while loop to run code while a condition evaluates to trueЭта конструкция серьёзно облегчает создание циклов с условием остановки: код получается проще и чище. Пока условие вычисляется в true, код запускается; иначе, цикл прерывается.
Перебор элементов коллекции с for
You can choose to use the while construct to loop over the elements of a collection, such as an array. For example, the loop in Listing 3-4 prints each element in the array a.
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("элемент: {}", a[index]);
index += 1;
}
}
while loopHere, the code counts up through the elements in the array. It starts at index 0 and then loops until it reaches the final index in the array (that is, when index < 5 is no longer true). Running this code will print every element in the array:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
элемент: 10
элемент: 20
элемент: 30
элемент: 40
элемент: 50
Все пять элементов массива появились на экране, как и ожидалось. Хотя переменная index и достигает в какой-то момент значения 5, цикл проверит условие на истинность и потому завершится раньше, чем произойдёт попытка получить шестой элемент массива.
However, this approach is error-prone; we could cause the program to panic if the index value or test condition is incorrect. For example, if you changed the definition of the a array to have four elements but forgot to update the condition to while index < 4, the code would panic. It’s also slow, because the compiler adds runtime code to perform the conditional check of whether the index is within the bounds of the array on every iteration through the loop.
Более лакончиным способом перебрать элементы коллекции является цикл for. Пример цикла for приведён в Листинге 3-5.
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("элемент: {element}");
}
}
for loopWhen we run this code, we’ll see the same output as in Listing 3-4. More importantly, we’ve now increased the safety of the code and eliminated the chance of bugs that might result from going beyond the end of the array or not going far enough and missing some items. Machine code generated from for loops can be more efficient as well because the index doesn’t need to be compared to the length of the array at every iteration.
С циклом for ваш код самостоятельно подстроится под изменения в коллекции, и вам не понадобится следить за двумя участками кода, как это приходилось бы делать в Листинге 3-4.“
Безопасность и простота циклов for делают его наиболее часто используемым циклом в Rust. Даже в случаях, когда вы хотите запустить некоторый код произвольное точное количество раз (например, как в цикле в Листинге 3-3), большинство программистов на Rust применят для этого цикл for. Если онкретнее, они воспользуются Range — структуре стандартной библиотеки, которая генерирует последовательность всех чисел между двумя границами (включая нижнюю границу, но не включая верхнюю).
Вот как будет выглядить обратный отсчёт с помощью цикла for и метода rev, переворачивающего генерируемую последовательность:
Файл: src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("ПОЕХАЛИ!!!");
}
Этот код чуть приятнее; не так ли?
Подведём итоги
You made it! This was a sizable chapter: You learned about variables, scalar and compound data types, functions, comments, if expressions, and loops! To practice with the concepts discussed in this chapter, try building programs to do the following:
- Конвертер температур из градусов Фаренгейта в Цельсия (и наоборот).
- Функция генерации _n_ого числа Фибоначчи.
- Программа, печатающая текст рождественской английской народной песни “The Twelve Days of Christmas”.
В следующей главе мы обсудим владение — концепцию Rust, которой обычно нет в других языках программирования.