Основы управления потоком в Rust

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Control Flow. Этот лаба является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

В этом лабе мы сосредоточимся на управлении потоком в Rust, что включает в себя использование if-выражений и циклов для выполнения кода на основе условий и для повторения кода, пока условие истинно.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") subgraph Lab Skills rust/variable_declarations -.-> lab-100391{{"Основы управления потоком в Rust"}} rust/mutable_variables -.-> lab-100391{{"Основы управления потоком в Rust"}} rust/boolean_type -.-> lab-100391{{"Основы управления потоком в Rust"}} rust/string_type -.-> lab-100391{{"Основы управления потоком в Rust"}} rust/for_loop -.-> lab-100391{{"Основы управления потоком в Rust"}} rust/function_syntax -.-> lab-100391{{"Основы управления потоком в Rust"}} rust/expressions_statements -.-> lab-100391{{"Основы управления потоком в Rust"}} end

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

Возможность выполнять некоторый код в зависимости от того, является ли условие true, и повторять некоторый код, пока условие истинно, являются основными строительными блоками в большинстве языков программирования. Самые распространенные конструкции, которые позволяют вам контролировать ход выполнения кода Rust, — это if-выражения и циклы.

if-выражения

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

Создайте новый проект под названием branches в вашей директории project, чтобы изучить if-выражение. В файле src/main.rs введите следующее:

cd ~/project
cargo new branches

Имя файла: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("условие истинно");
    } else {
        println!("условие ложно");
    }
}

Все if-выражения начинаются с ключевого слова if, за которым следует условие. В этом случае условие проверяет, имеет ли переменная number значение, меньшее 5. Мы помещаем блок кода для выполнения, если условие истинно, сразу после условия внутри фигурных скобок. Блоки кода, связанные с условиями в if-выражениях, иногда называются ветвями, точно так же, как ветви в match-выражениях, о которых мы говорили в разделе "Сравнение предположения с секретным числом".

По желанию мы также можем включить 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`
условие истинно

Попробуем изменить значение number на значение, которое делает условие ложным, чтобы посмотреть, что произойдет:

    let number = 7;

Запустите программу снова и посмотрите на вывод:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
условие ложно

Также стоит отметить, что условие в этом коде должно быть bool. Если условие не является bool, мы получим ошибку. Например, попробуйте запустить следующий код:

Имя файла: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("число было равно трем");
    }
}

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

Ошибка указывает на то, что Rust ожидал bool, но получил целое число. В отличие от языков, таких как Ruby и JavaScript, Rust не будет автоматически пытаться преобразовывать небулевы типы в булево. Вы должны быть явными и всегда предоставлять if булево значение в качестве условия. Например, если мы хотим, чтобы блок кода if выполнялся только тогда, когда число не равно 0, мы можем изменить if-выражение на следующее:

Имя файла: src/main.rs

fn main() {
    let number = 3;

    if number!= 0 {
        println!("число было не равным нулю");
    }
}

Запуск этого кода выведет число было не равным нулю.

Обработка нескольких условий с else if

Вы можете использовать несколько условий, комбинируя if и else в else if-выражении. Например:

Имя файла: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("число делится на 4");
    } else if number % 3 == 0 {
        println!("число делится на 3");
    } else if number % 2 == 0 {
        println!("число делится на 2");
    } else {
        println!("число не делится на 4, 3 или 2");
    }
}

Эта программа имеет четыре возможных пути выполнения. После запуска ее вы должны увидеть следующий вывод:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
число делится на 3

Когда эта программа выполняется, она依次检查每个if表达式,并执行第一个条件计算结果为true的主体。请注意,尽管6能被2整除,但我们看不到输出“число делится на 2”,也看不到else块中的“число не делится на 4, 3 или 2”文本。这是因为Rust只会执行第一个true条件的块,一旦找到一个,它甚至不会检查其余的条件。

使用过多的else if表达式会使代码变得杂乱,因此如果您有多个这样的表达式,可能需要重构代码。第6章介绍了一种强大的Rust分支构造,称为match,适用于这些情况。

Использование if в let-выражении

Поскольку if является выражением, мы можем использовать его справа от let-выражения для присвоения результата переменной, как показано в Listing 3-2.

Имя файла: src/main.rs

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}

Listing 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, должны быть одного и того же типа; в Listing 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

Выражение в блоке 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 внутри цикла, чтобы сообщить программе, когда остановить выполнение цикла. Подумайте, мы делали это в игре угадывания в разделе "Завершение программы после правильного ответа", чтобы выйти из программы, когда пользователь угадал правильное число и выиграл игру.

Мы также использовали 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, чтобы сохранить значение, возвращаемое из цикла. На каждой итерации цикла мы увеличиваем переменную counter на 1, а затем проверяем, равно ли значение counter 10. Когда это так, мы используем ключевое слово 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; выйдёт из внешнего цикла. Этот код выводит:

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

Имя файла: src/main.rs

fn main() {
    let mut number = 3;

    while number!= 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

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

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

Цикл по коллекции с помощью for

Вы можете выбрать использовать конструкцию while для перебора элементов коллекции, такой как массив. Например, цикл в Listing 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;
    }
}

Listing 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, цикл остановится перед попыткой получить шестое значение из массива.

Однако этот подход подвержен ошибкам; мы можем привести программу к панике, если значение индекса или условие проверки будет неверным. Например, если вы измените определение массива a на четыре элемента, но забыете обновить условие до while index < 4, код будет приводить к панике. Кроме того, этот подход медленный, потому что компилятор добавляет код времени выполнения для выполнения условной проверки того, находится ли индекс внутри границ массива на каждой итерации цикла.

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

Имя файла: src/main.rs

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

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

Listing 3-5: Цикл по каждому элементу коллекции с использованием цикла for

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

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

Безопасность и компактность циклов for делают их наиболее часто используемой конструкцией цикла в Rust. Даже в ситуациях, когда вы хотите выполнить некоторый код определенное количество раз, как в примере с обратным отсчетом, который использовал цикл while в Listing 3-3, большинство Rustaceans предпочтут использовать цикл for. Способ сделать это - использовать Range, предоставляемый стандартной библиотекой, который генерирует все числа последовательно, начиная с одного числа и заканчивая перед другим числом.

Вот, как бы выглядел обратный отсчет с использованием цикла for и другого метода, о котором мы еще не говорили, rev, чтобы перевернуть диапазон:

Имя файла: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

Этот код выглядит лучше, не так ли?

Резюме

Поздравляем! Вы завершили лабораторную работу по управлению потоком. Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.