Rust Курс

Функции в Rust: параметры, возвращаемые значения и замыкания

Самоучитель Python 3, собранный из материалов данного сайта.Предназначен в основном для тех, кто хочет изучить язык программирования Python с нуля.

Введение: Что такое функции и зачем они нужны?

Представь, что ты пишешь инструкцию для робота, который должен убирать твою комнату. Если каждый раз описывать все действия заново — «возьми пылесос, включи его, проведи по ковру, выключи, поставь на место» — это будет очень долго и утомительно. Гораздо удобнее один раз написать команду «пропылесось ковёр», а потом просто вызывать её, когда нужно.

В программировании такие «команды» называются функциями. Функция — это именованный блок кода, который можно вызывать многократно. В Rust функции играют особенно важную роль, потому что они помогают делать код:

  • Понятным — вместо кучи непонятных строк ты видишь осмысленные названия действий.
  • Переиспользуемым — написал один раз, используй сколько хочешь.
  • Легко изменяемым — нужно поменять логику? Исправляешь в одном месте, а не во всём коде.

На этом уроке мы научимся создавать свои функции, передавать им данные и получать результаты. Поехали!

1. Первая функция на Rust

Давай сразу напишем простую программу с функцией. Открой Rust Playground или создай новый проект через Cargo.

// Это наша первая функция!
fn privetstvie() {
    println!("Привет, я функция на Rust!");
}

fn main() {
    // Вызываем функцию по имени
    privetstvie();
    privetstvie(); // Можем вызвать сколько угодно раз!
}

Разберём по частям:

  • fn — ключевое слово, которое говорит Rust: «сейчас будет функция».
  • privetstvie — имя функции (давай на русском, чтобы было понятнее).
  • () — круглые скобки. В них мы будем передавать данные в функцию, но пока они пустые.
  • { ... } — тело функции, то есть код, который выполнится при вызове.
  • В main мы просто пишем имя функции со скобками — это и есть вызов.

Заметь: Rust неважно, в каком порядке объявлены функции. Главное, чтобы они были видны в месте вызова. Обычно main пишут последней, но можно и первой.

2. Параметры: как передать данные в функцию

Функции были бы бесполезны, если бы они всегда делали одно и то же. Представь функцию «свари суп». Без параметров она будет варить всегда один и тот же суп. А с параметрами мы можем сказать: «свари суп из картошки», «свари суп из грибов».

В Rust параметры указываются внутри круглых скобок. Нужно написать имя параметра и его тип через двоеточие.

fn skazhi_privet(imya: &str) {
    println!("Привет, {}!", imya);
}

fn main() {
    skazhi_privet("Анна");    // Выведет: Привет, Анна!
    skazhi_privet("Максим"); // Выведет: Привет, Максим!
}

Что здесь произошло?

  • Мы объявили параметр imya типа &str (строка).
  • Внутри функции используем {} — это подстановка значения.
  • При вызове передаём аргумент — конкретное имя в кавычках.

Важно! Параметры — это переменные, которые существуют только внутри функции. Как только функция закончила работу, они исчезают.

Несколько параметров

Можно передавать сколько угодно параметров, разделяя их запятыми:

fn opisanie_zhivotnogo(vid: &str, vozrast: u32) {
    println!("Это {}. Ему {} лет.", vid, vozrast);
}

fn main() {
    opisanie_zhivotnogo("кот", 3);
    opisanie_zhivotnogo("собака", 5);
}

Здесь vid — строка, а vozrast — целое число без знака (u32). Типы должны совпадать при вызове.

3. Возврат значения: как получить результат

Функции не только принимают данные, но и могут возвращать результаты. Например, функция «сложи два числа» должна вернуть сумму. В Rust для этого используется стрелка -> и тип возвращаемого значения.

fn slozhit(a: i32, b: i32) -> i32 {
    let summa = a + b;
    summa // Возвращаем значение (без точки с запятой!)
}

fn main() {
    let resultat = slozhit(5, 3);
    println!("5 + 3 = {}", resultat); // Выведет: 5 + 3 = 8
}

Обрати внимание на две вещи:

  1. После -> указан тип возвращаемого значения — i32 (целое число со знаком).
  2. В конце функции написано summa без точки с запятой. Это особенность Rust: последнее выражение в функции считается возвращаемым значением.

Можно также использовать ключевое слово return, но в Rust его обычно опускают:

fn umnozhit(x: i32, y: i32) -> i32 {
    return x * y; // Тоже работает, но так пишут реже
}

Если функция ничего не возвращает, пишут -> () или просто опускают тип. На самом деле println! — это макрос, который возвращает пустой кортеж ().

Функция с условием

Давай сделаем функцию, которая определяет, можно ли купить билет в кино (возраст >= 12 лет):

fn mozhno_v_kino(vozrast: u32) -> bool {
    if vozrast >= 12 {
        true
    } else {
        false
    }
}

fn main() {
    let vozrast_petyi = 10;
    if mozhno_v_kino(vozrast_petyi) {
        println!("Петя, проходи!");
    } else {
        println!("Петя, подрасти ещё.");
    }
}

Функция возвращает bool — true или false. Обрати внимание: внутри if мы пишем true и false без точки с запятой, потому что это возвращаемые значения.

4. Реальные аналогии из жизни

Чтобы лучше понять функции, представь, что твой мозг — это компьютер, а функции — это навыки, которые ты освоил.

  • Функция без параметров: «поздороваться» — ты просто говоришь «Привет!», никаких дополнительных данных не нужно.
  • Функция с параметрами: «приготовить бутерброд» — нужны ингредиенты (хлеб, сыр, колбаса). Ты передаёшь их, и получаешь готовый бутерброд.
  • Функция с возвратом: «решить пример 2+2» — ты берёшь числа, складываешь и возвращаешь результат 4.
  • Функция в функции: «приготовить завтрак» может вызывать «сварить яйцо», «поджарить тост» и «заварить чай».

В программировании это называется композиция функций — когда одна функция использует другие.

5. Пример посложнее: калькулятор

Давай напишем несколько функций для простого калькулятора:

fn slozhit(a: f64, b: f64) -> f64 {
    a + b
}

fn vychit(a: f64, b: f64) -> f64 {
    a - b
}

fn umnozhit(a: f64, b: f64) -> f64 {
    a * b
}

fn delit(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        println!("Ошибка: деление на ноль!");
        0.0
    } else {
        a / b
    }
}

fn main() {
    let x = 10.0;
    let y = 3.0;
    
    println!("{} + {} = {}", x, y, slozhit(x, y));
    println!("{} - {} = {}", x, y, vychit(x, y));
    println!("{} * {} = {}", x, y, umnozhit(x, y));
    println!("{} / {} = {}", x, y, delit(x, y));
}

Обрати внимание: мы используем f64 — числа с плавающей точкой (дробные). В функции delit мы проверяем, не ноль ли делитель, чтобы программа не упала.

Такой код легко читать: каждая функция делает только одно действие. Если нужно добавить новую операцию (например, возведение в степень), мы просто пишем ещё одну функцию.

6. Функции могут вызывать другие функции

Это очень мощная возможность. Давай напишем функцию, которая проверяет, является ли число чётным, и используем её в другой функции:

fn chetnoe(chislo: i32) -> bool {
    chislo % 2 == 0
}

fn proverka_chisel(spisok: &[i32]) {
    for &chislo in spisok {
        if chetnoe(chislo) {
            println!("{} — чётное", chislo);
        } else {
            println!("{} — нечётное", chislo);
        }
    }
}

fn main() {
    let chisla = [1, 2, 3, 4, 5, 6];
    proverka_chisel(&chisla);
}

Здесь proverka_chisel вызывает chetnoe для каждого числа. Это как шеф-повар, который просит помощника проверить яйца на свежесть.

7. Возврат нескольких значений с помощью кортежей

Иногда нужно вернуть из функции не одно, а несколько значений. В Rust для этого используют кортежи — это как маленькая коробочка, в которой лежит несколько вещей.

fn razdelit_chisla(spisok: &[i32]) -> (i32, i32) {
    let mut summa_pol = 0;
    let mut summa_otr = 0;
    
    for &chislo in spisok {
        if chislo > 0 {
            summa_pol += chislo;
        } else {
            summa_otr += chislo;
        }
    }
    
    (summa_pol, summa_otr) // Возвращаем кортеж из двух чисел
}

fn main() {
    let dannye = [5, -3, 7, -1, 10, -4];
    let (polozh, otric) = razdelit_chisla(&dannye);
    
    println!("Сумма положительных: {}", polozh);
    println!("Сумма отрицательных: {}", otric);
}

Тип возврата (i32, i32) — это кортеж из двух целых чисел. При вызове мы «распаковываем» его в две переменные: polozh и otric.

8. Область видимости переменных

Важное понятие: переменные, объявленные внутри функции, не видны снаружи. Это называется локальная область видимости.

fn test() {
    let sekret = 42; // Эта переменная живёт только внутри test()
    println!("Внутри функции: {}", sekret);
}

fn main() {
    test();
    // println!("{}", sekret); // ОШИБКА! sekret здесь не существует
}

Зачем это нужно? Чтобы разные функции не мешали друг другу. Ты можешь в одной функции использовать имя i для счётчика, а в другой — тоже i, и они не будут конфликтовать.

9. Константы и статические переменные

Иногда нужно, чтобы значение было доступно всем функциям. Для этого используют const (константа) или static (статическая переменная). Константы принято писать ЗАГЛАВНЫМИ_БУКВАМИ.

const MAKSIMALNAYA_SKOROST: u32 = 120;

fn proverit_skorost(skorost: u32) {
    if skorost > MAKSIMALNAYA_SKOROST {
        println!("Превышение! Максимум {}", MAKSIMALNAYA_SKOROST);
    } else {
        println("Скорость в норме.");
    }
}

fn main() {
    proverit_skorost(100);
    proverit_skorost(150);
}

Константы должны иметь известный тип, и их нельзя изменить. Это как табличка с правилами — все видят, но менять нельзя.

10. Типичные ошибки новичков

Давай разберём ошибки, которые часто встречаются у начинающих:

Ошибка 1: Забыл тип параметра

// НЕПРАВИЛЬНО:
fn privet(imya) { // Rust не знает, какой тип у imya
    println!("Привет, {}", imya);
}

// ПРАВИЛЬНО:
fn privet(imya: &str) {
    println!("Привет, {}", imya);
}

Ошибка 2: Поставил точку с запятой после возвращаемого значения

// НЕПРАВИЛЬНО:
fn sum(a: i32, b: i32) -> i32 {
    a + b; // Точка с запятой превращает это в оператор, а не возврат
}

// ПРАВИЛЬНО:
fn sum(a: i32, b: i32) -> i32 {
    a + b // Без точки с запятой — это выражение, которое возвращается
}

Ошибка 3: Несоответствие типов

// НЕПРАВИЛЬНО:
fn get_number() -> i32 {
    "пять" // Строка не может быть i32!
}

// ПРАВИЛЬНО:
fn get_number() -> i32 {
    5 // Целое число
}

Практические задания

Теперь твоя очередь! Попробуй выполнить эти задания. Если что-то не получается — перечитай урок или напиши в комментариях.

Задание 1: Приветствие по времени суток

Напиши функцию privetstvie_po_vremeni, которая принимает час (u32 от 0 до 23) и возвращает строку с приветствием:

  • 6-11: "Доброе утро!"
  • 12-17: "Добрый день!"
  • 18-23: "Добрый вечер!"
  • 0-5: "Доброй ночи!"

Подсказка: используй if-else или match. Функция должна возвращать &str.

Задание 2: Калькулятор площади

Напиши три функции:

  1. ploshad_kvadrata(storona: f64) -> f64 — площадь квадрата (сторона * сторона).
  2. ploshad_pryamougolnika(shirina: f64, vysota: f64) -> f64 — площадь прямоугольника.
  3. ploshad_kruga(radius: f64) -> f64 — площадь круга (π * радиус^2). Используй std::f64::consts::PI для числа π.

В main вызови все три функции и выведи результаты.

Задание 3: Анализ оценок

Напиши функцию analiz_ocenok(ocenki: &[u32]) -> (u32, u32, f64), которая принимает срез оценок (целые числа от 1 до 5) и возвращает кортеж:

  • Первое значение — максимальная оценка.
  • Второе — минимальная оценка.
  • Третье — средняя оценка (как дробное число).

Подсказка: для среднего нужно сумму всех оценок разделить на их количество. Используй ocenki.len() для длины. Не забудь преобразовать типы: сумма — u32, а среднее — f64, используй as f64.

Пример вызова:

let moy_ocenki = [4, 5, 3, 5, 4, 2];
let (max, min, sred) = analiz_ocenok(&moy_ocenki);
println!("Макс: {}, Мин: {}, Среднее: {:.2}", max, min, sred);

Удачи! Функции — это основа, на которой строится весь Rust. Освоив их, ты сможешь писать сложные программы, разбивая их на маленькие, понятные части.