Функции

Функции широко распространены в коде 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 на место пары фигурных скобок, содержащих 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.

Попробуем запустить этот код. Замените текущую программу проекта 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 `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 не так.

Выражения вычисляют значение и составляют большую часть остального кода, который вы напишете на 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: 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

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