Управление потоком
Важнейшей частью большинства языков программирования являются операторы ветвления и циклы — конструкции, позволяющие запускать код, только если (или: пока) некоторое условие истинно. Наиболее распространёнными конструкциями, позволяющими вам управлять потоком исполнения программы на Rust, являются выражения if
и циклы.
Выражения 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}"); }
Переменная 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
Выражение в блоке if
вычисляется в целое число, а выражение блоке else
— в строку. Такое недопустимо, поскольку типы переменных должны быть постоянны и известны уже на этапе компиляции. Компилятор должен знать тип number
, чтобы иметь возможность проверить, корректно ли мы применяем переменную number
дальше в программе. Поддержка неопределённости типов сделала бы компилятор Rust значительно более сложным, лишила бы нас многих гарантий формальной корректности кода, и позволила бы писать крайне запутанные программы — даже когда мы того не хотим.
Повторное исполнение кода с помощью циклов
Часто нужно исполнить некоторый объём кода больше, чем единожды. Для этого в Rust существуют несколько видов циклов, которые позволяют исполнить блок кода и затем вернуться к его началу. Чтобы опробовать циклы, создайте новый проект и назовите его loops.
В Rust есть три вида циклов: loop
, while
и for
. Попробуем каждый из них.
Повторение кода с помощью loop
Ключевое слово loop
означает, что следующий за ним блок кода надо исполнять бесконечно, раз за разом — до тех пор, пока вы явно не прикажете циклу остановиться.
Замените код в файле src/main.rs в директории loops на код из примера ниже:
Файл: src/main.rs
fn main() {
loop {
println!("и ещё раз,");
}
}
Запустив эту программу, мы увидим строку и ещё раз,
, одну за другой, бесконечно. Мы можем остановить это, вручную прервав исполнение программы. Большинство консолей поддерживают сочетание клавиш 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`
и ещё раз,
и ещё раз,
и ещё раз,
и ещё раз,
^Cи ещё раз,
Символ ^C
означает, что вы нажали Ctrl-C. Возможно, вы не увидите строчку и ещё раз,
после ^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!("result равна {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!("Окончательное значение 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
Условные циклы с while
Часто бывает нужно исполнять некоторый код, пока истинно некоторое условие; когда же условие перестаёт быть истинным, программа прерывает повтор цикла. Описанный механизм вполне можно реализовать с помощью loop
, if
, else
и break
; можете попробовать это в качестве тренировки. Однако, в многих языках программирования подобного рода цикл уже реализован, и Rust — не исключение. В нём такой цикл называется while
. Программа в Листинге 3-3 использует while
для отсчёта трёх и, в конце, печатает сообщение, после чего завершается.
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("ПОЕХАЛИ!!!"); }
Эта конструкция серьёзно облегчает создание циклов с условием остановки: код получается проще и чище. Пока условие вычисляется в true
, код запускается; иначе, цикл прерывается.
Перебор элементов коллекции с for
Вы также можете использовать конструкцию while
, чтобы пройтись по элементам коллекций (например, массива). Например, цикл в Листинге 3-4 напечатает каждый элемент массива a
.
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("элемент: {}", a[index]); index += 1; } }
Это код поочерёдно проходится по каждому элементу массива. Он начинает с элемента по индексу 0
, и затем перебирает индексы до последнего (то есть, до тех пор, пока index < 5
не окажется false
). Запустив этот код, вы увидите список всех элементов массива:
$ 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
, цикл проверит условие на истинность и потому завершится раньше, чем произойдёт попытка получить шестой элемент массива.
Однако такой подход легко может привести к ошибке: можно легко получить панику из-за попытки получить элемент за пределами массива. Например, если вы оставите в массиве a
лишь четыре элемента, а обновить определение условного цикла до while index < 4
, программа вызовет панику. А ещё это очень медленно: обращение к элементам массива с помощью переменной вынуждает компилятор добавить тормозящие код проверки на выход за пределы массива — и такие проверки нужно будет проводить на каждой итерации; всё это совершенно точно является излишеством.
Более лакончиным способом перебрать элементы коллекции является цикл for
. Пример цикла for
приведён в Листинге 3-5.
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("элемент: {element}"); } }
Запустив этот код, мы увидим тот же вывод, что и в случае Листинга 3-4. Но куда более важно то, что мы повысили надёжность нашего кода, избавив его от возможных ошибок, связанных как с попытками получить элемент за пределами массива, так и с преждевременной остановкой перебора.
С циклом 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!("ПОЕХАЛИ!!!"); }
Этот код чуть приятнее; не так ли?
Подведём итоги
Вы проделали большую работу! Эта глава была внушительной: вы узнали о переменных, неделимых и составных типах, функциях, комментариях, условных операторах ветвления и о циклах! Чтобы отработать изученное, попробуйте написать следующие программы:
- Конвертер температур из градусов Фаренгейта в Цельсия (и наоборот).
- Функция генерации _n_ого числа Фибоначчи.
- Программа, печатающая текст рождественской английской народной песни "The Twelve Days of Christmas".
В следующей главе мы обсудим владение — концепцию Rust, которой обычно нет в других языках программирования.