Главная / Курсы / Rust / Функции
# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!