# Глава 5. Функции
Функции являются «кирпичиками», из которых состоит любая программа на Rust. Функции могут быть свободными либо связанными со структурами. В этой главе будут рассмотрены свободные функции. В примере ниже определены две функции: `main()` и `revert()`.
```rust {.example_for_playground .example_for_playground_001}
fn main() {
let mut arr1: Vec<u32> = vec![0, 1, 2, 3, 4, 5, 6, 7];
revert(&mut arr1);
println!("{:?}", arr1);
let mut arr2: Vec<u32> = vec![10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
revert(&mut arr2);
println!("{:?}", arr2);
}
fn revert(arr: &mut Vec<u32>) {
let len: usize = arr.len();
let half: usize = len / 2;
for i in 0..half {
(arr[i], arr[len - i - 1]) = (arr[len - i - 1], arr[i]);
}
}
```
Если запустить программу, то в консоль будет выведено:
```
[7, 6, 5, 4, 3, 2, 1, 0]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
```
Функция `revert()` разворачивает элементы массива в обратном порядке, а `main()` — это точка входа программы (об этом будет рассказано дальше). Несмотря на то, что `revert()` определена после `main()`, она доступна для использования. В Rust неважно, где определена функция, главное, чтобы она была доступна в области видимости вызывающего кода.
Есть пара моментов, на которые стоит обратить внимание:
- `Vec` - это массив (вектор) переменного размера из стандартной библиотеки;
- макрос `vec!` формирует массив с заданными значениями;
- формальный параметр `arr` объявлен как изменяемая ссылка `&mut` на массив чисел; фактические аргументы `arr1` и `arr2` также передаются в `revert()` по изменяемым ссылкам;
- инструкция `(arr[i], arr[len - i - 1]) = (arr[len - i - 1], arr[i])` производит обмен значениями между двумя элементами массива, то же самое можно было бы сделать через временную переменную.
## Объявление функций
Объявление функции начинается с ключевого слова `fn`:
```rust
fn function_name([arg_1: type_1, arg_2: type_2,... arg_n: type_n]) [-> result_type]
{
[function body]
}
```
Здесь:
- `function_name` — имя функции; в Rust принято использовать [змеиный регистр](https://ru.wikipedia.org/wiki/Snake_case) для именования функций и переменных;
- `arg_1`...`arg_n` — имена параметров функции;
- `type_1`...`type_n` — типы параметров, в отличие от объявления переменных, тип параметра является обязательным;
- `result_type` - возвращаемый тип функции.
Количество и состав параметров функции может быть любым. Например, функция `hello_world()` не имеет параметров:
```rust
fn hello_world() {
println!("Hello world!");
}
```
А функция `swap()`, обменивающая значения двух переменных, имеет два параметра, которые являются одновременно входными и выходными:
```rust {.example_for_playground .example_for_playground_002}
fn swap(first: &mut i32, second: &mut i32) {
let tmp: i32 = *first;
*first = *second;
*second = tmp;
}
```
Необходимо реализовать функцию `move_to_end()`, которая перемещает в конец массива все элементы, равные заданному. При этом порядок следования остальных элементов должен остаться прежним. {.task_text}
Например, после выполнения `move_to_end()` массив [10, -1, 28, -1, -1, 7, 4] будет преобразован в [10, 28, 7, 4, -1, -1, -1] для перемещаемого значения, равного -1. {.task_text}
```rust {.task_source #rust_chapter_0050_task_0010}
fn move_to_end(arr: &mut Vec<i32>, val: i32) {
// добавьте реализацию функции
}
```
При обходе массива `arr` потребуется два счетчика: позиция обмена и текущая. В начале нужно найти элемент, значение которого равно заданному в `val`. Индекс найденного элемента сохраняется в позиции обмена. Текущая позиция выставляется сразу за позицией обмена. Далее в цикле проверяются все элементы начиная с текущей позиции. Если элемент на текущей позиции не равен `val`, то производится обмен значения элемента на позиции обмена с текущим элементом, а позиция обмена увеличивается на 1. На каждой итерации цикла текущая позиция увеличивается на 1. Цикл продолжается, пока значение текущей позиции не превышает размер массива `arr`. {.task_hint}
```rust {.task_answer}
fn move_to_end(arr: &mut Vec<i32>, val: i32) {
let len: usize = arr.len();
let mut swap_pos: usize = 'outer: {
for i in 0..len {
if arr[i] == val {
break 'outer i;
}
}
len
};
let mut cur_pos = swap_pos + 1;
while cur_pos < len {
if arr[cur_pos] != val {
(arr[swap_pos], arr[cur_pos]) = (arr[cur_pos], arr[swap_pos]);
swap_pos += 1;
}
cur_pos += 1;
}
}
```
## Возвращаемое значение
При объявлении функции можно указать возвращаемое значение. Функция `bit_count()` возвращает количество ненулевых битов в заданном числе:
```rust {.example_for_playground .example_for_playground_003}
fn bit_count(val: usize) -> usize {
let mut count: usize = 0;
let mut tmp = val;
while tmp > 0 {
count += tmp & 1;
tmp >>= 1;
}
count
}
```
Это может показаться странным, но в теле `bit_count()` отсутствует ключевое слово `return`. Однако никакой ошибки нет. По правилам языка последнее выражение в функции, записанное без точки с запятой `;`, является ее возвращаемым значением, в данном случае это `count`. Вместо `count` можно было бы написать `return count;` и ничего бы не изменилось, обе эти записи идентичны. Пример использования функции `bit_count()`:
```rust
let val: usize = 825774009;
println!("value: {:b}, bits: {}", val, bit_count(val));
```
Параметр форматирования `{:b}` определяет вывод целого числа в двоичном виде. В консоли будет напечатано:
```
value: 110001001110000100111110111001, bits: 16
```
Использовать или не использовать ключевое слово `return` зависит от программиста. Однако предпочтительной является форма записи без `return`. Ключевое слово `return` необходимо, когда функция имеет более одной точки выхода. Важно, чтобы возвращаемый тип всех точек выхода соответствовал заявленному в сигнатуре функции. В противном случае компилятор выдаст сообщение об ошибке.
Функция `get_file_name()` получает на вход путь к файлу и возвращает имя файла без расширения. Например, для _"/rust/get_file_name/Cargo.toml"_ результатом будет _"Cargo"_. Также функция должна обрабатывать специальные случаи. Путь, не содержащий имени файла, заканчивается на слеш, в этом случае `get_file_name()` должна вернуть пустую строку: _"/rust/get_file_name/src/"_ ==> _""_. Имена скрытых файлов в Linux начинаются с точки, функция должна корректно выделять такие имена: _"/rust/get_file_name/.gitignore"_ ==> _".gitignore"_. {.task_text}
В коде функции используются итераторы (о которых будет рассказано в других главах). Метод `chars()` возвращает посимвольный итератор, а `rev()` инвертирует исходный итератор, позволяя обойти строку в обратном порядке. Для получения подстроки с именем файла используются срезы (slice). Выражение `path[0..point_pos]` определяет срез начиная с 0-го (первого) символа строки и заканчивая символом с индексом `point_pos - 1`. Метод `to_string()` преобразует строковый срез в `String`. {.task_text}
Функция `get_file_name()` некорректно обрабатывает некоторые варианты путей к файлу. Нужно определить их и исправить код. {.task_text}
```rust {.task_source #rust_chapter_0050_task_0020}
fn get_file_name(path: &str) -> String {
let len: usize = path.len();
let mut pos: usize = len;
let mut point_pos: usize = len;
let mut slash_pos: usize = len;
for ch in path.chars().rev() {
pos -= 1;
if ch == '.' {
point_pos = pos;
} else if ch == '/' {
slash_pos = pos;
break;
}
}
if pos == 0 && slash_pos == len {
if point_pos == len || point_pos == 0 {
return path.to_string();
}
return path[0..point_pos].to_string();
}
path[slash_pos + 1..point_pos].to_string()
}
```
Решение содержит две ошибки. В цикле поиска позиций точки и слеша не учитывается, что точек в названии файла может быть более одной. Необходимо сохранять позицию первой справа точки, найденной до слеша. Вторая проблема: некорректно обрабатывается путь к файлу, имя которого начинается с точки. Перед последним выражением в функции необходимо проверить, не находится ли точка сразу после слеша. Если это так, то вернуть срез, начинающийся с позиции точки. {.task_hint}
```rust {.task_answer}
fn get_file_name(path: &str) -> String {
let len: usize = path.len();
let mut pos: usize = len;
let mut point_pos: usize = len;
let mut slash_pos: usize = len;
for ch in path.chars().rev() {
pos -= 1;
if ch == '.' {
if point_pos == len {
point_pos = pos;
}
} else if ch == '/' {
slash_pos = pos;
break;
}
}
if pos == 0 && slash_pos == len {
if point_pos == len || point_pos == 0 {
return path.to_string();
}
return path[0..point_pos].to_string();
}
if point_pos == slash_pos + 1 {
return path[point_pos..len].to_string();
}
path[slash_pos + 1..point_pos].to_string()
}
```
В отличие от входных параметров, которых может быть несколько, возвращаемое значение функции всегда только одно. Однако это может быть любой тип, включая составные: перечисление, кортеж, структура, массив и т.п. Если явно не указывать возвращаемое значение, то функция все равно будет возвращать специальное значение — пустой кортеж `()`, также его называют единичным типом:
```rust {.example_for_playground .example_for_playground_004}
fn my_return_value() {
print!("my return value is ");
}
fn main() {
let val = my_return_value();
println!("'{:?}'.", val);
}
```
При запуске программы в консоль будет выведено:
```
my return value is '()'.
```
Реализуйте функцию `seq_len()`, которая вычисляет максимальную длину непрерывной последовательности ненулевых бит. Например, для числа 982785 (11101111111100000001) максимальная длина непрерывной последовательности ненулевых бит равна 8. {.task_text}
```rust {.task_source #rust_chapter_0050_task_0030}
fn seq_len(val: usize) -> usize {
// добавьте реализацию функции
}
```
Для решения задачи потребуются 3 изменяемые переменные: копия `val`, текущая и максимальная длины. В цикле на каждой итерации нужно проверять значение младшего бита копии: `cp & 1`. Если бит установлен, то необходимо увеличить значение текущей длины на 1. Если младший бит равен 0, а текущая длина больше максимальной, то требуется сохранить в максимальной длине значение текущей. При бите равном 0 нужно сбросить значение текущей длины. В конце каждой итерации необходимо сдвинуть на 1 бит вправо значение копии: `cp >>= 1`. Цикл продолжается, пока значении копии больше 0. Также следует учесть случай, когда все биты `val` равны 1. {.task_hint}
```rust {.task_answer}
fn seq_len(val: usize) -> usize {
let mut cp = val;
let mut max: usize = 0;
let mut len: usize = 0;
while cp > 0 {
if cp & 1 > 0 {
len += 1;
} else {
if len > max {
max = len;
}
len = 0;
}
cp >>= 1;
}
if len > max {
max = len;
}
max
}
```
## Ограничения функций в Rust
В Rust на функции накладывается ряд ограничений, которых нет в других языках:
- параметр функции не может иметь значение по умолчанию,
- недопустимы функции с переменным числом параметров,
- не поддерживается перегрузка функций.
Значения по умолчанию для параметров функций авторы языка сочли скорее вредной, чем полезной особенностью. От функций с переменным числом параметров отказались, так как сложно сделать эффективную и безопасную реализацию подобного механизма. А вместо перегрузки функций авторы Rust предлагают использовать типажи (_traits_). О типажах будет рассказано позже.
Если по какой-то причине требуется использовать переменное количество параметров или перегрузку, то можно воспользоваться макросами, для которых нет подобных ограничений.
## Вложенные функции
В Rust можно объявлять функцию внутри другой. Внутренняя функция не имеет доступа к внешней области видимости, то есть для нее недоступны переменные владельца. Это обычная функция, которая недоступна извне:
```rust {.example_for_playground .example_for_playground_005}
fn process_name(text: &str) -> String {
fn is_valid(ch: &char) -> bool {
ch.is_alphanumeric() || *ch == ' ' || *ch == '_' || *ch == '&'
}
fn transform(ch: char) -> char {
if ch == ' ' {
'_'
} else {
ch.to_ascii_uppercase()
}
}
text.chars().filter(is_valid).map(transform).collect()
}
fn main() {
println!("{}", process_name("Horns & Hooves (LLC)."));
}
```
В примере вместо привычных циклов используются итераторы:
- `chars()` — возвращает итератор для посимвольного обхода строки,
- `filter()` — фильтрует входящую последовательность, используя заданный функциональный объект,
- `map()` — преобразует входящую последовательность с помощью переданного функционального объекта,
- `collect()` — преобразует входящую последовательность в строку `String`.
Внутри `process_name()` объявлены две функции:
- `is_valid()` — определяет, является ли символ допустимым; используется в `filter()` для фильтрации исходной последовательности символов;
- `transform()` — преобразует латинские буквы в верхней регистр, а пробел заменяется на знак подчеркивания; используется в `map()` для трансформации последовательности, полученной после `filter()`.
При запуске программы в консоль будет выведено:
```
HORNS_&_HOOVES_LLC
```
## Рекурсия
В Rust функция может вызывать саму себя напрямую либо опосредованно. Например, с помощью рекурсии можно реализовать бинарный поиск в отсортированном массиве:
```rust {.example_for_playground .example_for_playground_006}
fn binary_search(arr: &[i64], desired: i64) -> Option<usize> {
fn binary_search_impl(arr: &[i64], desired: i64, offset: usize) -> Option<usize> {
let len = arr.len();
let pos = len / 2;
let val = arr[pos];
if val == desired {
Some(offset + pos)
} else if len == 1 {
None
} else if val > desired {
binary_search_impl(&arr[0..pos], desired, offset)
} else {
binary_search_impl(&arr[pos..len], desired, offset + pos)
}
}
if arr.is_empty() {
return None;
}
binary_search_impl(&arr, desired, 0)
}
```
Всю работу выполняет внутренняя функция `binary_search_impl()`, которая рекурсивно вызывает себя в зависимости от того, в какую половину попадает искомое значение: справа или слева от центра. В примере используется перечисление [Option<T>](https://doc.rust-lang.org/std/option/enum.Option.html), которое необходимо, когда функция должна просигнализировать, что возвращать нечего. Перечисление `Option<T>` имеет два варианта:
- `None` — отсутствие результата,
- `Some(value)` — обертка для значения типа `T`.
Рекурсия — полезный инструмент, но использовать его следует с осторожностью. При слишком большом количестве вызовов может произойти переполнение стека, что приведет к аварийной остановке программы. Поэтому почти всегда предпочтительнее использовать итерирование вместо рекурсии.
Необходимо реализовать функцию `split_into_squares()`, вычисляющую минимальное число квадратов, которыми можно полностью замостить заданный прямоугольник. На вход функция получает длину и высоту прямоугольника, а возвращает минимальное число не пересекающихся квадратов. Например, прямоугольник размером 6x9 можно замостить тремя квадратами: 6x6, 3x3 и 3x3. {.task_text}
```rust {.task_source #rust_chapter_0050_task_0040}
fn split_into_squares(width: u32, height: u32) -> u32 {
// добавьте реализацию функции
}
```
Необходимо определить, что больше: высота или ширина. Вычислить размеры нового прямоугольника, отняв меньшее от большего. Рекурсивно вызвать функцию с новыми размерами, добавив к возвращаемому значению 1. Если ширина и высота равны, то это квадрат, а возвращаемое значение будет 1. Также следует учесть пограничный случай, когда высота или ширина равны 0. {.task_hint}
```rust {.task_answer}
fn split_into_squares(width: u32, height: u32) -> u32 {
if width == 0 || height == 0 {
0
} else if width == height {
1
} else if width > height {
1 + split_into_squares(width - height, height)
} else {
1 + split_into_squares(width, height - width)
}
}
```
## Заключение
- Объявление функции начинается с ключевого слова `fn`.
- Количество и состав параметров функции может быть любым, но возвращаемое значение только одно.
- В функции с несколькими точками выхода требуется использовать ключевое слово `return`.
- Если в функции единственная точка выхода, то использовать `return` необязательно. Последнее выражение в функции является ее возвращаемым значением.
- В функции не может быть значений параметров по умолчанию и переменного количества аргументов. Невозможно перегрузить функцию.
- Вложенная функция — это обычная функция, объявленная внутри другой.
- Использовать рекурсию следует с осторожностью из-за опасности переполнения стека.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!