Функции

Функции широко распространены в коде 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 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 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 является возвращаемым функцией значением, поэтому возвращаемый тип - 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: 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 выдаёт сообщение, которое, возможно, поможет исправить эту проблему: он предлагает удалить точку с запятой для устранения ошибки.