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