🦀 Rust

Модули, пакеты и импорт в Rust: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты строишь огромный дом. Ты же не будешь делать всё сам: замешивать бетон, пилить доски, варить трубы, прокладывать электричество и красить стены? Конечно, нет! Ты пригласишь специалистов: сантехника, электрика, плотника. Каждый из них — мастер в своём деле. У них есть свои инструменты и знания. Ты просто говоришь: «Сантехник, подключи мне раковину» — и он делает свою работу, не отвлекая тебя на детали.

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

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

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

Зачем это нужно новичку? Даже если ты пишешь небольшую программу, привычка делить код на модули спасёт тебя в будущем. Ты научишься мыслить структурно, а не «сваивать» всё в одну кучу. Это как научиться убираться в комнате: сначала кажется, что проще разбросать вещи, но потом ты тратишь часы на поиски носка.

Теория: как это работает изнутри

Давай заглянем под капот Rust и поймём, как устроена система модулей. Это не магия, а строгая иерархия.

Крейты (Crates)

Самый верхний уровень — это крейт (crate). Крейт — это единица компиляции. Rust компилирует каждый крейт отдельно. Есть два типа крейтов:

  • Бинарный крейт (binary crate) — это программа, которую можно запустить. У неё есть точка входа — функция main(). Когда ты пишешь cargo new my_project, ты создаёшь бинарный крейт.
  • Библиотечный крейт (library crate) — это набор кода, который можно использовать в других программах. У него нет main(). Он как библиотека книг: ты берёшь её, читаешь, но сам её не запускаешь. Когда ты пишешь cargo new my_lib --lib, ты создаёшь библиотеку.

Пакеты (Packages)

Пакет — это набор крейтов, объединённых одним файлом Cargo.toml. Один пакет может содержать один библиотечный крейт и несколько бинарных. Обычно пакет — это твой проект. Когда ты создаёшь проект через cargo new, ты создаёшь пакет.

Модули (Modules)

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

Модули работают по принципу дерева. Есть корневой модуль (root module) — это файл main.rs для бинарного крейта или lib.rs для библиотечного. От него растут ветки — дочерние модули.

Путь (Path)

Чтобы обратиться к чему-то внутри модуля, нужно указать путь. Путь бывает двух видов:

  • Абсолютный путь — начинается с crate (корень текущего крейта) или с названия внешнего крейта.
  • Относительный путь — начинается с self (текущий модуль), super (родительский модуль) или просто с имени текущего модуля.

Видимость (Visibility)

По умолчанию всё в Rust приватно. Это значит, что если ты создал функцию внутри модуля, она не видна снаружи. Чтобы сделать её публичной, нужно добавить ключевое слово pub. Это система безопасности: ты сам решаешь, что можно показывать миру, а что должно остаться внутри.

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

Синтаксис в Rust: все формы и варианты

Теперь перейдём к практике. Как объявлять модули, как их подключать и как импортировать из них код.

1. Объявление модуля

Есть два способа объявить модуль:

Способ 1: Встроенный модуль (inline)

Ты пишешь mod имя_модуля { ... } прямо в файле. Это удобно для маленьких модулей.


// main.rs
mod greetings {
    pub fn hello() {
        println!("Привет из модуля greetings!");
    }
}

fn main() {
    // Вызов функции через абсолютный путь
    crate::greetings::hello();
    // Или через относительный путь
    greetings::hello();
}

Способ 2: Модуль из отдельного файла

Ты создаёшь файл имя_модуля.rs в той же папке, что и main.rs (или lib.rs). Rust сам найдёт его, если ты объявишь mod имя_модуля; в корневом файле.

Структура файлов:


my_project/
├── src/
│   ├── main.rs
│   └── greetings.rs
└── Cargo.toml

Файл greetings.rs:


// greetings.rs
pub fn hello() {
    println!("Привет из файла greetings.rs!");
}

Файл main.rs:


// main.rs
mod greetings; // Объявляем модуль, Rust ищет файл greetings.rs

fn main() {
    greetings::hello();
}

2. Вложенные модули и папки

Если модуль становится большим, его можно разбить на подмодули. Для этого используется папка с именем модуля и файл mod.rs внутри неё (или, в современном Rust, файл с именем модуля, а внутри — папка).

Структура файлов (старый стиль с mod.rs):


my_project/
├── src/
│   ├── main.rs
│   └── math/
│       ├── mod.rs
│       ├── add.rs
│       └── subtract.rs
└── Cargo.toml

Файл math/mod.rs:


// math/mod.rs
pub mod add;
pub mod subtract;

Файл math/add.rs:


// math/add.rs
pub fn add_two_numbers(a: i32, b: i32) -> i32 {
    a + b
}

Файл math/subtract.rs:


// math/subtract.rs
pub fn subtract_two_numbers(a: i32, b: i32) -> i32 {
    a - b
}

Файл main.rs:


// main.rs
mod math;

fn main() {
    let sum = math::add::add_two_numbers(5, 3);
    let diff = math::subtract::subtract_two_numbers(10, 4);
    println!("Сумма: {}, Разность: {}", sum, diff);
}
Современный стиль (Rust 2018+): Вместо mod.rs можно использовать файл с именем модуля, а папку назвать так же. Например, math.rs и папка math/. Rust сначала ищет math.rs, а потом math/mod.rs. Рекомендуется использовать math.rs + math/.

3. Ключевое слово use

Писать полные пути каждый раз — утомительно. Ключевое слово use позволяет сократить путь. Оно работает как «создание ярлыка».


// main.rs
mod math;

// Создаём ярлык для функции
use math::add::add_two_numbers;

fn main() {
    // Теперь можно вызывать просто по имени
    let result = add_two_numbers(10, 20);
    println!("Результат: {}", result);
}

4. Импорт с переименованием (as)

Если имена конфликтуют или просто слишком длинные, можно дать псевдоним.


use std::collections::HashMap as Map;

fn main() {
    let mut my_map = Map::new();
    my_map.insert("ключ", "значение");
}

5. Импорт нескольких элементов

Можно импортировать несколько элементов из одного модуля, используя фигурные скобки.


use std::collections::{HashMap, HashSet};

fn main() {
    let map: HashMap<String, i32> = HashMap::new();
    let set: HashSet<i32> = HashSet::new();
}

6. Импорт всего содержимого модуля (*)

Можно импортировать всё публичное содержимое модуля. Но будь осторожен: это может загрязнить пространство имён.


use math::add::*;

fn main() {
    // Теперь доступны все публичные функции из add
    let result = add_two_numbers(1, 2);
}

7. Внешние пакеты (зависимости)

Чтобы использовать код из других крейтов (например, популярный крейт serde для сериализации), нужно:

  1. Добавить зависимость в Cargo.toml.
  2. Импортировать её с помощью use.

Файл Cargo.toml:


[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = "1.0"  # Добавляем крейт serde

Файл main.rs:


// Импортируем трейт Serialize из крейта serde
use serde::Serialize;

#[derive(Serialize)]
struct Person {
    name: String,
    age: u8,
}

fn main() {
    let person = Person {
        name: "Анна".to_string(),
        age: 25,
    };
    // Теперь person можно сериализовать в JSON, YAML и т.д.
    println!("{:?}", serde_json::to_string(&person).unwrap());
}

Примеры кода — от простого к сложному

Пример 1: Простейший модуль в одном файле


// Пример 1: Встроенный модуль
mod calculator {
    // Публичная функция сложения
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    // Приватная функция (не видна снаружи)
    fn secret_formula(x: i32) -> i32 {
        x * 42
    }
}

fn main() {
    // Вызываем публичную функцию
    let result = calculator::add(5, 3);
    println!("5 + 3 = {}", result);

    // Следующая строка вызовет ошибку компиляции:
    // let secret = calculator::secret_formula(10);
    // Ошибка: function `secret_formula` is private
}

Объяснение: Мы создали модуль calculator с двумя функциями. add — публичная, её можно вызвать извне. secret_formula — приватная, она доступна только внутри модуля. Rust защищает нас от случайного использования внутренних деталей.

Пример 2: Модуль из отдельного файла

Структура проекта:


project/
├── src/
│   ├── main.rs
│   └── greetings.rs
└── Cargo.toml

Файл greetings.rs:


// greetings.rs
/// Функция приветствия
pub fn say_hello(name: &str) {
    println!("Привет, {}! Добро пожаловать в мир Rust.", name);
}

/// Функция прощания
pub fn say_goodbye(name: &str) {
    println!("До свидания, {}! Было приятно познакомиться.", name);
}

Файл main.rs:


// main.rs
// Объявляем модуль greetings. Rust автоматически найдёт файл greetings.rs
mod greetings;

fn main() {
    // Используем функции из модуля
    greetings::say_hello("Алексей");
    greetings::say_goodbye("Алексей");
}

Объяснение: Код разделён на два файла. main.rs — точка входа, greetings.rs — библиотека функций. Это базовый способ организации кода.

Пример 3: Вложенные модули с папками

Структура проекта:


project/
├── src/
│   ├── main.rs
│   └── shapes/
│       ├── mod.rs
│       ├── circle.rs
│       └── rectangle.rs
└── Cargo.toml

Файл shapes/mod.rs:


// shapes/mod.rs
pub mod circle;
pub mod rectangle;

Файл shapes/circle.rs:


// shapes/circle.rs
pub fn area(radius: f64) -> f64 {
    std::f64::consts::PI * radius * radius
}

pub fn circumference(radius: f64) -> f64 {
    2.0 * std::f64::consts::PI * radius
}

Файл shapes/rectangle.rs:


// shapes/rectangle.rs
pub fn area(width: f64, height: f64) -> f64 {
    width * height
}

pub fn perimeter(width: f64, height: f64) -> f64 {
    2.0 * (width + height)
}

Файл main.rs:


// main.rs
mod shapes;

fn main() {
    let circle_area = shapes::circle::area(5.0);
    let rect_perimeter = shapes::rectangle::perimeter(3.0, 4.0);

    println!("Площадь круга: {:.2}", circle_area);
    println!("Периметр прямоугольника: {:.2}", rect_perimeter);
}

Объяснение: Мы создали иерархию модулей. shapes — родительский модуль, circle и rectangle — дочерние. Каждый дочерний модуль отвечает за свою геометрическую фигуру.

Пример 4: Использование use для сокращения путей


// Используем модуль из примера 3
mod shapes;

// Создаём ярлыки для часто используемых функций
use shapes::circle::area as circle_area;
use shapes::rectangle::perimeter;

fn main() {
    // Теперь не нужно писать полный путь
    let area = circle_area(7.0);
    let perim = perimeter(5.0, 6.0);

    println!("Площадь круга: {}", area);
    println!("Периметр: {}", perim);
}

Объяснение: use делает код чище. Мы переименовали circle::area в circle_area, чтобы избежать путаницы с rectangle::area.

Пример 5: Реэкспорт (pub use)

Иногда нужно сделать элемент дочернего модуля доступным на уровне родительского модуля. Это называется реэкспорт.


// shapes/mod.rs
mod circle;
mod rectangle;

// Реэкспортируем функцию area из circle на уровень shapes
pub use circle::area as circle_area;
pub use rectangle::area as rect_area;

Теперь в main.rs можно писать:


mod shapes;

fn main() {
    // Вызываем area через shapes, хотя сама функция определена в shapes::circle
    let area = shapes::circle_area(10.0);
    println!("Площадь круга: {}", area);
}

Объяснение: Реэкспорт позволяет создать удобный публичный интерфейс. Пользователю модуля не нужно знать внутреннюю структуру. Он просто вызывает shapes::circle_area.

Пример 6: Работа с внешним крейтом (библиотекой)

Добавим в проект крейт rand для генерации случайных чисел.

Файл Cargo.toml:


[package]
name = "random_example"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8"

Файл main.rs:


// Импортируем нужные трейты и функции из крейта rand
use rand::Rng;

fn main() {
    // Создаём генератор случайных чисел
    let mut rng = rand::thread_rng();

    // Генерируем случайное целое число от 1 до 100
    let random_number: i32 = rng.gen_range(1..=100);
    println!("Случайное число: {}", random_number);

    // Генерируем случайное число с плавающей точкой от 0.0 до 1.0
    let random_float: f64 = rng.gen();
    println!("Случайное дробное число: {:.4}", random_float);

    // Генерируем случайное булево значение
    let random_bool: bool = rng.gen();
    println!("Случайный булевый флаг: {}", random_bool);
}

Объяснение: Мы подключили внешнюю библиотеку rand. Cargo автоматически скачает её с crates.io. Мы импортировали трейт Rng, который предоставляет методы для генерации случайных чисел.

Пример 7: Создание собственной библиотеки (lib crate)

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

Структура проекта:


my_project/
├── src/
│   ├── main.rs          # Бинарный крейт
│   └── lib.rs           # Библиотечный крейт
└── Cargo.toml

Файл lib.rs:


// lib.rs
/// Модуль для работы с текстом
pub mod text_utils {
    /// Переворачивает строку задом наперёд
    pub fn reverse(s: &str) -> String {
        s.chars().rev().collect()
    }

    /// Считает количество слов в строке
    pub fn word_count(s: &str) -> usize {
        s.split_whitespace().count()
    }
}

/// Модуль для математических операций
pub mod math_utils {
    /// Возвращает наибольший общий делитель (НОД)
    pub fn gcd(a: u64, b: u64) -> u64 {
        if b == 0 {
            a
        } else {
            gcd(b, a % b)
        }
    }
}

Файл main.rs:


// main.rs
// Импортируем библиотечный крейт. Имя крейта совпадает с именем пакета.
use my_project::text_utils;
use my_project::math_utils;

fn main() {
    let text = "Привет, мир! Это Rust.";
    println!("Исходный текст: {}", text);
    println!("Перевёрнутый: {}", text_utils::reverse(text));
    println!("Количество слов: {}", text_utils::word_count(text));

    let a = 48;
    let b = 18;
    println!("НОД({}, {}) = {}", a, b, math_utils::gcd(a, b));
}

Объяснение: Мы создали библиотеку my_project (файл lib.rs) и использовали её в main.rs. Обрати внимание на путь импорта: my_project::text_utils::reverse. Имя крейта — это имя пакета, указанное в Cargo.toml.

Сравнение с Python: чем похоже и чем отличается

Если ты знаком с Python, тебе будет проще понять модули в Rust. Но есть важные различия.

Характеристика Python Rust
Объявление модуля Файл .py автоматически становится модулем. Не нужно ничего объявлять. Нужно явно объявить mod имя; в корневом файле. Rust не делает файл модулем автоматически.
Импорт import module_name или from module import function use crate::module_name::function; или use module_name::function;
Псевдонимы import module as alias use module as alias;
Приватность По умолчанию всё публично. Приватность обозначается подчёркиванием (_private), но это соглашение, а не правило языка. По умолчанию всё приватно. Нужно явно указывать pub. Это строгое правило, нарушение приводит к ошибке компиляции.
Пакеты Папка с файлом __init__.py (в Python 3.3+ необязательно). Папка с файлом mod.rs или имя_модуля.rs + папка имя_модуля/.
Внешние зависимости Устанавливаются через pip, импортируются по имени. Указываются в Cargo.toml, Cargo скачивает и компилирует их. Импортируются по имени крейта.
Реэкспорт Можно сделать через from .submodule import func в __init__.py. Используется pub use.
Точка входа Любой файл можно запустить. if __name__ == "__main__": Только файл main.rs (для бинарного крейта).
Главное отличие: Rust требует явности. В Python ты можешь импортировать что угодно откуда угодно, и это часто приводит к путанице. Rust заставляет тебя думать о структуре программы заранее. Это кажется сложным в начале, но спасает от хаоса в больших проектах.

Типичные ошибки и подводные камни

Ошибка 1: Забыл объявить модуль

Ты создал файл utils.rs, но забыл написать mod utils; в main.rs. Rust не найдёт файл.


// main.rs
// Забыли: mod utils;

fn main() {
    // Ошибка: cannot find module `utils`
    utils::do_something();
}

Решение: Всегда добавляй mod имя_файла; в корневой файл крейта.

Ошибка 2: Попытка вызвать приватную функцию


mod my_module {
    fn private_func() {
        println!("Это секрет!");
    }
}

fn main() {
    // Ошибка: function `private_func` is private
    my_module::private_func();
}

Решение: Добавь pub перед функцией, если она должна быть доступна снаружи.

Ошибка 3: Конфликт имён при импорте


mod math {
    pub fn add(a: i32, b: i32) -> i32 { a + b }
}

mod calculator {
    pub fn add(a: i32, b: i32) -> i32 { a + b + 1 }
}

use math::add;
use calculator::add; // Ошибка: конфликт имён

fn main() {
    let result = add(1, 2); // Какой add вызвать?
}

Решение: Используй псевдонимы (as) или импортируй через родительский модуль.


use math::add as math_add;
use calculator::add as calc_add;

Ошибка 4: Неправильный путь к модулю

Если модуль находится в папке, путь должен быть правильным. Например, если у тебя файл shapes/circle.rs, то в main.rs нужно писать mod shapes;, а внутри shapes/mod.rs — pub mod circle;.


// Неправильно:
mod shapes::circle; // Так нельзя!

// Правильно:
mod shapes; // Объявляем родительский модуль
// В shapes/mod.rs: pub mod circle;

Ошибка 5: Забыл добавить зависимость в Cargo.toml

Ты пытаешься импортировать крейт serde, но не добавил его в [dependencies].


use serde::Serialize; // Ошибка: cannot find crate `serde`

Решение: Добавь serde = "1.0" в Cargo.toml и выполни cargo build.

Ошибка 6: Циклические зависимости между модулями

Модуль A импортирует модуль B, а модуль B импортирует модуль A. Rust не позволяет этого делать.


// a.rs
mod b; // Ошибка: circular dependency

// b.rs
mod a; // Ошибка: circular dependency

Решение: Перепроектируй архитектуру. Вынеси общий код в третий модуль или используйте трейты.

Лучшие практики и советы профессионалов

1. Один модуль — одна ответственность

Каждый модуль должен отвечать за одну чётко определённую задачу. Если модуль начинает делать слишком много, разбей его на подмодули. Например, модуль database может содержать подмодули connections, queries, migrations.

2. Используй pub use для создания удобного API

Не заставляй пользователей твоего моду