Глава 5: Функции

Содержание: Определение и вызов функций Параметры и возвращаемые значения Анонимные функции и замыкания (closures) Передача функций как аргументов Примеры: рекурсия и замыкания Упражнение: Написать функцию с замыканием для фильтрации

Добро пожаловать в пятую лекцию нашего курса по Rust! Сегодня мы разберём одну из фундаментальных тем программирования — функции. Функции в Rust позволяют структурировать код, повторно использовать логику и делать программы более читаемыми. Мы рассмотрим определение и вызов функций, работу с параметрами и возвращаемыми значениями, анонимные функции и замыкания (closures), передачу функций как аргументов, а также разберём примеры рекурсии и замыканий. Лекция завершится упражнением, где вы напишете функцию с замыканием для фильтрации данных. Приступим!


1. Определение и вызов функций

Основы

Функции в Rust определяются с помощью ключевого слова fn. Синтаксис прост и строг:

fn имя_функции() {
    // тело функции
}

Чтобы вызвать функцию, укажите её имя с круглыми скобками:

fn say_hello() {
    println!("Привет, мир!");
}

fn main() {
    say_hello(); // Вызов функции
}
Совет: Имена функций пишите в стиле snake_case
т.е. маленькими буквами с разделителем _ между словами.
Функция main — точка входа в программу.

2. Параметры и возвращаемые значения

Параметры

Функции могут принимать параметры с явным указанием типа:

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 — для раннего выхода.

3. Анонимные функции и замыкания (closures)

Что такое замыкания?

Замыкания — это анонимные функции, которые могут захватывать переменные из окружающей их области видимости. Они очень гибкие и часто используются там, где нужно передать поведение как аргумент, например, в методы вроде 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
}

Почему это важно?

Захват окружения делает замыкания мощным инструментом, потому что:

Аналогия

Представь, что замыкание — это рюкзак. Когда ты его создаёшь, оно "кладёт в себя" нужные вещи (переменные) из комнаты (окружения). В зависимости от того, как ты используешь эти вещи, оно либо берёт их "на время" (по ссылке), либо забирает насовсем (move).

Многострочные замыкания

fn main() {
    let multiply = |x, y| {
        let result = x * y;
        result
    };
    println!("2 * 3 = {}", multiply(2, 3)); // Вывод: 2 * 3 = 6
}
Совет: Замыкания хороши для кратких операций, для сложной логики используйте функции.

4. Передача функций как аргументов

Функции как параметры

Тип функции — её сигнатура:

// Функция 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 — для замыканий.

5. Примеры: Рекурсия и замыкания

Рекурсия

Рекурсия - Это техника, при которой функция вызывает сама себя с изменённым аргументом.
В примере ниже 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
}

6. Упражнение: Написать функцию с замыканием для фильтрации

Задача: Напишите функцию 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: от базового определения до замыканий и передачи функций как аргументов. Вы научились работать с параметрами, возвращать значения и использовать мощь замыканий для гибкости. Эти навыки станут основой для более сложных тем которые ждут нас впереди.

Практикуйтесь с примерами и упражнением, чтобы закрепить материал.