Функції (Functions)
Функції поширені в коді Rust. Ви вже бачили одну з найважливіших функцій у мові: функцію main, яка є точкою входу багатьох програм. Ви також бачили ключове слово fn, яке дозволяє вам оголошувати нові функції.
Код Rust використовує snake case як традиційний стиль для імен функцій і змінних, у якому всі літери є малими, а підкреслення розділяють слова. Ось програма, яка містить приклад визначення функції:
Filename: src/main.rs
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Ми визначаємо функцію в Rust, вводячи fn, за яким слідують ім’я функції та набір круглих дужок. Фігурні дужки вказують компілятору, де починається і закінчується тіло функції.
Ми можемо викликати будь-яку функцію, яку визначили, ввівши її ім’я, за яким слідує набір круглих дужок. Оскільки another_function визначена в програмі, її можна викликати зсередини функції main. Зверніть увагу, що ми визначили another_function після функції main у вихідному коді; ми могли б визначити її і до неї. Rust не має значення, де ви визначаєте свої функції, лише те, що вони визначені десь в області видимості, яка може бути видима викликачеві.
Давайте почнемо новий бінарний проєкт під назвою functions, щоб далі дослідити функції. Помістіть приклад another_function у src/main.rs і запустіть його. Ви повинні побачити такий вивід:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
Рядки виконуються в тому порядку, в якому вони з’являються у функції main. Спочатку друкується повідомлення “Hello, world!”, а потім викликається another_function і друкується її повідомлення.
Параметри (Parameters)
Ми можемо визначати функції так, щоб вони мали параметри (parameters), які є спеціальними змінними, що є частиною сигнатури функції. Коли функція має параметри, ви можете надати їй конкретні значення для цих параметрів. Технічно, конкретні значення називаються аргументами (arguments), але в повсякденному спілкуванні люди, як правило, використовують слова параметр і аргумент взаємозамінно як для змінних у визначенні функції, так і для конкретних значень, переданих під час виклику функції.
У цій версії another_function ми додаємо parameter:
Filename: 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` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
Оголошення another_function має один parameter з іменем x. Тип x вказано як i32. Коли ми передаємо 5 у another_function, макрос println! підставляє 5 туди, де в рядку формату була пара фігурних дужок, що містили x.
У сигнатурах функцій ви маєте оголошувати тип кожного параметра. Це свідоме рішення в дизайні Rust: вимога анотацій типів у визначеннях функцій означає, що компілятор майже ніколи не потребує, щоб ви використовували їх деінде в коді для визначення того, який тип ви маєте на увазі. Компілятор також може надавати більш корисні повідомлення про помилки, якщо він знає, яких типів очікує функція.
Під час визначення кількох параметрів розділяйте оголошення параметрів комами, ось так:
Filename: 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` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
Оскільки ми викликали функцію зі значенням 5 для value і 'h' для unit_label, вивід програми містить ці значення.
Оператори та вирази (Statements and Expressions)
Тіла функцій складаються з послідовності операторів, які за бажанням завершуються виразом. Поки що функції, які ми розглядали, не містили кінцевого виразу, але ви бачили вираз як частину оператора. Оскільки Rust є мовою на основі виразів, це важлива відмінність, яку потрібно розуміти. В інших мовах немає таких самих відмінностей, тож давайте подивимося, що таке оператори та вирази (operators and expressions) і як їхні відмінності впливають на тіла функцій.
- Оператори (statements) — це інструкції, які виконують певну дію і не повертають значення.
- Вирази (expressions) обчислюються до результуючого значення.
Давайте подивимося на деякі приклади.
Насправді ми вже використовували оператори та вирази (operators and expressions) . Створення змінної та присвоєння їй значення за допомогою ключового слова let — це оператор. У Лістинг (Listing) 3-1, let y = 6; — це оператор.
fn main() {
let y = 6;
}
Визначення функцій також є операторами; увесь попередній приклад сам по собі є оператором. (Як ми незабаром побачимо, виклик функції, однак, не є оператором.)
Оператори не повертають значень. Тому ви не можете присвоїти let-оператор іншій змінній, як це намагається зробити наведений нижче код; ви отримаєте помилку:
Filename: 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);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
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;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
Оператор let y = 6 не повертає значення, тож немає нічого, до чого міг би прив’язатися x. Це відрізняється від того, що відбувається в інших мовах, таких як C і Ruby, де присвоєння повертає значення присвоєння. У цих мовах ви можете написати x = y = 6 і щоб і x, і y мали значення 6; у Rust це не так.
Вирази (expressions) обчислюються до значення і становлять більшу частину решти коду, який ви писатимете в Rust. Розгляньте математичну операцію, таку як 5 + 6, яка є виразом, що обчислюється до значення 11. Вирази можуть бути частиною операторів: у Лістингу (Listing) 3-1, 6 в операторі let y = 6; — це вираз, який обчислюється до значення 6. Виклик функції — це вираз. Виклик макросу — це вираз. Новий блок області видимості, створений за допомогою фігурних дужок, — це вираз, наприклад:
Filename: src/main.rs
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
Цей вираз (expression):
{
let x = 3;
x + 1
}
є блоком, який, у цьому випадку, обчислюється до 4. Це значення прив’язується до y як частина оператора let. Зверніть увагу на рядок x + 1 без крапки з комою в кінці, що відрізняється від більшості рядків, які ви бачили досі. Вирази (Expressions) не включають завершувальних крапок з комою. Якщо ви додасте крапку з комою в кінець виразу, ви перетворите його на оператор, і тоді він не повертатиме значення. Майте це на увазі, коли далі досліджуватимете значення, що повертаються функціями, та вирази.
Функції зі значеннями, що повертаються (Functions with Return Values)
Функції можуть повертати значення коду, який їх викликає. Ми не даємо імен поверненим значенням, але маємо оголосити їхній тип після стрілки (->). У Rust значення, що повертається функцією, є синонімом значення останнього виразу (expression) у блоці тіла функції. Ви можете вийти з функції раніше, використовуючи ключове слово return і вказавши значення, але більшість функцій неявно повертають останній вираз (expression). Ось приклад функції, яка повертає значення:
Filename: 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` profile [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;
}
По-друге, функція five не має параметрів і визначає тип значення, що повертається, але тіло функції — це самотнє 5 без крапки з комою, тому що це вираз, значення якого ми хочемо повернути.
Давайте подивимося на інший приклад:
Filename: 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, перетворивши його з виразу на оператор?
Filename: 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 to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
Основне повідомлення про помилку, mismatched types, розкриває ключову проблему цього коду. Визначення функції plus_one говорить, що вона повертатиме i32, але оператори не обчислюються до значення, що виражається (), одиничним типом (unit type). Отже, нічого не повертається, що суперечить визначенню функції й призводить до помилки. У цьому виводі Rust надає повідомлення, яке може допомогти виправити цю проблему: він пропонує прибрати крапку з комою, що виправило б помилку.