Функції

Функції використовуються скрізь у коді на Rust. Ви вже бачили одну з найважливіших функцій у мові - функцію main, яка є точкою входу багатьох програм. Ви також бачили ключове слово fn, яке дозволяє вам оголошувати нові функції.

У мові Rust для назв функцій і змінних домовлено використовувати зміїний регістр - тобто всі літери маленькі, а слова відокремлюються підкресленнями. Ось приклад програми, що містить визначення функції:

Файл: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Визначення функцій у Rust починаються з fn, далі іде назва функції і пара дужок. Фігурні дужки кажуть компілятору, де починається і закінчується тіло функції.

Ми можемо викликати будь-яку визначену нами функцію, написавши її назву і пару дужок. Оскільки в програмі є визначеної another_function, її можна викликати зсередини функції main. Зверніть увагу, що ми визначили another_function у початковому коді після функції main; так само її можна було визначити до функції <0>main</0>. Для Rust не має значення, де ви визначаєте функції, важливо, щоб вони були визначені хоч десь у області видимості, доступної з місця виклику.

Почнімо новий двійковий проєкт з назвою functions, щоб глибше дослідити функції. Помістіть приклад another_function до файлу src/main.rs і запустіть його. Ви маєте побачити таке:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Another function.

Рядки виконуються в порядку, в якому вони знаходяться в функції main. Спершу виводиться повідомлення “Hello, world!”, а потім викликається another_function і виводить своє повідомлення.

Параметри

При визначенні функції ми можемо задати параметри, тобто спеціальні змінні, що є частиною сигнатури функції. Коли функція має параметри, ми можемо надати функції конкретні значення для цих параметрів. Формально, конкретні значення звуться аргументами або фактичними параметрами, а параметри у визначенні функції - формальними параметрами, але зазвичай слова <0>параметр</0> та <0>аргумент</0> використовуються як для частини визначення функції, так і для конкретних значень, які були передані при виклику функції.

Додамо параметр у нову версію another_function:

Файл: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

Запустіть цю програму; вона має вивести таке:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

Проголошення another_function містить один параметр під назвою x. Тип x зазначено як i32. Коли в another_function передається 5, макрос println! виведе 5 на місце фігурних дужок з x у форматній стрічці.

У сигнатурі функції ви обов'язково маєте проголошувати тип кожного параметру. Це свідоме рішення у дизайні мови Rust: обов'язкові анотації типів у визначенні функцій означають, що компілятору дуже рідко знадобиться просити вас використовувати їх деінде ще в коді, щоб зрозуміти, який тип ви мали на увазі. Також компілятор може надавати більш помічні повідомлення про помилки, якщо знатиме, які типи параметрів очікує функція.

При проголошенні кількох параметрів відокремлюйте окремі проголошення комами, ось так:

Файл: src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Цей приклад створює функцію print_labeled_measurement з двома параметрами. Перший параметр зветься value і має тип i32. Другий зветься unit_label і має тип char. Функція виводить текст, що містить і value, і unit_label.

Спробуймо запустити цей код. Замініть програму у файлі src/main.rs вашого проєкту functions попереднім прикладом, і запустіть його командою cargo run:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
The measurement is: 5h

Оскільки ми викликали функцію зі значенням 5 для параметра value і значенням 'h' для unit_label, вивід програми містить обидва цих значення.

Інструкції та Вирази

Тіла функцій складаються з послідовності інструкцій, яка може закінчуватися виразом. Поки що функції, про які ми згадували, не мали виразу наприкінці, але вирази були частиною інструкцій. Оскільки Rust є мовою, що ґрунтується на виразах, важливо розуміти цю відмінність. Інші мови не мають таких відмінностей, тому роздивімося, що таке інструкції та вирази і як різниця між ними впливає на тіла функцій.

  • Інструкції (<0>statement</0>) - це команди, що виконують певну дію і не повертають значення.
  • Вирази (<0>expression</0>) обчислюються, в результаті даючи певне значення. Розгляньмо приклади.

Власне, ми вже використовували інструкції та вирази. Створення змінної та приписування їй значення за допомогою ключового слова let є інструкцією. У Блоці коду 3-1 let y = 6; є інструкцією.

Файл: src/main.rs

fn main() {
    let y = 6;
}

Блок коду 3-1. Визначення функції main, що містить одну інструкцію

Визначення функцій також є інструкціями; весь попередній приклад як такий є інструкцією.

Інструкції не повертають значень. Таким чином, не можна присвоїти інструкцію let іншій змінній, як ми намагаємося в наступному коді; ви отримаєте помилку:

Файл: src/main.rs

fn main() {
    let x = (let y = 6);
}

При спробі запустити цю програму, ви отримаєте повідомлення про помилку:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^

error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are unstable
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6;
  |

For more information about this error, try `rustc --explain E0658`.
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` due to 3 previous errors; 1 warning emitted

Інструкція let y = 6 не повертає значення, тому немає нічого, з чим можна було б зв'язати x. Це відрізняється від інших мов, таких як C чи Ruby, де присвоєння повертає значення, яке воно присвоїло. У тих мовах можна написати x = y = 6 і обидві змінні x та y набудуть значення 6; у Rust так робити не можна.

Вирази обчислюються у певне значення і складають більшу частину коду, який ви писатимете на Rust. Розгляньмо просту математичну операцію, таку, як 5 + 6, яка є виразом, що обчислюється у значення 11. Вирази можуть бути частинами інструкцій: у Блоці коду 3-1 в інструкції let y = 6;, 6 - це вираз, що обчислюється у значення 6. Виразами також є виклик функції чи макросу; блок, що створює нову область видимості за допомогою фігурних дужок - це також вираз, наприклад:

Файл: src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

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

Цей вираз:

{
    let x = 3;
    x + 1
}

є блоком, який, в цьому випадку, обчислюється у 4. Це значення прив'язується до y, як частина інструкції let. Зверніть увагу, що x + 1 не має крапки з комою наприкінці, на відміну від більшості рядків, які нам поки що траплялися. Вирази не мають завершувальної крапки з комою. Якщо ви додасте крапку з комою в кінець виразу, ви зробите його інструкцією, яка не повертає значення. Пам'ятайте це, коли вивчатимете далі значення, які повертають функції та вирази.

Функції, що Повертають Значення

Функції можуть повертати значення в код, що їх викликав. Цим значенням ми не даємо власних імен, але маємо проголосити їхній тип після стрілочки (->). У Rust значення, що його повертає функція - це те саме, що значення останнього виразу в блоці - тілі функції. Ви можете також вийти з функції раніше за допомогою ключового слова return і вказання значення, але більшість функцій неявно повертають значення останнього виразу. Ось приклад функції, що повертає значення:

Файл: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

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

У функції five немає викликів інших функцій, макросів чи навіть інструкцій let - саме тільки число 5. І це абсолютно коректна функція в Rust. Зверніть увагу, що тут зазначено тип значення, яке функція повертає -> i32. Спробуймо запустити цей код; вивід має виглядати так:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

5 у five є значенням, яке повертає функція, і тому тип, який повертає функція - i32. Розгляньмо це детальніше. Є два важливі моменти: по-перше, рядок let x = five(); показує, що ми використовуємо значення, яке повернула функція, для ініціалізації змінної. Оскільки функція five повертає 5, цей рядок робить те саме, що й такий:

#![allow(unused)]
fn main() {
let x = 5;
}

По-друге, функція п'ять не має параметрів і визначає тип значення, що повертає, але тіло функції -- самотнє 5 без крапки з комою -- це вираз, значення якого ми хочемо повернути.

Подивімося інший приклад:

Файл: src/main.rs

fn main() {
    let x = plus_one(5);

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

fn plus_one(x: i32) -> i32 {
    x + 1
}

Якщо виконати цей код, він виведе The value of x is: 6. Але якщо ми поставимо крапку з комою в кінець рядка x + 1, щоб він став не виразом, а інструкцією, ми дістанемо помилку:

Файл: src/main.rs

fn main() {
    let x = plus_one(5);

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

fn plus_one(x: i32) -> i32 {
    x + 1;
}

Компіляція цього коду призводить до такої помилки:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon

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

Основне повідомлення про помилку mismatched types (“невідповідні типи”) розкриває основну проблему цього коду. Визначення функції plus_one каже, що вона має повернути i32, але інструкції не обчислюються в значення, що позначається як (), одиничний тип. Таким чином, нічого не повертається, що суперечить визначенню функції й призводить до помилки. У цьому виведенні Rust повідомляє про можливість виправити цю проблему: він радить прибрати крапку з комою, що дійсно виправить помилку.