Функции

Каждая программа на 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!() вызывает остановку текущего потока исполнения с заданным сообщением. Поскольку эта функция вызывает остановку исполнения, она никогда не вернёт управление. Поэтому тип её возвращаемого значения обозначается знаком ! и читается как «расходится».

Если добавить функцию diverges() и запустить её, то вы получите следующее сообщение:

thread ‘<main>’ panicked at ‘Эта функция не возвращает управление!’, hello.rs:2

Для получение более подробной информации вы можете посмотреть трассировку установив переменную среды RUST_BACKTRACE:

$ RUST_BACKTRACE=1 ./diverges
thread '<main>' panicked at 'Эта функция не возвращает управление!', hello.rs:2
stack backtrace:
   1:     0x7f402773a829 - sys::backtrace::write::h0942de78b6c02817K8r
   2:     0x7f402773d7fc - panicking::on_panic::h3f23f9d0b5f4c91bu9w
   3:     0x7f402773960e - rt::unwind::begin_unwind_inner::h2844b8c5e81e79558Bw
   4:     0x7f4027738893 - rt::unwind::begin_unwind::h4375279447423903650
   5:     0x7f4027738809 - diverges::h2266b4c4b850236beaa
   6:     0x7f40277389e5 - main::h19bb1149c2f00ecfBaa
   7:     0x7f402773f514 - rt::unwind::try::try_fn::h13186883479104382231
   8:     0x7f402773d1d8 - __rust_try
   9:     0x7f402773f201 - rt::lang_start::ha172a3ce74bb453aK5w
  10:     0x7f4027738a19 - main
  11:     0x7f402694ab44 - __libc_start_main
  12:     0x7f40277386c8 - <unknown>
  13:                0x0 - <unknown>

RUST_BACKTRACE также работает при выполнении команды run:

$ RUST_BACKTRACE=1 cargo run
     Running `target/debug/diverges`
thread '<main>' panicked at 'Эта функция не возвращает управление!', hello.rs:2
stack backtrace:
   1:     0x7f402773a829 - sys::backtrace::write::h0942de78b6c02817K8r
   2:     0x7f402773d7fc - panicking::on_panic::h3f23f9d0b5f4c91bu9w
   3:     0x7f402773960e - rt::unwind::begin_unwind_inner::h2844b8c5e81e79558Bw
   4:     0x7f4027738893 - rt::unwind::begin_unwind::h4375279447423903650
   5:     0x7f4027738809 - diverges::h2266b4c4b850236beaa
   6:     0x7f40277389e5 - main::h19bb1149c2f00ecfBaa
   7:     0x7f402773f514 - rt::unwind::try::try_fn::h13186883479104382231
   8:     0x7f402773d1d8 - __rust_try
   9:     0x7f402773f201 - rt::lang_start::ha172a3ce74bb453aK5w
  10:     0x7f4027738a19 - main
  11:     0x7f402694ab44 - __libc_start_main
  12:     0x7f40277386c8 - <unknown>
  13:                0x0 - <unknown>

Значение расходящейся функции может быть использовано как значение любого типа:

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

Указатели на функции

Можно объявить имя, связанное с функцией:

let f: fn(i32) -> i32;

f — это имя, связанное с указателем на функцию, которая принимает в качестве аргумента i32 и возвращает i32. Например:

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

// без вывода типа
let f: fn(i32) -> i32 = plus_one;

// с выводом типа
let f = plus_one;

Теперь мы можем использовать f, чтобы вызвать функцию:

# fn plus_one(i: i32) -> i32 { i + 1 }
# let f = plus_one;
let six = f(5);