Avatar ·

Как правильно разбить модуль Rust на несколько файлов?

Всем привет! Я начал изучать Rust и столкнулся с проблемой организации кода в модулях. У меня есть файл `main.rs`, в котором я объявил модуль `shapes` вот так: ```rust mod shapes; fn main() { let rect = shapes::Rectangle::new(3.0, 4.0); println!("Площадь: {}", rect.area()); } ``` В `shapes.rs` я определил структуры и функции: ```rust pub struct Rectangle { width: f64, height: f64, } impl Rectangle { pub fn new(w: f64, h: f64) -> Rectangle { Rectangle { width: w, height: h } } pub fn area(&self) -> f64 { self.width * self.height } } ``` Всё работает, но теперь я хочу добавить ещё фигуры: `Circle`, `Triangle`. Если я всё запихну в один `shapes.rs`, файл станет огромным. Я пробовал создать папку `shapes/` и положить туда `mod.rs`, `rectangle.rs`, `circle.rs`. В `mod.rs` написал: ```rust pub mod rectangle; pub mod circle; ``` Но компилятор выдаёт ошибки, что не может найти типы. Как правильно организовать модуль из нескольких файлов? Нужно ли как-то реэкспортировать содержимое? Или, может, есть другой способ? Буду благодарен за подробное объяснение с примерами, как это делается в реальных проектах. Спасибо! <<>>

Основная идея: модуль = папка или файл

В Rust модуль может быть представлен либо отдельным файлом (shapes.rs), либо папкой с файлом mod.rs (или shapes/mod.rs в современном Rust — просто shapes.rs как корень модуля, а содержимое — в подфайлах). Ваша ошибка в том, что вы не реэкспортировали типы из подмодулей наверх. По умолчанию, если вы пишете pub mod rectangle; в mod.rs, то rectangle становится подмодулем shapes, но его содержимое не видно напрямую из main.rs как shapes::Rectangle — оно доступно только как shapes::rectangle::Rectangle.

Правильная структура

Создайте папку shapes/ и внутри неё следующие файлы:

src/
├── main.rs
└── shapes/
    ├── mod.rs      // корень модуля shapes
    ├── rectangle.rs
    └── circle.rs

shapes/rectangle.rs — содержимое такое же, как у вас было в shapes.rs:

pub struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    pub fn new(w: f64, h: f64) -> Rectangle {
        Rectangle { width: w, height: h }
    }

    pub fn area(&self) -> f64 {
        self.width * self.height
    }
}

shapes/circle.rs:

pub struct Circle {
    radius: f64,
}

impl Circle {
    pub fn new(r: f64) -> Circle {
        Circle { radius: r }
    }

    pub fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

shapes/mod.rs — ключевой файл. Здесь мы объявляем подмодули и реэкспортируем их публичные типы:

// Объявляем подмодули
pub mod rectangle;
pub mod circle;

// Реэкспортируем основные типы на уровень модуля shapes
pub use rectangle::Rectangle;
pub use circle::Circle;

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

mod shapes;

fn main() {
    let rect = shapes::Rectangle::new(3.0, 4.0);
    let circ = shapes::Circle::new(2.5);
    println!("Rect area: {}, Circle area: {}", rect.area(), circ.area());
}

Почему это работает?

  • pub mod rectangle; — делает подмодуль rectangle публичным, но его содержимое доступно только по полному пути shapes::rectangle::Rectangle.
  • pub use rectangle::Rectangle; — реэкспортирует тип Rectangle на уровень модуля shapes, так что он становится доступен как shapes::Rectangle.
  • Без pub use вам пришлось бы писать use shapes::rectangle::Rectangle; в main.rs, что неудобно.

Современный синтаксис (Rust 2018+)

Начиная с Rust 2018, вместо mod.rs можно использовать файл с тем же именем, что и модуль-папка. То есть вы можете переименовать shapes/mod.rs в shapes.rs, а подмодули оставить в папке shapes/:

src/
├── main.rs
├── shapes.rs      // корень модуля (вместо mod.rs)
└── shapes/
    ├── rectangle.rs
    └── circle.rs

Содержимое shapes.rs будет таким же, как было в mod.rs. Этот подход считается более чистым, так как файл корня модуля не называется одинаково для всех папок.

Дополнительный совет: префиксы и конфликты имён

Если вы реэкспортируете много типов, следите за возможными конфликтами имён. Например, если у вас есть Rectangle в двух подмодулях, придётся использовать алиасы: pub use rectangle::Rectangle as Rect;. В целом, реэкспорт — это стандартная практика для создания чистого API.

<<>>

Альтернативный подход: один файл с вложенными модулями

Если ваша кодовая база не слишком большая, вы можете вообще не разбивать на отдельные файлы, а использовать вложенные модули прямо в одном файле. Например, в shapes.rs:

pub mod rectangle {
    pub struct Rectangle {
        width: f64,
        height: f64,
    }

    impl Rectangle {
        pub fn new(w: f64, h: f64) -> Rectangle {
            Rectangle { width: w, height: h }
        }

        pub fn area(&self) -> f64 {
            self.width * self.height
        }
    }
}

pub mod circle {
    pub struct Circle {
        radius: f64,
    }

    impl Circle {
        pub fn new(r: f64) -> Circle {
            Circle { radius: r }
        }

        pub fn area(&self) -> f64 {
            std::f64::consts::PI * self.radius * self.radius
        }
    }
}

// Реэкспорт для удобства
pub use rectangle::Rectangle;
pub use circle::Circle;

Этот способ проще для понимания новичками, но при росте проекта файл станет нечитаемым. Я рекомендую использовать его только для небольших модулей (до 200 строк). Для серьёзных проектов лучше сразу переходить на разбиение по файлам, как описано в основном ответе.

Ещё один вариант — использовать include! макрос для вставки содержимого других файлов, но это считается плохой практикой, так как нарушает систему модулей и видимости. Придерживайтесь стандартного подхода с mod и pub use.