Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Керування потоком (Control Flow)

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

Вирази if (if Expressions)

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

Створіть новий проєкт під назвою branches у вашому каталозі projects, щоб дослідити вираз if. У файлі src/main.rs введіть таке:

Filename: 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, інколи називають гілками (arms), так само як гілки (arms) у виразах match, про які ми говорили в розділі “Порівняння припущення із секретним числом” у розділі 2.

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

Filename: 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 на такий:

Filename: 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 (Handling Multiple Conditions with else if)

Ви можете використовувати кілька умов, комбінуючи if і else у виразі else if. Наприклад:

Filename: 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 виконує лише блок для першої умови true, і, щойно знаходить таку, навіть не перевіряє решту.

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

Використання if в операторі let (Using if in a let Statement)

Оскільки if є виразом, ми можемо використовувати його в правій частині оператора let, щоб присвоїти результат змінній, як у Лістингу (Listing) 3-2.

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, мають бути одного й того самого типу; у Лістингу (Listing) 3-2 результати як гілки if, так і гілки else були цілими числами i32. Якщо типи не збігаються, як у такому прикладі, ми отримаємо помилку:

Filename: 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 визначався лише під час виконання; компілятор був би складнішим і надавав би менше гарантій щодо коду, якби йому доводилося відстежувати кілька гіпотетичних типів для будь-якої змінної.

Повторення за допомогою циклів (Repetition with Loops)

Часто корисно виконувати блок коду більше ніж один раз. Для цього Rust надає кілька циклів, які проходитимуть по коду всередині тіла циклу до кінця, а потім одразу починатимуть знову з початку. Щоб експериментувати з циклами, створімо новий проєкт під назвою loops.

У Rust є три види циклів: loop, while і for. Спробуймо кожен із них.

Повторення коду за допомогою loop (Repeating Code with loop)

Ключове слово loop повідомляє Rust виконувати блок коду знову і знову або нескінченно, або доки ви явно не скажете йому зупинитися.

Як приклад, змініть файл src/main.rs у вашому каталозі loops так, щоб він виглядав ось так:

Filename: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

Коли ми запустимо цю програму, ми побачимо again!, надруковане знову і знову безперервно, поки ми не зупинимо програму вручну. Більшість терміналів підтримують комбінацію клавіш ctrl-C, щоб перервати програму, яка застрягла в безперервному циклі. Спробуйте:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

Символ ^C позначає місце, де ви натиснули ctrl-C.

Ви можете побачити, а можете й не побачити слово again!, надруковане після ^C, залежно від того, де код був у циклі, коли отримав сигнал переривання.

На щастя, Rust також надає спосіб вийти з циклу за допомогою коду. Ви можете розмістити ключове слово break всередині циклу, щоб сказати програмі, коли зупинити виконання циклу. Пригадайте, що ми зробили це в грі на вгадування в розділі “Вихід після правильного припущення” розділу 2, щоб вийти з програми, коли користувач виграв гру, вгадавши правильне число.

Ми також використовували continue у грі на вгадування, яке в циклі повідомляє програмі пропустити будь-який код, що залишився в цій ітерації циклу, і перейти до наступної ітерації.

Повернення значень із циклів (Returning Values from Loops)

Одним із способів використання 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.

Ви також можете return ізсередини циклу. Тоді як break лише виходить із поточного циклу, return завжди виходить із поточної функції.

Уточнення за допомогою міток циклів (Loop Labels to Disambiguate Between Multiple Loops)

Якщо у вас є цикли всередині циклів, 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 (Conditional Loops with while)

Програмі часто потрібно оцінювати умову всередині циклу. Поки умова є true, цикл виконується. Коли умова перестає бути true, програма викликає break, зупиняючи цикл. Можна реалізувати поведінку такого типу, використовуючи комбінацію loop, if, else і break; ви могли б спробувати це зараз у програмі, якщо хочете. Однак цей шаблон настільки поширений, що Rust має вбудовану мовну конструкцію для нього, яку називають циклом while. У Лістингу (Listing) 3-3 ми використовуємо while, щоб програма виконала цикл тричі, щоразу рахуючи вниз, а потім, після циклу, надрукувала повідомлення і вийшла.

fn main() {
    let mut number = 3;

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

        number -= 1;
    }

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

Ця конструкція усуває багато вкладеності, яка була б необхідна, якби ви використовували loop, if, else і break, і вона зрозуміліша. Поки умова обчислюється до true, код виконується; інакше він виходить із циклу.

Циклічний прохід по колекції за допомогою for (Looping Through a Collection with for)

Ви можете обрати використання конструкції while для проходу по елементах колекції, такої як масив. Наприклад, цикл у Лістингу (Listing) 3-4 друкує кожен елемент у масиві a.

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 перестає бути true). Запуск цього коду надрукує кожен елемент у масиві:

$ 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 виглядає як код у Лістингу (Listing) 3-5.

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

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

Коли ми запустимо цей код, ми побачимо такий самий вивід, як у Лістингу (Listing) 3-4. Що важливіше, тепер ми підвищили безпеку коду і усунули ризик помилок, які можуть виникнути через вихід за кінець масиву або через те, що ви не дійшли достатньо далеко і пропустили деякі елементи. Машинний код, згенерований з циклів for, також може бути ефективнішим, тому що індекс не потрібно порівнювати з довжиною масиву на кожній ітерації.

Використовуючи цикл for, вам не потрібно було б пам’ятати про зміну будь-якого іншого коду, якби ви змінили кількість значень у масиві, як це було б із методом, використаним у Лістингу (Listing) 3-4.

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

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

Filename: src/main.rs

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

Цей код трохи кращий, чи не так?

Підсумок (Summary)

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

  • Перетворювати температури між Фаренгейтом і Цельсієм.
  • Генерувати n-те число Фібоначчі.
  • Друкувати текст різдвяної колядки “The Twelve Days of Christmas”, використовуючи повторення в пісні.

Коли будете готові рухатися далі, ми поговоримо про концепцію в Rust, яка не поширена в інших мовах програмування: володіння (ownership).