Глава 15: Модульная система в Rust

Содержание: Что такое модули и зачем они нужны? Основы: mod и use Пути и видимость: pub, crate Работа с внешними зависимостями Иерархия модулей Примеры: разделение кода на модули Практические советы Упражнение: Создать проект с несколькими модулями Заключение

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


1. Что такое модули и зачем они нужны?

Rust — это язык, который стремится к безопасности, читаемости и масштабируемости. Когда ваш проект растёт, код становится сложнее, и без правильной организации его будет трудно поддерживать. Модульная система в Rust решает эту проблему, позволяя:

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


Основы: mod и use

Ключевое слово mod

В Rust модули объявляются с помощью ключевого слова mod. Оно говорит компилятору: "Здесь начинается новый модуль". Модуль может быть определён:

  1. Внутри текущего файла (inline-модуль).
  2. В отдельном файле.

Пример inline-модуля:

mod my_module {
    fn say_hello() {
        println!("Привет из модуля!");
    }
}

Здесь мы объявили модуль my_module, внутри которого есть функция say_hello. Но если вы попробуете вызвать my_module::say_hello() прямо сейчас, получите ошибку. Почему? Потому что функция по умолчанию приватная, и к ней нет доступа снаружи модуля. Об этом чуть позже.

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

Чтобы использовать элементы из модуля (функции, структуры и т.д.), применяется use. Оно "импортирует" нужные имена в текущую область видимости.

Пример:

mod my_module {
    pub fn say_hello() {  // Добавили pub, чтобы функция стала публичной
        println!("Привет из модуля!");
    }
}

fn main() {
    use my_module::say_hello;  // Импортируем функцию
    say_hello();  // Теперь можно вызывать её напрямую
}

С use вы можете сократить путь до элемента, чтобы не писать каждый раз полный путь вроде my_module::say_hello().


Пути и видимость: pub, crate

Пути в Rust

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

Пример:

mod outer {
    pub mod inner {
        pub fn hello() {
            println!("Привет из inner!");
        }
    }
}

fn main() {
    outer::inner::hello();  // Абсолютный путь от корня
}

Видимость: pub

По умолчанию все элементы в Rust (функции, структуры, модули) являются приватными. Чтобы сделать их доступными извне, используется ключевое слово pub. Без него никто за пределами модуля не сможет использовать ваш код.

Пример:

mod my_module {
    fn private_fn() {  // Приватная функция
        println!("Я приватная!");
    }
    pub fn public_fn() {  // Публичная функция
        println!("Я публичная!");
        private_fn();  // Приватную функцию можно вызывать внутри модуля
    }
}

fn main() {
    my_module::public_f

n();  // Работает
    // my_module::private_fn();  // Ошибка: private_fn приватная
}

Ключевое слово crate

crate обозначает корень вашего проекта. Оно полезно для абсолютных путей, особенно в больших проектах.

Пример:

mod outer {
    pub mod inner {
        pub fn say() {
            println!("Говорю из inner!");
        }
    }
}

fn main() {
    crate::outer::inner::say();  // Абсолютный путь через crate
}

Работа с внешними зависимостями

Rust использует менеджер пакетов Cargo для управления зависимостями. Чтобы подключить внешнюю библиотеку (или "крейт"), нужно:

  1. Добавить её в файл Cargo.toml.
  2. Использовать её в коде через use.

Пример: подключим крейт rand для генерации случайных чисел.

Шаг 1: Обновите Cargo.toml

[dependencies]
rand = "0.8.5"

Шаг 2: Используйте в коде

use rand::Rng;

fn main() {
    let random_number = rand::thread_rng().gen_range(1..101);  // Случайное число от 1 до 100
    println!("Случайное число: {}", random_number);
}

После этого выполните cargo run, и Cargo автоматически загрузит и подключит rand.

Совет: Если вы не знаете, какой крейт использовать, загляните на crates.io — это официальный репозиторий библиотек для Rust.


Иерархия модулей

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

Пример структуры проекта

my_project/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── lib.rs  (опционально, если это библиотека)
    └── outer.rs

В языке программирования Rust файл lib.rs используется, если ваш проект представляет собой библиотеку (library), а не просто исполняемое приложение (binary).

Основное различие между библиотекой и приложением

Файл main.rs

mod outer;  // Объявляем модуль outer, который находится в outer.rs

fn main() {
    outer::say_hello();
}

Файл outer.rs

pub fn say_hello() {
    println!("Привет из outer!");
}

Вложенные модули

Если у вас есть модуль outer, а внутри него ещё один модуль inner, структура может выглядеть так:

src/
├── main.rs
└── outer/
    ├── mod.rs  // Описание модуля outer
    └── inner.rs  // Вложенный модуль inner

src/outer/mod.rs

pub mod inner;  // Объявляем вложенный модуль inner

pub fn outer_fn() {
    println!("Это outer_fn!");
}

src/outer/inner.rs

pub fn inner_fn() {
    println!("Это inner_fn!");
}

src/main.rs

mod outer;

fn main() {
    outer::outer_fn();
    outer::inner::inner_fn();
}

Rust автоматически ищет файлы по имени модуля или использует mod.rs для определения структуры.


Примеры: разделение кода на модули

Давайте создадим небольшой проект, чтобы закрепить знания.

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

simple_game/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── player.rs
    └── game.rs

Cargo.toml

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

src/player.rs

pub struct Player {
    pub name: String,
    pub score: i32,
}

impl Player {
    pub fn new(name: &str) -> Player {
        Player {
            name: String::from(name),
            score: 0,
        }
    }

    pub fn add_score(&mut self, points: i32) {
        self.score += points;
    }
}

src/game.rs

use crate::player::Player;  // Импортируем Player из модуля player

pub fn play_game(player: &mut Player) {
    player.add_score(10);
    println!("{} набрал {} очков!", player.name, player.score);
}

src/main.rs

mod player;  // Объявляем модуль player
mod game;    // Объявляем модуль game

use player::Player;
use game::play_game;

fn main() {
    let mut player = Player::new("Алексей");
    play_game(&mut player);
}

Запустите cargo run, и вы увидите:

Алексей набрал 10 очков!

Этот пример показывает, как модули разделяют логику (игрок и игра) и как они взаимодействуют через публичные интерфейсы.


Практические советы

  1. Имена модулей: Используйте осмысленные имена, отражающие их назначение (например, network, utils, models).
  2. Минимум публичного: Делайте публичным (pub) только то, что действительно нужно извне. Это улучшает инкапсуляцию.
  3. Иерархия: Не создавайте слишком глубокую вложенность модулей (3-4 уровня максимум), чтобы не запутаться.
  4. Документация: Добавляйте комментарии с /// к публичным элементам — это автоматически попадёт в документацию (генерируется через cargo doc).
  5. use для удобства: Если путь длинный, сокращайте его с помощью use, но не злоупотребляйте — код должен оставаться читаемым.

Упражнение: Создать проект с несколькими модулями

Задание

Создайте проект bookstore, который моделирует книжный магазин. У вас должны быть:

Решение

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

bookstore/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── book.rs
    └── store.rs

Cargo.toml

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

src/book.rs

pub struct Book {
    pub title: String,
    pub price: f64,
}

impl Book {
    pub fn new(title: &str, price: f64) -> Book {
        Book {
            title: String::from(title),
            price,
        }
    }
}

src/store.rs

use crate::book::Book;

pub struct Store {
    books: Vec<Book>,
}

impl Store {
    pub fn new() -> Store {
        Store { books: Vec::new() }
    }

    pub fn add_book(&mut self, book: Book) {
        self.books.push(book);
    }

    pub fn total_price(&self) -> f64 {
        self.books.iter().map(|book| book.price).sum()
    }
}

src/main.rs

mod book;
mod store;

use book::Book;
use store::Store;

fn main() {
    let mut store = Store::new();
    store.add_book(Book::new("Война и мир", 15.99));
    store.add_book(Book::new("1984", 9.99));
    println!("Общая стоимость книг: ${:.2}", store.total_price());
}

Проверка

Запустите cargo run. Вывод должен быть:

Общая стоимость книг: $25.98

Заключение

Модульная система Rust — это мощный инструмент для структурирования кода. Мы изучили, как объявлять модули с mod, импортировать их с use, управлять видимостью через pub, работать с путями и внешними зависимостями, а также организовывать иерархию модулей. Теперь вы можете уверенно разделять код на логические части и поддерживать его читаемость.

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

Если у вас остались вопросы, перечитайте примеры или попробуйте изменить упражнение — добавьте, например, метод для удаления книг из магазина. Удачи!