Функции
Функции в коде на Rust встречаются повсюду. Одну из самых важных функций языка
вы уже видели — это main, точка входа во многие программы. Видели вы и
ключевое слово fn, которым объявляются новые функции.
Имена функций и переменных в Rust принято писать в стиле snake case: все буквы строчные, а слова разделяются подчёркиваниями. Вот программа с примером определения функции:
Файл: 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 и выводит своё
сообщение.
Параметры
У функций могут быть параметры — особые переменные, которые являются частью сигнатуры функции. Если у функции есть параметры, при вызове в них можно передать конкретные значения. Строго говоря, сами конкретные значения называются аргументами, но в разговоре слова «параметр» и «аргумент» часто используют как синонимы — и для переменных в определении функции, и для значений, которые передаются при вызове.
В этой версии 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` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
У another_function один параметр с именем 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` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
Мы вызвали функцию со значением 5 для value и 'h' для unit_label, и
именно эти значения появились в выводе.
Инструкции и выражения
Тело функции состоит из последовательности инструкций, которая может заканчиваться выражением. У функций, которые мы видели до сих пор, такого завершающего выражения не было, но выражения внутри инструкций вам уже встречались. Rust — язык, основанный на выражениях (expression-based), поэтому разница между инструкциями и выражениями здесь важна. В других языках такого разделения нет, так что давайте разберёмся, что такое инструкции и выражения и как их различие сказывается на теле функций.
- Инструкции (statements) выполняют какое-то действие и не возвращают значения.
- Выражения (expressions) вычисляются в итоговое значение.
Посмотрим на примеры.
На самом деле мы уже пользовались и инструкциями, и выражениями. Создать
переменную и присвоить ей значение с помощью let — это инструкция. В
листинге 3-1 let y = 6; — инструкция.
fn main() {
let y = 6;
}
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);
| ^^^
|
= 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)]` (part of `#[warn(unused)]`) 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
нечего. В этом Rust отличается от других языков, например C и Ruby, где
присваивание возвращает присвоенное значение. Там можно написать x = y = 6,
и обе переменные, x и y, получат значение 6. В Rust так не получится.
Выражения вычисляются в значение, и из них состоит большая часть остального
кода, который вы будете писать на Rust. Возьмём математическую операцию,
например 5 + 6: это выражение, которое вычисляется в 11. Выражения могут
входить в инструкции: в листинге 3-1 6 в инструкции let y = 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` 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 без точки с запятой. Точки с запятой нет,
потому что это выражение, значение которого мы хотим вернуть.
Посмотрим на другой пример:
Файл: 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 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, но
инструкция ни во что не вычисляется — это обозначается (), единичным типом.
Получается, функция ничего не возвращает, а это противоречит её определению —
отсюда и ошибка. В выводе Rust даже подсказывает, как, скорее всего, это
исправить: убрать точку с запятой. И это действительно устранит ошибку.