Добро пожаловать в пятую лекцию нашего курса по Rust! Сегодня мы разберём одну из фундаментальных тем программирования — функции. Функции в Rust позволяют структурировать код, повторно использовать логику и делать программы более читаемыми. Мы рассмотрим определение и вызов функций, работу с параметрами и возвращаемыми значениями, анонимные функции и замыкания (closures), передачу функций как аргументов, а также разберём примеры рекурсии и замыканий. Лекция завершится упражнением, где вы напишете функцию с замыканием для фильтрации данных. Приступим!
Функции в Rust определяются с помощью ключевого слова fn
. Синтаксис прост и строг:
fn имя_функции() {
// тело функции
}
Чтобы вызвать функцию, укажите её имя с круглыми скобками:
fn say_hello() {
println!("Привет, мир!");
}
fn main() {
say_hello(); // Вызов функции
}
snake_case
_
между словами.main
— точка входа в программу.Функции могут принимать параметры с явным указанием типа:
fn add(a: i32, b: i32) {
println!("Сумма: {}", a + b);
}
fn main() {
add(5, 3); // Вывод: Сумма: 8
}
Тип возврата указывается после ->
. Последнее выражение — возвращаемое значение:
fn add(a: i32, b: i32) -> i32 {
a + b // Без точки с запятой
}
fn main() {
let result = add(5, 3);
println!("Результат: {}", result); // Вывод: Результат: 8
}
Явный возврат с return
:
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
return
— для раннего выхода.Замыкания — это анонимные функции, которые могут захватывать переменные из окружающей их области видимости. Они очень гибкие и часто используются там, где нужно передать поведение как аргумент, например, в методы вроде map
, filter
или в потоках (threads). Синтаксис:
|параметры| выражение
Пример:
fn main() {
let add_one = |x| x + 1;
println!("5 + 1 = {}", add_one(5)); // Вывод: 5 + 1 = 6
}
Захват окружения означает, что замыкание может "взять" переменные из той области видимости, где оно было создано, и использовать их внутри себя. Окружение — это, по сути, контекст, в котором замыкание определено, то есть все переменные, которые доступны в этом месте кода на момент создания замыкания.
В отличие от обычных функций, которые работают только с переданными им аргументами и не имеют прямого доступа к внешним переменным (кроме глобальных), замыкания могут "захватывать" эти внешние переменные и работать с ними, даже если они не были явно переданы как параметры.
fn main() {
let value = 10;
let add_value = |x| x + value;
println!("3 + 10 = {}", add_value(3)); // Вывод: 3 + 10 = 13
}
Захват окружения делает замыкания мощным инструментом, потому что:
map
или filter
).Представь, что замыкание — это рюкзак. Когда ты его создаёшь, оно "кладёт в себя" нужные вещи (переменные) из комнаты (окружения). В зависимости от того, как ты используешь эти вещи, оно либо берёт их "на время" (по ссылке), либо забирает насовсем (move).
fn main() {
let multiply = |x, y| {
let result = x * y;
result
};
println!("2 * 3 = {}", multiply(2, 3)); // Вывод: 2 * 3 = 6
}
Тип функции — её сигнатура:
// Функция apply принимает два параметра:
// 1. x типа i32 (32-битное целое число)
// 2. f - функцию с сигнатурой fn(i32) -> i32, то есть функцию,
// которая принимает i32 и возвращает i32
// Сама apply возвращает i32
fn apply(x: i32, f: fn(i32) -> i32) -> i32 {
// Применяем переданную функцию f к аргументу x
// и возвращаем результат
f(x)
}
// Простая функция, которая удваивает входное число
// Принимает x типа i32 и возвращает x * 2 тоже типа i32
fn double(x: i32) -> i32 {
x * 2
}
fn main() {
// Вызываем apply с двумя аргументами:
// 1. Число 5
// 2. Функция double
// double будет применена к 5, то есть 5 * 2 = 10
let result = apply(5, double);
// Выводим результат в консоль
// Макрос println! форматирует строку
// {} - это placeholder для значения result
println!("Удвоенное: {}", result); // Вывод: Удвоенное: 10
}
// Функция apply_closure принимает число и замыкание:
// 1. x - аргумент типа i32
// 2. f - замыкание с типом impl Fn(i32) -> i32
// impl Fn(i32) -> i32 означает, что это реализация трейта Fn:
// - Fn - это один из трейтов в Rust для работы с замыканиями
// - Fn описывает функции/замыкания, которые могут быть вызваны с аргументом i32
// и возвращают i32, при этом захватывают окружение по ссылке
// Возвращает функция i32
fn apply_closure(x: i32, f: impl Fn(i32) -> i32) -> i32 {
// Применяем переданное замыкание f к аргументу x
f(x)
}
fn main() {
let offset = 3; // Создаём переменную offset в текущей области(скоупе)
// Определяем замыкание add_offset
// |x| - это параметр замыкания (в данном случае x)
// x + offset - тело замыкания, которое прибавляет offset к x
// Замыкание "захватывает" переменную offset из окружающей области видимости
let add_offset = |x| x + offset;
// Вызываем apply_closure:
// - Передаём 5 как первый аргумент
// - Передаём замыкание add_offset как второй аргумент
// Внутри apply_closure будет выполнен код add_offset(5), то есть 5 + 3
println!("5 + 3 = {}", apply_closure(5, add_offset)); // Вывод: 5 + 3 = 8
}
fn
для простых функций, impl Fn
— для замыканий.Рекурсия - Это техника, при которой функция вызывает сама себя с изменённым аргументом.
В примере ниже factorial
вызывает себя с n - 1
, пока не дойдёт до базового случая n <= 1
.
Базовый случай:
Условие n <= 1
необходимо, чтобы рекурсия не стала бесконечной.
Когда n
становится 1 или 0, возвращается 1, и рекурсия "разворачивается" обратно.
Как работает:
factorial(5)
→ 5 * factorial(4)
→ 5 * (4 * factorial(3))
→
и так далее до 5 * 4 * 3 * 2 * 1 = 120
.
// Функция factorial вычисляет факториал числа n
// Принимает n типа u32 (32-битное беззнаковое целое число)
// Возвращает u32
fn factorial(n: u32) -> u32 {
// Условие выхода из рекурсии:
// Если n <= 1, возвращаем 1 (факториал 0 и 1 равен 1)
if n <= 1 {
1
} else {
// Рекурсивный случай:
// Умножаем n на факториал предыдущего числа (n - 1)
// Функция вызывает сама себя с уменьшенным аргументом
n * factorial(n - 1)
}
}
fn main() {
// Вычисляем факториал числа 5
// factorial(5) раскладывается так:
// 5 * factorial(4) = 5 * (4 * factorial(3)) = 5 * (4 * (3 * factorial(2))) =
// 5 * (4 * (3 * (2 * factorial(1)))) = 5 * 4 * 3 * 2 * 1 = 120
println!("Факториал 5 = {}", factorial(5)); // Вывод: Факториал 5 = 120
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6]; // Создаём вектор (динамический массив) целых чисел
// Определяем замыкание is_even
// |x| - параметр x, тело замыкания проверяет, чётное ли число
// Возвращает true, если x делится на 2 без остатка
let is_even = |x| x % 2 == 0;
// Используем итератор для фильтрации и подсчёта:
// 1. numbers.iter() - создаёт итератор по элементам вектора
// 2. filter() - фильтрует элементы, пропуская только те, для которых
// замыкание |&&x| is_even(x) возвращает true
// &&x - двойное разыменование, так как iter() возвращает ссылки
// 3. count() - подсчитывает количество элементов, прошедших фильтр
let even_count = numbers.iter().filter(|&&x| is_even(x)).count();
println!("Чётных чисел: {}", even_count); // Вывод: Чётных чисел: 3
}
Задача: Напишите функцию filter_numbers
, принимающую вектор чисел и замыкание для фильтрации, и возвращающую новый вектор.
fn filter_numbers(numbers: Vec, predicate: impl Fn(i32) -> bool) -> Vec {
let mut result = Vec::new();
for num in numbers {
if predicate(num) {
result.push(num);
}
}
result
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
// Фильтрация чётных чисел
let is_even = |x| x % 2 == 0;
let even_numbers = filter_numbers(numbers.clone(), is_even);
println!("Чётные числа: {:?}", even_numbers); // Вывод: [2, 4, 6]
// Фильтрация чисел больше 3
let greater_than_three = |x| x > 3;
let big_numbers = filter_numbers(numbers, greater_than_three);
println!("Числа > 3: {:?}", big_numbers); // Вывод: [4, 5, 6]
}
Улучшение: Перепишите с использованием filter
и итераторов.
Мы изучили функции в Rust: от базового определения до замыканий и передачи функций как аргументов. Вы научились работать с параметрами, возвращать значения и использовать мощь замыканий для гибкости. Эти навыки станут основой для более сложных тем которые ждут нас впереди.
Практикуйтесь с примерами и упражнением, чтобы закрепить материал.