% Функции
Каждая программа на 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);