Функции

Функции — это неотъемлемая часть программ на Rust. Вы уже увидели одну из некоторых наиболее важных функций языка — функцию main, являющуюся точкой входа большинства программа. Вы также знакомы с ключевым словом fn, используемым для объявления новых функций.

Принятым в Rust стилем написания имён функций и переменных является snake case — в нём используются буквы только в нижнем регистре, а слова разделяются нижними подчёркиваниями. Вот программа, содержащая пример определения функции:

Файл: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Другая функция.");
}

Функция в 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!
Другая функция.

Выражения исполняются в том порядке, в каком они записаны в функции main. Первым печатается сообщение "Hello, world!", и только потом вызывается another_function и печатается её сообщение.

Параметры

Мы можем определить функцию с параметрами — специальными переменными, являющимися частью сигнатуры функции. Если функция имеет параметры, то чтобы её вызвать, вы должны предоставить ей конкретные значения каждого параметра. Строго говоря, передаваемые значения называются аргументами, но в обиходе слова параметр и аргумент взаимозаменяемы и могут использоваться чтобы говорить как о переменных в определении функции, так и о конкретных значениях, передаваемых функции при её вызове.

Добавим функции another_function параметр:

Файл: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("Значение x: {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`
Значение x: 5

Функция another_function имеет один параметр под названием x. Тип x определён как i32. Когда мы вызываем функцию another_function с аргументом 5, макрос println! принимает переменную x нашей функции и помещает вместо неё её значение 5 на место метки подстановки.

Вы должны указывать тип каждого параметра при объявлении функции. Такая особенность Rust сделана намеренно: требуя аннотировать тип сразу при определении функции, компилятор избавляет вас от необходимости самому уточнять тип при каждом использовании функции. Компилятор также может дать более полезные сообщения об ошибках, если будет знать, какие типы функция ожидает получить.

Чтобы определить функцию с несколькими параметрами, разделите их запятыми; вот так:

Файл: src/main.rs

fn main() {
    print_labeled_measurement(5, 'ч');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("Физическая величина: {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`
Физическая величина: 5ч

Поскольку мы вызвали функцию с аргументами 5 и 'ч' (соответственно параметрам value и unit_label), вывод программы содержит эти значения.

Инструкции и выражения

Тело функции состоит из последовательности инструкций и (необязательно) выражения в конце. Пока что ни одна из рассмотренных нами функций не имела завершающего выражения, но вы уже видели применение выражений в инструкциях. Поскольку Rust — это язык, основенный на выражениях, важно понять разницу между инструкциями и выражениями; другие языки часто не имеют подобного разделения. Давайте посмотрим, чем являются инструкции и выражения и как их отличия влияют на работу функций.

  • Инструкция — это некоторое действие; она не возвращает значение.
  • Выражение — это последовательность вычислений, производящая некоторое значение.

На самом деле, мы уже использовали инструкции и выражения. Создание переменной и приписывание ей значения с помощью ключегового слова let — это инструкция. Посмотрите в Листинг 3-1: let y = 6; является инструкцией.

fn main() {
    let y = 6;
}

Определения функций — это тоже инструкция; вообще, весь предыдущий пример сам по себе является выражением. (Но как мы увидим далее, вызов функции инструкцией не является.)

Инструкции не возвращают значений. Следовательно, вы не можете присвоить инструкцию 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: переменной y будет присвоена 6, и это присвоение само по себе вернёт ту же 6 и присвоит её переменной x. В Rust такое сделать не выйдет.

Выражения вычисляются в значение; они составляют основную долю кода, который вы будете писать на Rust. Например, математические операции (вроде 5 + 6) являются выражениями (выражение 5 + 6 вычислится в значение 11). Выражения могут быть частью инструкций: например, 6 в инструкции let y = 6; в Листинге 3-1 является выражением, которое вычисляется в значение 6. Вызов функции тоже является выражением, равно как и вызов макроса. Новый блок кода, определённый фигурными скобками, тоже является выражением; например:

Файл: src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("Значение y: {y}");
}

Это выражение ...

{
    let x = 3;
    x + 1
}

... является блоком кода, который (в данном случае) вычисляется в 4. Это значение, как часть инструкции let, связывается с переменной y. Обратите внимание, что строка x + 1 не завершается точкой с запятой. Выражения не включают в себя точку с запятой. Если вы добавите точку с запятой в конец выражения, вы превратите его в инструкцию, и оно перестанет возвращать значение, в которое вычисляется. Помните об этом, пока мы будем рассматривать возвращение значений функциями и выражениями.

Функции, возвращающие значения

Функции могут возвращать значения коду, который их вызывает. Возвращаемые значения не обозначаются именами, но мы должны указывать их тип после стрелки (->). В Rust, возвращаемым значением функции является значение последнего выражения в её теле. Вы можете вернуть значение из функции раньше её завершения, использовав ключевое слово return и указав значение, которое хотите вернуть, но большинство функций неявно возвращают значение последнего выражения. Вот пример функции, возвращающей значение:

Файл: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("Значение x: {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`
Значение x: 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!("Значение x: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Запуск этого кода напечатает Значение x: 6. Но есть мы поставим точку с запятой в конце строки x + 1 (превратив её тем самым из выражения в инструкцию), мы получим ошибку:

Файл: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("Значение x: {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. Следовательно, функции нечего возвращать, и это противоречит её определению, что и вызывает ошибку компиляции. В выводе выше, Rust предлагает потенциальное (и, в общем-то, правильное) решение проблемы: убрать точку с запятой.