Функции
Как и в других языках программирования, в Rust функции — это механизм, позволяющий разбить программу на отдельные гранулированные подпрограммы.
Синтаксис объявления функции:
fn func_name(arg1: Тип1, arg2: Тип2) -> ТипВозвращаемогоЗначения {
тело функции
}
Пример:
fn sum(a: i32, b: i32) -> i32 {
a + b
}
fn safe_divide(a: f32, b: f32) -> f32 {
if b != 0.0 {
a / b
} else {
0.0
}
}
fn main() {
let a = sum(1, 2);
println!("{a}");
let b = safe_divide(12.0, 4.0);
println!("{b}");
}
При объявлении аргументов функции можно вставлять запятую после последнего аргумента, и поведение будет таким же, как и без неё.
fn sum(a: i32, b: i32,) -> i32 { .. }
return
Как мы видим, последнее вычисленное значение автоматически является результатом функции.
При необходимости “досрочно” выйти из функции, следует явно использовать оператор return:
fn safe_divide(a: f32, b: f32) -> f32 {
if b == 0.0 {
return 0.0;
}
a / b
}
fn main() {
println!("12 / 3 = {}", safe_divide(12.0, 3.0));
println!("12 / 0 = {}", safe_divide(12.0, 0.0));
}
Функции внутри функций
Rust позволяет объявлять функцию внутри другой функции.
Если часть функциональности функции хорошо гранулирована и используется несколько раз и при этом не нужна больше нигде в программе, то её можно вынести в отдельную внутреннюю функцию.
// Возвращает i-й элемент последовательности Фибоначчи.
// Индексация элементов последовательности - с нуля.
fn fibonacci_nth_element(index: usize) -> u32 {
if index == 0 {
return 0;
}
if index == 1 {
return 1;
}
// Высчитывет i-й элемент последовательности Фибоначчи
// * x0 - i-й элемент последовательности
// * x1 - (i+1)-й элемент последовательности
// * next_index - индекс следующего (i+2) элемента
// * desired_index - индекс искомого элемента
fn next_fibonacci(x0: u32, x1: u32, next_index: usize, desired_index: usize) -> u32 {
let x2 = x0 + x1;
if next_index == desired_index {
x2
} else {
next_fibonacci(x1, x2, next_index + 1, desired_index)
}
}
next_fibonacci(0, 1, 2, index)
}
fn main() {
println!("{}", fibonacci_nth_element(0)); // 0
println!("{}", fibonacci_nth_element(1)); // 1
println!("{}", fibonacci_nth_element(2)); // 1
println!("{}", fibonacci_nth_element(3)); // 2
println!("{}", fibonacci_nth_element(4)); // 3
}
return и never type
Tip
Приведённая ниже информация не является необходимой для программирования на Rust, а скорее просто даёт лучшее понимание системы типов.
В главе Примитивные типы данных мы уже упоминали never type !, который используется для тех выражений, которые не возвращают управление в вызывающий код.
Оператор return возвращает значение типа !, так как он завершает исполнение функции и, следовательно, ничего в неё не возвращает.
fn gen_num() -> i32 {
// Переменная v имеет тип !
let v = return 5;
}
fn main() {
let a = gen_num();
}
Never type не представляет каких-то реальных данных, а просто играет роль заглушки, чтобы “склеить” воедино систему типов Rust. Давайте рассмотрим примере:
fn safe_divide(a: f32, b: f32) -> f32 {
let non_zero_divider: f32 =
if b != 0.0 {
b
} else {
return 0.0
};
a / non_zero_divider
}
fn main() {
println!("12 / 0 = {}", safe_divide(12.0, 0.0));
}
Обратите внимание, что тип переменной non_zero_divider — f32. Но как так получается? Ведь тип результата в первой ветке выражения if — f32, а тип результата во второй ветке — ! (never type).
Дело в том, что тип never type автоматически приводится к любому другому типу. Это абсолютно безопасно, так как в реальности это преобразование значения типа ! в значение другого типа всё равно никогда не происходит. Но именно это свойство never type является тем самым “клеем”, который согласовывает типы в подобных выражениях.
Tip
Программисты знакомые с языком Scala, могут провести аналогию с типом
Nothing.
const функции
В Rust можно задать функцию, которая может быть выполнена на этапе компиляции. Такая функция отмечается ключевым словом const.
#![allow(unused)]
fn main() {
const fn func_name(arg1: Тип1, arg2: Тип2) -> ТипВозвращаемогоЗначения {
тело функции
}
}
Например:
const PI: f32 = 3.14;
const TAU: f32 = double(PI);
const fn double(num: f32) -> f32 {
num * 2.0
}
fn main() {
println!("Tau = {TAU}");
}
static переменные
Функция может содержать статические переменные, значения которых сохраняются между вызовами функций.
Это проще показать на примере.
fn sum_with_previous(x: i32) -> i32 {
static mut PREV: i32 = 0; // статическая переменная
unsafe {
let result = PREV + x;
PREV = x;
result
}
}
fn main() {
println!("{}", sum_with_previous(1)); // 1
println!("{}", sum_with_previous(2)); // 3
println!("{}", sum_with_previous(7)); // 9
println!("{}", sum_with_previous(-6)); // 1
}
Функция sum_with_previous складывает значение аргумента со значением аргумента из своего предыдущего вызова. Для этого она использует статическую переменную PREV, которая живёт вне контекста вызова функции.
Как видно, PREV инициализируется нулём. Эта инициализация выполняется только для первого вызова функции.
Также хочется отметить, что всё взаимодействие с мутабельной статической переменной может выполняться только внутри блока unsafe. Дело в том, что потенциально эта функция может быть одновременно вызвана из нескольких параллельных потоков, что может привести к сохранению или чтению некорректного значения PREV одним из потоков. Это классический пример гонок за данные (data race), что в безопасном Rust недопустимо. Именно поэтому необходим unsafe блок.
Это первый раз, когда мы сталкиваемся с блоком unsafe. Мы еще познакомимся с ним подробнее, а пока что скажем, что вся ответственность за безопасность кода внутри блока unsafe ложится на плечи его автора. Поэтому стоит избегать использования небезопасного Rust, если в этом нет необходимости.
Как вы могли понять, мутабельные статические переменные не особо безопасны по своей природе и могут быть использованы без дополнительных механизмов синхронизации только в однопоточной среде. К счастью, в back-end приложениях вы скорее всего никогда не будете их использовать — мы рассмотрели их только для того, чтобы больше раскрыть тему функций.