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

Здатність виконувати чи ні певний код залежно від того, чи умова істинна (true), чи повторити певний код кілька разів, доки умова true - базові будівельні елементи коду у більшості мов програмування. Найпоширеніші конструкції, що дозволяють вам управляти потоком виконання коду на Rust є вирази if та цикли.

Вирази if

Вираз if дозволяє розгалужувати код у залежності від умов. Ви задаєте умову, а потім вказуєте: “Якщо цю умову дотримано, запустити цей блок коду. Якщо ж умову не дотримано, не запускай цей блок коду”.

Створіть новий проект з назвою branches у вашій теці projects для вправ із виразом if. У файл 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. Ми розміщуємо блок коду, який треба виконати, якщо умова true, одразу після умови в фігурних дужках. Блоки коду, прив'язані до умов у виразах if, іноді звуть рукавами, так само як рукави у виразах match, що ми обговорювали у підрозділі Порівняння здогадки з таємним числом Розділу 2.

Також можна додати необов'язковий вираз else, як ми зробили тут, щоб надати програмі альтернативний блок коду для виконання, якщо умова виявиться false. Якщо ви не надасте виразу else, а умова буде false, програма просто пропустить блок 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 на таке, що зробить умову хибною, і подивитися, що станеться:

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

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

Помилка показує, що Rust очікував bool, але виявив ціле число. Rust не буде автоматично намагатися перетворити небулівські типи в булівський, на відміну від таких мов, як Ruby чи JavaScript. Ви маєте завжди явно надавати виразу 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 чи 2 з блоку else - бо Rust виконає тільки той блок, в якого першого умова буде true, а знайшовши його, не перевіряє всю решту умов.

Забагато виразів else if можуть захарастити ваш код, тому, якщо вам треба більш ніж одна така конструкція, цілком можливо, що знадобиться рефакторизувати ваш код. У Розділі 6 описана потужна конструкція мови Rust для розгалуження, що зветься match, для таких випадків.

Використання if в інструкції let

Because if is an expression, we can use it on the right side of a let statement to assign the outcome to a variable, as in 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: Assigning the result of an if expression to a variable

Змінна 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

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

Вираз у блоці if обчислюється у ціле число, а вираз у блоці else обчислюється у стрічку. Це не працює, оскільки змінна мусить мати лише один тип. Rust має точно знати під час компіляції тип змінної 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, а потім перевіряємо, чи дорівнює 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; вийде з зовнішнього циклу. Цей код виведе:

$ cargo run
   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

Програмі часто потрібно обчислювати умову в циклі. Доки умова true, цикл виконується. Коли умова припиняє бути true, програма викликає 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, і вона зрозуміліша. Поки умова 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;
    }
}

Блок коду 3-4: перебір елементів колекції за допомогою циклу while

Тут код перелічує всі елементи в масиві. Він починає з індексу 0, а потім повторює, доки не досягне останнього індексу масиву (тобто коли index < 5 вже не буде true). Виконання цього коду виведе всі елементи масиву:

$ 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 виглядає так, як показано в Блоці коду 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: Looping through each element of a collection using a for loop

Запустивши цей код, ми побачимо такий самий вивід, як і в Блоці коду 3-4. Що важливіше, ми збільшили безпеку коду та усунули можливість помилок - тепер неможливо, що код перейде за кінець масиву чи завершиться зарано, пропустивши кілька значень.

При використанні циклу for вам не треба пам'ятати, що треба змінити якийсь інший код, якщо ви змінили кількість значень у масиві, як це потрібно за методу, застосованого в Блоці коду 3-4.

Безпечність і лаконічність циклів for робить їх найпоширенішою конструкцією циклів у Rust. Навіть у ситуаціях, де треба виконати певний код визначену кількість разів, як у прикладі відліком в циклі while з Блоку коду 3-3, більшість растацеанців скористаються циклом for. Для цього треба буде скористатися типом Range ("діапазон"), який надається стандартною бібліотекою і генерує послідовно всі числа, починаючи з одного і закінчуючись перед іншим.

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

Файл: src/main.rs

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

Виглядає трохи краще, правда ж?

Підсумок

Нарешті закінчили! Це був величенький розділ: ви вивчили змінні, скалярні та складені типи даних, функції, коментарі, вирази if, та ще цикли! Якщо ви хочете повправлятися з концепціями, обговореними у цьому розділі, спробуйте написати програми, що роблять таке:

  • Конвертує температуру між шкалами Фаренгейта та Цельсія.
  • Обчислює n-е число Фібоначчі.
  • Виводить слова англійської різдвяної пісні "Дванадцять днів Різдва" з використанням повторень у пісні (якщо хочете - можете спробувати вивести казку "Ріпка").

Коли будете готові продовжувати, ми поговоримо про концепцію мови Rust, якої немає серед поширених в інших мовах програмування - володіння. ch02-00-guessing-game-tutorial.html#comparing-the-guess-to-the-secret-number ch02-00-guessing-game-tutorial.html#quitting-after-a-correct-guess