Управление выполнением кода

Решение о том выполнить ту или иную часть кода в зависимости от условия, и решение о том продолжить или нет выполнение некоторого кода в цикле пока выполняется какое-то условие - задачи которые решаются в большинстве языков специализированными базовыми блоками. Наиболее общими конструкциями, позволяющими управлять выполнением кода в Rust являются выражения if и циклы.

Выражения if

Выражение if позволяет разделить ваш код на ветви и выполнять ту или иную ветвь кода в зависимости от условий. Вы предоставляете условие и затем пишите утверждение вида "если это условие выполняется/верное, выполнить данный блок кода; если не выполняется, не выполнять этот блок кода".

Чтобы изучить выражение if, создадим новый проект с названием 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. Блок кода, который мы хотим выполнить, если условие истинно, размещён сразу после условия в фигурных скобках. Блоки кода ассоциированные с условиями в выражении if иногда называют ветками/arms, подобно веткам в выражении match из секции “Сравнение предположения и загаданный номер” главы 2.

Опционально можно включить выражение else, которое мы используем в данном примере, чтобы предоставить программе альтернативный блок выполнения кода, выполняющийся при ложном условии. Если не написать выражение else и условие будет ложным, то программа просто пропустит блок if и перейдёт к выполнению кода размещённого дальше.

Результат работы программы:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [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 [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

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches`

To learn more, run the command again with --verbose.

Ошибка говорит, что Rust ожидал тип bool, но получил значение целочисленного типа. В отличии от других языков вроде Ruby и JavaScript, Rust не будет пытаться автоматически конвертировать не булевые типы в булевые. Необходимо быть явными и всегда предоставлять значение типа bool в выражение 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 [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

Во время выполнения, программа проверяет каждое выражение if по порядку и выполняет первый блок для которого условие вычисляется в истинное. Заметьте, что не смотря на то что 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);
}

Листинг 3-2: Присвоение результата if-выражения переменной при её инициализации

Переменная number будет привязана к значению, которое является результатом выражения if. Запустим код и посмотрим, что происходит:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [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

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches`

To learn more, run the command again with --verbose.

Выражение в блоке if вычисляется как целое число, а выражение в блоке else вычисляется как строка. Это не будет работать, потому что переменные должны иметь одинаковый тип. Rust должен знать во время компиляции, какой тип имеет переменная number, поэтому он может проверить во время компиляции, что её тип корректен везде, где мы используем number. Rust не сможет сделать этого, если тип number может быть определён только во время выполнения. Тогда компилятор был бы более сложным и давал бы меньше гарантий о коде, потому что должен был бы отслеживать несколько гипотетических типов для любой переменной.

Повторение выполнения кода с помощью циклов

Довольно часто бывает полезно выполнить блок кода более одного раза. Для такой задачи Rust предоставляет несколько разновидностей циклов (loops). Цикл выполняет код внутри тела цикла от начала тела и до конца, а затем немедленно возвращается обратно в начало своего тела. Для экспериментов создадим новый проект с именем 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) для выхода из программы, когда пользователь выиграл игру, угадав правильное число.

Возврат чисел из цикла

Одно из применений цикла 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. Если равен, то используется ключевое слово break со значением counter * 2. После цикла мы используем точку с запятой чтобы завершить выражение которое назначает значение переменной result. В итоге печатается значение из переменной result, которое в данном случае равно 20.

Циклы по условию 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!!!");
}

Листинг 3-3: Использует цикл while для выполнения кода, пока условие истинно

Данная конструкция устраняет множество вложенностей, которые понадобились бы при использовании loop, if, else и break и является более понятной. Пока условие является истинным, код выполняется; иначе происходит выход из цикла.

Цикл по элементам коллекции с помощью for

Можно использовать конструкцию while для прохода по элементам коллекции, например такой, как массив. Посмотрим на листинг 3-4.

Файл: 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;
    }
}

Листинг 3-4: Проход по элементам коллекции используя цикл while

Данный код проходит по всем элементам массива. Он начинается с индекса 0 и затем идёт далее, пока не достигнет последнего индекса массива (когда условие index < 5 больше не является истинным). Запуск данного кода печатает каждый элемента массива:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [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, то цикл прекратит выполнение до того, как мы попытаемся извлечь шестой несуществующий элемент из массива.

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

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

Файл: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

Листинг 3-5: Проход по всем элементам коллекции используя цикл for

При запуске данного кода, мы увидим вывод подобный листингу 3-4. Более важно то, что мы увеличили безопасность кода и уменьшили шанс появления ошибки, которая могла бы получится из-за выхода за границы массива или прохода коллекции не до конца и пропуска некоторых элементов.

Например, если обновить объявление массива a так чтобы он хранил четыре элемента в коде листинга 3-4, но забыть обновить условие while index < 4, то код завершится паникой. Используя цикл for вам не придётся помнить, что необходимо изменение любого другого кода, при изменении количества элементов в массиве.

Безопасность и краткость цикла 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 и циклами. Если хотите практиковаться с концепциями рассмотренными в данной главе, то попробуйте написать следующие программы:

  • конвертер температур из единиц Фаренгейта в единицы Цельсия,
  • Генератор чисел Фибоначчи.
  • генератор строк сказки "12 дней Рождества" использующий преимущество повторяющихся строк в сказке.

Если вы готовы двигаться далее, то в следующей главе мы расскажем о концепции языка Rust, которая отсутствует в других языках - это владение.