Функции

Каждая программа на Rust имеет по крайней мере одну функцию — main:

fn main() {
}

Это простейшее объявление функции. Как мы упоминали ранее, ключевое слово fn объявляет функцию. За ним следует её имя, пустые круглые скобки (поскольку эта функция не принимает аргументов), а затем тело функции, заключённое в фигурные скобки. Вот функция foo:

fn foo() {
}

Итак, что насчёт аргументов, принимаемых функцией? Вот функция, печатающая число:

fn print_number(x: i32) {
    println!("x равен: {}", x);
}

Вот полная программа, использующая функцию print_number:

fn main() {
    print_number(5);
}

fn print_number(x: i32) {
    println!("x равен: {}", x);
}

Как видите, аргументы функций похожи на операторы let: вы можете объявить тип аргумента после двоеточия.

Вот полная программа, которая складывает два числа и печатает их:

fn main() {
    print_sum(5, 6);
}

fn print_sum(x: i32, y: i32) {
    println!("сумма чисел: {}", x + y);
}

Аргументы разделяются запятой — и при вызове функции, и при её объявлении.

В отличие от let, вы должны объявлять типы аргументов функции. Этот код не скомпилируется:

fn print_sum(x, y) {
    println!("сумма чисел: {}", x + y);
}

Вы увидите такую ошибку:

expected one of `!`, `:`, or `@`, found `)`
fn print_number(x, y) {

Это осознанное решение при проектировании языка. Бесспорно, вывод типов во всей программе возможен. Однако даже в Haskell считается хорошим стилем явно документировать типы функций, хотя в этом языке и возможен полный вывод типов. Мы считаем, что принудительное объявление типов функций при сохранении локального вывода типов — это хороший компромисс.

Как насчёт возвращаемого значения? Вот функция, которая прибавляет один к целому:

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

Функции в Rust возвращают ровно одно значение, тип которого объявляется после «стрелки». «Стрелка» представляет собой дефис (-), за которым следует знак «больше» (>). Заметьте, что в функции выше нет точки с запятой. Если бы мы добавили её:

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

мы бы получили ошибку:

error: not all control paths return a value
fn add_one(x: i32) -> i32 {
     x + 1;
}

help: consider removing this semicolon:
     x + 1;
          ^

Здесь показаны две интересные особенности Rust. Во-первых, это язык, ориентированный на выражения, и во-вторых, смысл точки с запятой отличается от смысла аналогичного символа в других языках с синтаксисом на основе фигурных скобок и точки с запятой. Эти две особенности связаны.

Выражения и операторы

Rust — в первую очередь язык, ориентированный на выражения. Есть только два типа операторов, а всё остальное является выражением.

А в чём же разница? Выражение возвращает значение, в то время как оператор - нет. Вот почему мы получаем здесь «not all control paths return a value»: оператор х + 1; не возвращает значение. Есть два типа операторов в Rust: «операторы объявления» и «операторы выражения». Все остальное — выражения. Давайте сначала поговорим об операторах объявления.

Оператор объявления — это связывание. В некоторых языках связывание переменных может быть записано как выражение, а не только как оператор. Например, в Ruby:

x = y = 5

Однако, в Rust использование let для связывания не является выражением. Следующий код вызовет ошибку компиляции:

let x = (let y = 5); // expected identifier, found keyword `let`

Здесь компилятор сообщил нам, что ожидал увидеть выражение, но let является оператором, а не выражением.

Обратите внимание, что присвоение уже связанной переменной (например: y = 5) является выражением, но его значение не особенно полезно. В отличие от других языков, где результатом присваивания является присваиваемое значение (например, 5 из предыдущего примера), в Rust значением присваивания является пустой кортеж ().

let mut y = 5;

let x = (y = 6);  // x будет присвоено значение `()`, а не `6`

Вторым типом операторов в Rust является оператор выражения. Его цель - превратить любое выражение в оператор. В практическом плане, грамматика Rust ожидает, что за операторами будут идти другие операторы. Это означает, что вы используете точку с запятой для отделения выражений друг от друга. Rust выглядит как многие другие языки, которые требуют использовать точку с запятой в конце каждой строки. Вы увидите её в конце почти каждой строки кода на Rust.

Из-за чего мы говорим «почти»? Вы это уже видели в этом примере:

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

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

Досрочный возврат из функции

А что насчёт досрочного возврата из функции? У нас есть для этого ключевое слово return:

fn foo(x: i32) -> i32 {
    return x;

    // дальнейший код не будет исполнен!
    x + 1
}

return можно написать в последней строке тела функции, но это считается плохим стилем:

fn foo(x: i32) -> i32 {
    return x + 1;
}

Если вы никогда не работали с языком, в котором операторы являются выражениями, предыдущее определение без return может показаться вам странным. Но со временем вы просто перестанете замечать это.

Расходящиеся функции

Для функций, которые не возвращают управление («расходящихся»), в Rust есть специальный синтаксис:

fn diverges() -> ! {
    panic!("Эта функция не возвращает управление!");
}

panic! — это макрос, как и println!(), который мы встречали ранее. В отличие от println!(), panic!() вызывает остановку текущего потока исполнения с заданным сообщением.

Поскольку эта функция вызывает остановку исполнения, она никогда не вернёт управление. Поэтому тип её возвращаемого значения обозначается знаком ! и читается как «расходится». Значение расходящейся функции может быть использовано как значение любого типа:

# fn diverges() -> ! {
#    panic!("Эта функция никогда не выходит!");
# }
let x: i32 = diverges();
let x: String = diverges();