Разделить модуль на несколько файлов

103

Я хочу иметь модуль с несколькими структурами в нем, каждая в своем собственном файле. На Mathпримере модуля:

Math/
  Vector.rs
  Matrix.rs
  Complex.rs

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

use Math::Vector;

fn main() {
  // ...
}

Однако модульная система Rust (которая сначала немного сбивает с толку) не предоставляет очевидного способа сделать это. Кажется, это позволяет вам хранить весь модуль в одном файле. Это не по-деревенски? Если нет, то как мне это сделать?

звездный пейзаж
источник
1
Я понял: «Я хочу иметь модуль с несколькими структурами, каждая в своем собственном файле». это означает, что вы хотите, чтобы каждое определение структуры находилось в отдельном файле.
BurntSushi5
1
Это нельзя считать простоватым, хотя модульная система, безусловно, допускает такое структурирование. Обычно предпочтительно, чтобы путь к модулю напрямую соответствовал пути файловой системы, например, struct foo::bar::Bazдолжна быть определена в foo/bar.rsили foo/bar/mod.rs.
Крис Морган

Ответы:

112

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

Я думаю, что ключевым моментом здесь является использование pub use, которое позволит вам повторно экспортировать идентификаторы из других модулей. Прецедент этого есть в std::ioящике Rust, где некоторые типы из подмодулей реэкспортируются для использования вstd::io .

Изменить (2019-08-25): следующая часть ответа была написана довольно давно. Он объясняет, как настроить такую ​​модульную структуру в rustcодиночку. Сегодня Cargo обычно используется в большинстве случаев. Хотя нижеследующее все еще в силе, некоторые его части (например #![crate_type = ...]) могут показаться странными. Это не рекомендуемое решение.

Чтобы адаптировать ваш пример, мы могли бы начать с этой структуры каталогов:

src/
  lib.rs
  vector.rs
main.rs

Вот ваш main.rs:

extern crate math;

use math::vector;

fn main() {
    println!("{:?}", vector::VectorA::new());
    println!("{:?}", vector::VectorB::new());
}

И ваш src/lib.rs:

#[crate_id = "math"];
#[crate_type = "lib"];

pub mod vector; // exports the module defined in vector.rs

И наконец src/vector.rs:

// exports identifiers from private sub-modules in the current
// module namespace
pub use self::vector_a::VectorA;
pub use self::vector_b::VectorB;

mod vector_b; // private sub-module defined in vector_b.rs

mod vector_a { // private sub-module defined in place
    #[derive(Debug)]
    pub struct VectorA {
        xs: Vec<i64>,
    }

    impl VectorA {
        pub fn new() -> VectorA {
            VectorA { xs: vec![] }
        }
    }
}

И здесь происходит волшебство. Мы определили подмодуль, math::vector::vector_aкоторый имеет некоторую реализацию особого вида вектора. Но мы не хотим, чтобы клиенты вашей библиотеки заботились о наличии vector_aподмодуля. Вместо этого мы хотели бы сделать его доступным в math::vectorмодуле. Это делается с помощью pub use self::vector_a::VectorA, который повторно экспортирует vector_a::VectorAидентификатор в текущий модуль.

Но вы спросили, как это сделать, чтобы можно было поместить свои специальные векторные реализации в разные файлы. Это то, что mod vector_b;делает линия. Он указывает компилятору Rust искать vector_b.rsфайл для реализации этого модуля. И, конечно же, вот наш src/vector_b.rsфайл:

#[derive(Debug)]
pub struct VectorB {
    xs: Vec<i64>,
}

impl VectorB {
    pub fn new() -> VectorB {
        VectorB { xs: vec![] }
    }
}

С точки зрения клиента, тот факт, что VectorAи VectorBопределены в двух разных модулях в двух разных файлах, совершенно непрозрачен.

Если вы находитесь в том же каталоге, что и main.rs, вы сможете запустить его с помощью:

rustc src/lib.rs
rustc -L . main.rs
./main

В целом, глава «Ящики и модули» в книге Rust довольно хороша. Примеров много.

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

src/
  lib.rs
  vector/
      mod.rs
      vector_b.rs
main.rs

Команды для компиляции и запуска также остаются прежними.

BurntSushi5
источник
Думаю, вы неправильно поняли, что я имел в виду под словом «вектор». Я говорил о векторе как о математической величине , а не о структуре данных. Кроме того, я не использую самую последнюю версию ржавчины, потому что создавать ее на окнах немного затруднительно.
starscape
+1 Не совсем то, что мне нужно, но указывало мне правильное направление.
starscape
@EpicPineapple Действительно! И Vec можно использовать для представления таких векторов. (Для большего N, конечно.)
BurntSushi5
1
@EpicPineapple Не могли бы вы объяснить, что я пропустил в моем ответе, чтобы я мог его обновить? Я изо всех сил пытаюсь увидеть разницу между вашим ответом и моим, кроме использования math::Vec2вместо math::vector::Vec2. (То есть та же концепция, но на один модуль глубже.)
BurntSushi5
1
Я не вижу этих критериев в вашем вопросе. Насколько я понимаю, я ответил на заданный вопрос. (Который действительно спрашивал, как отделить модули от файлов.) Извините, что он не работает на Rust 0.9, но это связано с использованием нестабильного языка.
BurntSushi5
42

Правила модуля Rust:

  1. Исходный файл - это просто отдельный модуль (за исключением специальных файлов main.rs, lib.rs и mod.rs).
  2. Каталог - это просто компонент пути к модулю.
  3. Файл mod.rs - это просто модуль каталога.

Файл matrix.rs 1 в каталоге math - это просто модуль math::matrix. Это просто. То, что вы видите в своей файловой системе, вы также найдете в исходном коде. Это взаимно однозначное соответствие путей к файлам и путей к модулям 2 .

Таким образом, вы можете импортировать структуру Matrixс помощью use math::matrix::Matrix, потому что структура находится внутри файла matrix.rs в каталоге math. Не счастлив? Вы бы предпочли use math::Matrix;очень многое, не так ли? Это возможно. Повторно экспортируйте идентификатор math::matrix::Matrixв math / mod.rs с помощью:

pub use self::math::Matrix;

Есть еще один шаг, чтобы заставить это работать. Rust требуется объявление модуля для загрузки модуля. Добавьте mod math;в main.rs. Если вы этого не сделаете, вы получите сообщение об ошибке от компилятора при импорте, например:

error: unresolved import `math::Matrix`. Maybe a missing `extern crate math`?

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

Добавьте это в начало main.rs:

mod math;
pub use math::Matrix;

Декларация модуля также для необходимых подмодулей vector, matrixи complex, поскольку mathнеобходимо загрузить их повторно экспортировать их. Реэкспорт идентификатора работает только в том случае, если вы загрузили модуль идентификатора. Это значит, что для реэкспорта идентификатора math::matrix::Matrixнужно написать mod matrix;. Вы можете сделать это в math / mod.rs. Поэтому создайте файл с таким содержимым:

mod vector;
pub use self::vector::Vector;

mod matrix;
pub use self::matrix::Matrix;

mod complex;
pub use self::complex::Complex;

Аааи все готово.


1 Имена исходных файлов в Rust обычно начинаются со строчной буквы. Вот почему я использую matrix.rs, а не Matrix.rs.

2 Явы разные. Вы packageтоже объявляете путь с помощью . Это избыточно. Путь уже очевиден из местоположения исходного файла в файловой системе. Зачем повторять эту информацию в объявлении вверху файла? Конечно, иногда проще быстро взглянуть на исходный код, чем выяснить расположение файла в файловой системе. Я могу понять людей, которые говорят, что это менее запутанно.

окончательно
источник
25

Пуристы Rusts, вероятно, назовут меня еретиком и возненавидят это решение, но это намного проще: просто сделайте каждую вещь в отдельном файле, а затем используйте макрос « include! » В mod.rs:

include!("math/Matrix.rs");
include!("math/Vector.rs");
include!("math/Complex.rs");

Таким образом, вы не получите дополнительных вложенных модулей и избежите сложных правил экспорта и перезаписи. Просто, эффективно, без суеты.

hasvn
источник
1
Вы просто выбросили пространство имен. Изменение одного файла, не связанного с другим, теперь может повредить другие файлы. Ваше использование «использования» становится негерметичным (т.е. все как use super::*). Вы не можете скрыть код от других файлов (что важно для небезопасного использования безопасных абстракций)
Демур Румед
13
Да, но это именно то, что я хотел в этом случае: иметь несколько файлов, которые ведут себя как один для целей пространственного именования. Я не защищаю это для каждого случая, но это полезный обходной путь, если вы по какой-либо причине не хотите иметь дело с методом «один модуль на файл».
hasvn 06
Это здорово, у меня есть часть моего модуля, которая является только внутренней, но самодостаточной, и это помогло. Я также постараюсь найти подходящее модульное решение, но это не так просто.
rjh
7
Меня не волнует, что меня называют еретиком, ваше решение удобно!
sailfish009
21

Хорошо, некоторое время боролся с моим компилятором и, наконец, заставил его работать (спасибо BurntSushi за указание pub use.

main.rs:

use math::Vec2;
mod math;

fn main() {
  let a = Vec2{x: 10.0, y: 10.0};
  let b = Vec2{x: 20.0, y: 20.0};
}

математика / mod.rs:

pub use self::vector::Vec2;
mod vector;

math / vector.rs

use std::num::sqrt;

pub struct Vec2 {
  x: f64,
  y: f64
}

impl Vec2 {
  pub fn len(&self) -> f64 {
    sqrt(self.x * self.x + self.y * self.y) 
  }

  // other methods...
}

Таким же образом можно добавить и другие структуры. ПРИМЕЧАНИЕ: скомпилирован с помощью 0.9, а не master.

звездный пейзаж
источник
4
Обратите внимание, что вы используете mod math;в main.rsпаре вашу mainпрограмму с вашей библиотекой. Если вы хотите, чтобы ваш mathмодуль был независимым, вам нужно скомпилировать его отдельно и связать с ним extern crate math(как показано в моем ответе). В Rust 0.9 возможно, что extern mod mathвместо этого используется синтаксис .
BurntSushi5
20
Было бы справедливо отметить ответ BurntSushi5 как правильный.
IluTov
2
@NSAddict Нет. Чтобы отделить модули от файлов, не нужно создавать отдельный ящик. Это чрезмерно спроектировано.
nalply
1
Почему это не самый популярный ответ ?? Был задан вопрос, как разбить проект на несколько файлов, что так просто, как показывает этот ответ, а не как разбить его на ящики, что сложнее, и на это ответил @ BurntSushi5 (может быть, вопрос был отредактирован?). ..
Renato
6
Ответ @ BurntSushi5 должен был быть принятым ответом. Это социально неудобно и, может быть, даже означать задать вопрос, получить очень хороший ответ, затем обобщить его как отдельный ответ и отметить свое резюме как принятый.
hasanyasin 02
5

Я хотел бы добавить сюда, как вы включаете файлы Rust, когда они глубоко вложены. У меня такая структура:

|-----main.rs
|-----home/
|---------bathroom/
|-----------------sink.rs
|-----------------toilet.rs

Как получить доступ sink.rsили toilet.rsоткуда main.rs?

Как уже упоминалось, Rust не знает файлов. Вместо этого он видит все как модули и подмодули. Чтобы получить доступ к файлам в каталоге ванной комнаты, вам нужно экспортировать их или поставить наверх. Вы делаете это, указывая имя файла с каталогом, к которому вы хотите получить доступ, и pub mod filename_inside_the_dir_without_rs_extвнутри файла.

Пример.

// sink.rs
pub fn run() { 
    println!("Wash my hands for 20 secs!");
}

// toilet.rs
pub fn run() {
    println!("Ahhh... This is sooo relaxing.")
}
  1. Создайте файл с именем bathroom.rsвнутри homeкаталога:

  2. Экспортируйте имена файлов:

    // bathroom.rs
    pub mod sink;
    pub mod toilet;
  3. Создайте файл с именем home.rs рядом сmain.rs

  4. pub mod файл bathroom.rs

    // home.rs
    pub mod bathroom;
  5. В пределах main.rs

    // main.rs
    // Note: If you mod something, you just specify the 
    // topmost module, in this case, home. 
    mod home;
    
    fn main() {
        home::bathroom::sink::run();
    }

    use также могут использоваться операторы:

    // main.rs
    // Note: If you mod something, you just specify the 
    // topmost module, in this case, home. 
    use home::bathroom::{sink, toilet};
    
    fn main() {
        sink::run();
        sink::toilet();
    }

Включение других родственных модулей (файлов) в подмодули

В случае, если вы хотите использовать sink.rsfrom toilet.rs, вы можете вызвать модуль, указав selfилиsuper ключевые слова .

// inside toilet.rs
use self::sink;
pub fn run() {
  sink::run();
  println!("Ahhh... This is sooo relaxing.")
}

Окончательная структура каталогов

Вы получите что-то вроде этого:

|-----main.rs
|-----home.rs
|-----home/
|---------bathroom.rs
|---------bathroom/
|-----------------sink.rs
|-----------------toilet.rs

Приведенная выше структура работает только с Rust 2018 и новее. Следующая структура каталогов также действительна для 2018 года, но именно так в 2015 году работал.

|-----main.rs
|-----home/
|---------mod.rs
|---------bathroom/
|-----------------mod.rs
|-----------------sink.rs
|-----------------toilet.rs

В котором home/mod.rsто же, что ./home.rsи home/bathroom/mod.rsто же самое, что иhome/bathroom.rs . Rust внес это изменение, потому что компилятор запутается, если вы включите файл с тем же именем, что и каталог. Версия 2018 года (показанная первой) исправляет эту структуру.

См. Это репо для получения дополнительной информации и это видео на YouTube для общего объяснения.

И последнее ... избегайте дефисов! snake_caseВместо этого используйте .

Важная заметка

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

Это означает, что для sink.rsобнаружения toilet.rsвам потребуется использовать их, используя описанные выше методы вплоть доmain.rs !

Другими словами, делать pub mod sink;или use self::sink; внутри неtoilet.rs будет работать, если вы не раскрыли их полностью до main.rs!

Поэтому всегда не забывайте ставить файлы наверх!

Хосе А
источник
2
... это безумно запутано по сравнению с C ++, который что-то говорит
Джозеф Гарвин,