Главная / Курсы / Rust / Условия и циклы

Глава 3. Условия и циклы

Условия и циклы являются базовыми конструкциями большинства языков программирования. В Rust есть условное выражение if и три вида циклов: loop, while и for. В Rust блок кода является выражением и может быть присвоен переменной. Условия и циклы также являются выражениями.

Пример ниже демонстрирует эту особенность языка. В нем вычисляется первое число Фибоначчи, превышающее 100:

let mut cur = 1;
let mut next = 2;
let over100 = loop {
if next > 100 {
break next;
}
let new = cur + next;
cur = next;
next = new;
};
println!("First Fibonacci number over 100: {over100}");

Рассчитанное число Фибоначчи возвращается из выражения loop с помощью оператора break и сохраняется в переменной over100. В результате будет напечатано:

First Fibonacci number over 100: 144

Выражение if

Синтаксис выражения if cхож c синтаксисом условий в других языках программирования:

if <condition 1> {
<branch expression 1>
} [else if <condition 2> {
<branch expression 2>
}]
...
[else if <condition N> {
<branch expression N>
}] [else {
<branch expression N + 1>
}]

Если значение выражения condition 1 истинно, то выполнится первая ветка branch expression 1. В противном случае программа перейдет к проверке выражения condition 2. Если второе условное выражение ложно, то программа перейдет к проверке следующего. И так до конца, пока не закончатся все ветки выражения if.

Последней веткой может быть блок else, который выполнится, если условные выражения в предыдущих ветках окажутся ложными. Все блоки выражения if, кроме первого, необязательны и могут быть опущены.

Поскольку if является выражением, то все его ветки должны возвращать значения одного и того же типа. Блок каждой ветки может состоять из нескольких выражений, последнее выражение в блоке является результирующим. Однако если последнее выражение в блоке оканчивается точкой с запятой ;, то результирующим значением будет пустой кортеж ().

Функция min_divider() для переданного параметра вычисляет минимальный делитель меньше 10:

fn min_divider(value: u32) -> u32 {
if value % 2 == 0 {
2
} else if value % 3 == 0 {
3
} else if value % 5 == 0 {
5
} else if value % 7 == 0 {
7
} else {
1
}
}

Для входных значений 6, 49, 9, 35, 11 функция вернет: 2, 7, 3, 5, 1. В функции min_divider() отсутствует явное указание возвращаемого значения. По правилам языка ключевое слово return можно опустить, а значение последнего выражения будет результатом выполнения функции. В min_divider() это условное выражение, определяющее делитель.

Нужно реализовать функцию calc_season_number(), вычисляющую порядковый номер времени года (подробнее о функциях будет рассказано в следующей главе).

  • "Spring" ==> 1
  • "Summer" ==> 2
  • "Autumn" ==> 3
  • "Winter" ==> 4
  • 0 (ноль), если время года не удалось распознать

На вход функция получает константную ссылку на символьный литерал season_name, а возвращает 32-битное беззнаковое целое:

fn calc_season_number(season_name: &str) -> u32 {
// добавьте реализацию функции
}

Необходимо использовать выражение if / else if / else. В каждой ветке условия нужно сравнить season_name с одним названием времени года. При выполнении условия ветки нужно возвращать соответствующий номер времени года. Условное выражение следует завершить веткой else, которая будет возвращать 0.

fn calc_season_number(season_name: &str) -> u32 { if season_name == "Spring" { 1 } else if season_name == "Summer" { 2 } else if season_name == "Autumn" { 3 } else if season_name == "Winter" { 4 } else { 0 } }
Задача # 1

Важный момент: в условных выражениях необходимо использовать логический тип. Rust — строго типизированный язык и не производит неявных преобразований в логический тип из какого-то другого.

let x = 11;
if x % 2 {
println!("The number {} is odd.", x);
}

Данный пример не скомпилируется, так как в условии ожидается тип bool, а не целочисленный:

error[E0308]: mismatched types
 --> src\main.rs:3:8
  |
3 |     if x % 2 {
  |        ^^^^^ expected `bool`, found integer

Для исправления ошибки достаточно добавить проверку на неравенство нулю x % 2 != 0, и код станет работать как ожидается.

В условных выражениях можно использовать операции сравнения и логические операции. К операциям сравнения относятся:

  • == — равно, 135 == 135
  • != — не равно, 135 != 531
  • > — больше, 24.5 > 24.0
  • < — меньше, 24.5 < 24.6
  • >= — больше или равно, 100 >= 99 и 100 >= 100
  • <= — меньше или равно, -100 <= -99 и -100 <= -100

Логические операции это: отрицание !, логическое «И» (конъюнкция) &&, логическое «ИЛИ» (дизъюнкция) ||. Операции && и || называют ленивыми, так как правый операнд вычисляется только тогда, когда значения левого операнда недостаточно для определения результата выражения:

  • && — вычисляет свой правый операнд только тогда, когда левый операнд принимает значение true,
  • || — вычисляет свой правый операнд только тогда, когда левый операнд принимает значение false.

Так как if является выражением, то можно его использовать для инициализации переменных:

let <variable name> = if <condition> { <expression on true> } else { <expression on false> };

Такая запись напоминает тернарный оператор из других языков программирования, но обладает более широкими возможностями. Допускается использование блоков else if и составных выражений, например:

let k = 12;
let kaboom = if k % 2 == 0 {
if k % 3 == 0 {
"kaboom"
} else {
"ka"
}
} else if k % 3 == 0 {
"boom"
} else {
"nope"
};
println!("{k} - {kaboom}");

Для четных чисел данное выражение if будет возвращать "ka", для чисел кратных 3 — "boom", для четных и кратных 3 — "kaboom", для всех остальных чисел — "nope". Поскольку 12 кратно 2 и 3, то результатом выражения будет "12 — kaboom".

Обратите внимание, что после значения, которое может быть результатом выражения if, нет точки с запятой ;. Именно ее отсутствие указывает, что значение является результатом выражения в данной ветке условия.

Необходимо написать функцию max(), возвращающую максимальное значение из двух заданных first и second.

fn max(first: i64, second: i64) -> i64 {
// добавьте реализацию функции
}

Необходимо сравнить first с second и вернуть большее в соответствующей ветке условия.

fn max(first: i64, second: i64) -> i64 { if first > second { first } else { second } }
Задача # 2

В стандартной библиотеке уже имеется обобщенная версия функции max(), пригодная для разных типов данных: std::cmp::max<T>(v1: T, v2: T) -> T. Поэтому в продуктовой разработке реализовывать такую функцию самостоятельно не требуется.

Циклы

В языке Rust есть три вида циклов: for, while и loop. Любой цикл можно прервать, использовав ключевое слово break, либо продолжить итерирование, пропустив какую-то часть тела цикла с помощью ключевого слова continue. Ключевое слово return также прервет выполнение цикла и функции, в которой этот цикл находится.

Выражение for

Выражение for используется для последовательного перебора элементов коллекции. Синтаксис for:

for <variable name> in <sequence of values> {
[body]
}

В этом примере последовательно печатаются все элементы массива grade_array:

let grade_array = ["trainee", "junior", "middle", "senior"];

for grade in grade_array {
println!("{grade}");
}

Ожидаемый вывод:

trainee
junior
middle
senior

Цикл for также позволяет итерироваться по заданному диапазону чисел, например, с 0 до 9 включительно:

for num in 0..10 {
print!("{num} ");
}

Будет напечатано:

0 1 2 3 4 5 6 7 8 9 

В данном примере функция sum_positive() вычисляет сумму всех положительных элементов заданного массива:

fn sum_positive(numbers: &[i32]) -> usize {
let mut sum: usize = 0;
for num in numbers {
sum += if *num > 0 { *num as usize } else { 0 };
}
return sum;
}

Стоит обратить внимание, что переменная num, используемая в итерации по массиву, является ссылкой &i32, а не значением типа i32. Поэтому для получения значения текущего элемента применяется оператор разыменования ссылки *.

При вычислении суммы происходит преобразование типа i32 в usize с помощью выражения *num as usize. Это необходимо, так как Rust строго типизированный язык и не позволяет в арифметических операциях использовать разные целые типы.

Требуется реализовать функцию count(), возвращающую количество искомых desired элементов в массиве numbers. Если в массиве не найдено ни одного указанного числа, то возвращается 0.


fn count(numbers: &[i32], desired: i32) -> usize {
// добавьте реализацию функции
}

Необходимо объявить счетчик, проинициализировав его 0. В цикле for обойти все элементы массива numbers. Увеличить значение счетчика на 1 для каждого элемента, равного desired. Для возврата значения из функции достаточно на последней строке написать название переменной счетчика без точки с запятой.

fn count(numbers: &[i32], desired: i32) -> usize { let mut found: usize = 0; for num in numbers { found += if *num == desired { 1 } else { 0 } } found }
Задача # 3

Выражение while

Выражение while выполняется до тех пор, пока его условие истинно. Синтаксис while:

while <condition> {
[body]
}

Цикл while полезен в тех случаях, когда заранее неизвестно число итераций. В этом примере в заданном наборе чисел находятся все монотонно возрастающие последовательности. Для найденных последовательностей печатается диапазон и содержимое:

fn find_end(sequence: &[u64], start: usize) -> usize {
let mut pos = start;
let len = sequence.len();
while pos + 1 < len && sequence[pos] < sequence[pos + 1] {
pos += 1;
}
pos + 1
}

fn main() {
let seq = [6, 1, 2, 4, 3, 5, 7, 9, 11, 8, 16];
println!("initial: {:?}", seq);
let mut pos = 0;
let mut end = find_end(&seq, pos);

while pos < seq.len() {
let slice = &seq[pos..end];
println!("{}..{}: {:?}", pos, end, slice);
pos = end;
end = find_end(&seq, pos);
}
}

Функция find_end() вычисляет конец монотонно возрастающей последовательности, начиная с позиции start. В строке let slice = &seq[pos..end] формируется срез (слайс) с pos до end-1 включительно. Для получения длины среза используется метод len(). Подробнее о срезах будет рассказано в следующих главах.

Тип переменных pos, end и len не объявлен явно, он вычисляется во время компиляции и определяется как usize.

Ожидаемый вывод:

initial: [6, 1, 2, 4, 3, 5, 7, 9, 11, 8, 16]
0..1: [6]
1..4: [1, 2, 4]
4..9: [3, 5, 7, 9, 11]
9..11: [8, 16]

Требуется реализовать функцию validate(), проверяющую корректность входящего массива байт. Массив байт содержит один или нескольких пакетов, последовательно расположенных друг за другом. Каждый пакет состоит из трех частей: типа, размера и блока данных. Тип пакета имеет длину 1 байт и может принимать значения от 1 до 15. Размер данных имеет длину 1 байт и может принимать любое допустимое значение для u8 (0..255). Количество байт в блоке данных должно быть равным указанному размеру.

Функция validate() проверяет, что каждый пакет, содержащийся во входном массиве, корректен. Если найден некорректный пакет или входной массив пустой, то функция возвращает false. Если все пакеты массива корректны, то validate() возвращает true.

Пример 1. Входной массив [7, 4, 10, 20, 30, 40] содержит пакет типа 7, размер блока данных — 4, блок данных — [10, 20, 30, 40]. Для этого массива функция validate() должна вернуть true.

Пример 2. Входной массив [2, 1, 100, 5, 8, 11, 10, 13] содержит два пакета. Первый пакет имеет тип 2, размер блока данных — 1, блок данных — [100]. Второй пакет имеет тип 5, размер блока данных — 8 и блок данных — [11, 10, 13]. Данные второго пакета неполные, 3 байта вместо ожидаемых 8, поэтому validate() должна вернуть false.

fn validate(data: &[u8]) -> bool {
// добавьте реализацию функции
}

Для обхода массива data удобнее использовать цикл while. На каждой итерации цикла требуется проверить корректность текущего пакета. Сначала нужно убедится, что пакет имеет длину не меньше 2 байт. Потом проверить, что байт с типом пакета имеет корректное значение. В конце итерации следует проверить, что количество оставшихся элементов в массиве не меньше размера текущего пакета. Вычислить начало следующего пакета можно с помощью выражения data[pos + 1] as usize + 2. Где pos — начало текущего пакета, data[pos + 1] — размер данных пакета, а data[pos + 1] as usize — выражение преобразования из u8 в usize.

fn validate(data: &[u8]) -> bool { let mut pos = 0; let end = data.len(); while pos < end { if pos + 2 > end { return false; } let data_type: u8 = data[pos]; if data_type < 1 || data_type > 15 { return false; } pos += 1; let data_size = data[pos] as usize; pos += data_size + 1; if pos > end { return false; } } return end != 0; }
Задача # 4

Выражение loop

Выражение loop выполняется до тех пор, пока не встретится оператор прерывания цикла: break или return. Диспетчер сообщений (event loop), воркер потока — примеры, в которых loop будет полезен. Синтаксис:

loop {
[body]
}

Эта небольшая программа читает из консоли ввод пользователя и выводит на консоль прочитанное. Цикл повторяется до тех пор, пока вo введенной строке не встретится слово "exit" или "quit":

use std::io;

fn main() -> io::Result<()> {

loop {
let mut input = String::new();
let stdin = io::stdin();
stdin.read_line(&mut input)?;

if input.contains("exit") || input.contains("quit") {
break;
}
print!("You typed: {}", input);
}

Ok(())
}

В этом примере для работы с консолью используется модуль std::io. Функция main() возвращает пустой кортеж либо ошибку. В строке stdin.read_line(&mut input)? оператор ? распаковывает (unwrap) возвращаемое значение, в случае ошибки завершает работу функции, передавая ошибку вызывающей функции.

Выражение Ok(()) создает вариант перечисления io::Result<()> с пустым кортежем в качестве значения. Так как это последнее выражение в функции, то оно является возвращаемым значением.

Модуль — это логически связанный набор: функций, структур, типажей и других модулей. Модули составляют крейты. Так модуль std::io является частью крейта std и содержит внутри себя модуль std::io::prelude.

О крейтах, модулях, алгебраических типах и кортежах будет рассказано в других главах.

Заключение

  • В Rust есть условное выражение if и три вида циклов: loop, while и for.
  • Условия и циклы являются выражениями и могут быть присвоены переменной.
  • Условное выражение if может иметь более одной ветки else if и в конце одну ветку else.
  • В условных выражениях можно использовать только логический тип.
  • Rust имеет общепринятый набор операций сравнения: ==, !=, >, <, >=, <=. И логических операций: !, ||, &&.
  • Логические операции && и || являются ленивыми и вычисляются только тогда, когда значения левого операнда недостаточно.
  • Цикл for используется для последовательного перебора элементов коллекции.
  • Цикл while требуется в тех случаях, когда заранее неизвестно число итераций.
  • Цикл loop выполняется, пока не встретится break или return. Он полезен для организации бесконечных циклов.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!