Функции

Функции широко распространены в коде 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 [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 [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

Объявление another_function имеет один параметр с именем x. Тип переменной x задан как i32. Когда мы передаём 5 в another_function, макрос println! помещает 5 на место пары фигурных скобок в строке форматирования.

В сигнатурах функций вы должны объявить тип каждого параметра. Требование аннотаций типов в определениях функций - это намеренное решение в дизайне 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.

Попробуем запустить этот код. Замените текущую программу проекта functions в файле src/main.rs на предыдущий пример и запустите его с помощью 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 является языком, основанным на выражениях, это важное различие необходимо понимать. В других языках таких различий нет, поэтому давайте рассмотрим, что такое операторы и выражения, и как их различия влияют на тела функций.

Операторы - это инструкции, которые выполняют какое-либо действие и не возвращают значение. Выражения вычисляют результирующее значение. Давайте посмотрим на несколько примеров.

Фактически мы уже использовали операторы и выражения. Создание переменной и присвоение ей значения с помощью 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 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 experimental
 --> 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
  = help: you can write `matches!(<expr>, <pattern>)` instead of `let <pattern> = <expr>`

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 2 previous errors; 1 warning emitted

Оператор let y = 6не возвращает значения, так что нет ничего что можно было бы назначить переменной x. Такое поведение отлично от некоторых других языков, типа 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 [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

Число 5 в функции five является возвращаемым значением функции (можно сказать что функция five вычисляется в 5), вот почему возвращаемым типом является i32. Рассмотрим пример более детально. Есть два важных момента: первый строка let x = five(); показывает, что мы используем значение возвращаемое функцией для инициализации переменной. Так как функция five возвращает 5, то эта строка эквивалентна следующей:


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

Второй момент, функция five не имеет входных параметров и определяет тип возвращаемого значения. Само тело функции - единственная 5 без точки с запятой. Т.к. мы хотим, чтобы функция возвращала значение, последняя строка функции должна быть выражением (не иметь после себя знак точки с запятой). В данной функции мы хотим вернуть 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: consider removing this semicolon

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

Основное сообщение об ошибке «несовпадающие типы» раскрывает основную проблему с этим кодом. В определении функции plus_one говорится, что она вернёт i32, но операторы не вычисляют значение, которое выражается (). Следовательно, ничего не возвращается, что противоречит определению функции и приводит к ошибке. В этом выводе Rust предоставляет сообщение, которое, возможно, поможет исправить эту проблему: он предлагает удалить точку с запятой, что исправит ошибку.