Podziel moduł na kilka plików

102

Chcę mieć moduł z wieloma strukturami, każda w swoim własnym pliku. Na przykładzie Mathmodułu:

Math/
  Vector.rs
  Matrix.rs
  Complex.rs

Chcę, aby każda struktura znajdowała się w tym samym module, którego użyłbym z mojego głównego pliku, na przykład:

use Math::Vector;

fn main() {
  // ...
}

Jednak system modułów Rusta (który na początku jest nieco zagmatwany) nie zapewnia oczywistego sposobu, aby to zrobić. Wydaje się, że pozwala tylko na posiadanie całego modułu w jednym pliku. Czy to nie rustykalne? Jeśli nie, jak mam to zrobić?

starscape
źródło
1
Zinterpretowałem: „Chcę mieć moduł z wieloma strukturami, każda w swoim własnym pliku”. co oznacza, że ​​chcesz, aby każda definicja struktury znajdowała się w jej własnym pliku.
BurntSushi5,
1
Nie byłoby to uważane za rustykalne, chociaż system modułowy z pewnością pozwala na taką strukturę. Generalnie preferowane jest, aby ścieżka modułu bezpośrednio odpowiadała ścieżce systemu plików, np. Struktura foo::bar::Bazpowinna być zdefiniowana w foo/bar.rslub foo/bar/mod.rs.
Chris Morgan,

Odpowiedzi:

111

System modułów Rusta jest w rzeczywistości niesamowicie elastyczny i pozwoli Ci ujawnić dowolną strukturę, ukrywając strukturę kodu w plikach.

Myślę, że kluczem jest tutaj wykorzystanie pub use, które pozwoli na reeksportowanie identyfikatorów z innych modułów. Jest to precedens w std::ioskrzynce Rusta, gdzie niektóre typy z podmodułów są ponownie eksportowane do użytku wstd::io .

Edycja (25.08.2019): poniższa część odpowiedzi została napisana dość dawno temu. Wyjaśnia, jak skonfigurować taką strukturę modułu rustcsamodzielnie. Obecnie w większości przypadków używa się Cargo. Chociaż poniższe informacje są nadal aktualne, niektóre z nich (np. #![crate_type = ...]) Mogą wydawać się dziwne. To nie jest zalecane rozwiązanie.

Aby dostosować Twój przykład, możemy zacząć od tej struktury katalogów:

src/
  lib.rs
  vector.rs
main.rs

Oto twoje main.rs:

extern crate math;

use math::vector;

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

I twój src/lib.rs:

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

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

I wreszcie 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![] }
        }
    }
}

I tu dzieje się magia. Zdefiniowaliśmy podmoduł, math::vector::vector_aktóry ma pewną implementację specjalnego rodzaju wektora. Ale nie chcemy, aby klienci Twojej biblioteki przejmowali się tym, że istnieje vector_apodmoduł. Zamiast tego chcielibyśmy udostępnić go w math::vectormodule. Odbywa się to za pomocą pub use self::vector_a::VectorA, który ponownie eksportuje vector_a::VectorAidentyfikator w bieżącym module.

Ale zapytałeś, jak to zrobić, aby móc umieścić swoje specjalne implementacje wektorowe w różnych plikach. To właśnie mod vector_b;robi linia. Instruuje kompilator Rusta, aby szukał vector_b.rspliku do implementacji tego modułu. I rzeczywiście, oto nasz src/vector_b.rsplik:

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

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

Z punktu widzenia klienta fakt, że VectorAi VectorBsą zdefiniowane w dwóch różnych modułach w dwóch różnych plikach, jest całkowicie nieprzejrzysty.

Jeśli jesteś w tym samym katalogu main.rs, powinieneś móc go uruchomić za pomocą:

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

Ogólnie rozdział „Skrzynie i moduły” w książce Rust jest całkiem niezły. Jest wiele przykładów.

Wreszcie, kompilator Rust automatycznie szuka również podkatalogów. Na przykład powyższy kod będzie działał bez zmian w tej strukturze katalogów:

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

Polecenia kompilacji i uruchomienia również pozostają takie same.

BurntSushi5
źródło
Wydaje mi się, że źle zrozumiałeś, co mam na myśli przez „wektor”. Mówiłem o wektorze jako o wielkości matematycznej , a nie o strukturze danych. Nie używam też najnowszej wersji rdzy, ponieważ tworzenie w systemie Windows jest trochę uciążliwe.
starscape,
+1 nie było dokładnie tym, czego potrzebowałem, ale wskazało mi właściwy kierunek.
pejzaż z gwiazd
@EpicPineapple Rzeczywiście! Do reprezentacji takich wektorów można użyć Vec. (Oczywiście dla większego N).
BurntSushi5
1
@EpicPineapple Czy możesz wyjaśnić, czego brakowało w mojej odpowiedzi, abym mógł ją zaktualizować? Staram się zobaczyć różnicę między twoją a moją odpowiedzią inną niż używanie math::Vec2zamiast math::vector::Vec2. (tj. ta sama koncepcja, ale o jeden moduł głębiej)
BurntSushi5,
1
Nie widzę tych kryteriów w twoim pytaniu. O ile widzę, odpowiedziałem na zadane pytanie. (Który naprawdę pytał, jak oddzielić moduły od plików). Przepraszam, że nie działa w Rust 0.9, ale wiąże się to z obszarem używania niestabilnego języka.
BurntSushi5,
39

Zasady modułu Rust to:

  1. Plik źródłowy jest po prostu własnym modułem (z wyjątkiem specjalnych plików main.rs, lib.rs i mod.rs).
  2. Katalog to tylko składnik ścieżki modułu.
  3. Plik mod.rs jest po prostu modułem katalogu.

Plik matrix.rs 1 w katalogu math to tylko moduł math::matrix. To jest łatwe. To, co widzisz w swoim systemie plików, znajdziesz również w kodzie źródłowym. Jest to zgodność jeden do jednego ścieżek plików i ścieżek modułów 2 .

Możesz więc zaimportować strukturę za Matrixpomocą use math::matrix::Matrix, ponieważ struktura znajduje się w pliku matrix.rs w katalogu matematycznym. Nieszczęśliwy? Zamiast tego wolisz use math::Matrix;bardzo dużo, prawda? To jest możliwe. Ponownie wyeksportuj identyfikator math::matrix::Matrixw math / mod.rs z:

pub use self::math::Matrix;

Jest jeszcze jeden krok, aby to zadziałało. Rust potrzebuje deklaracji modułu, aby załadować moduł. Dodaj mod math;w main.rs. Jeśli tego nie zrobisz, kompilator wyświetli komunikat o błędzie podczas importowania:

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

Wskazówka jest tutaj myląca. Nie ma potrzeby tworzenia dodatkowych skrzynek, z wyjątkiem oczywiście, że naprawdę zamierzasz napisać oddzielną bibliotekę.

Dodaj to na górze main.rs:

mod math;
pub use math::Matrix;

Deklaracja modułu jest również niezbędna dla podmodułów vector, matrixa complexponieważ mathtrzeba je załadować, aby je ponownie wyeksportować. Ponowny eksport identyfikatora działa tylko wtedy, gdy załadowałeś moduł identyfikatora. Oznacza to, że aby ponownie wyeksportować identyfikator, math::matrix::Matrixktóry musisz zapisać mod matrix;. Możesz to zrobić w math / mod.rs. Dlatego utwórz plik z następującą zawartością:

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

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

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

Aaa i gotowe.


1 Nazwy plików źródłowych zwykle rozpoczynają się małą literą w Rust. Dlatego używam matrix.rs, a nie Matrix.rs.

2 Java jest inna. Ty też deklarujesz ścieżkę package. To jest zbędne. Ścieżka jest już widoczna w lokalizacji pliku źródłowego w systemie plików. Po co powtarzać tę informację w deklaracji na początku pliku? Oczywiście czasami łatwiej jest rzucić okiem na kod źródłowy, zamiast znaleźć lokalizację pliku w systemie plików. Rozumiem ludzi, którzy mówią, że to mniej zagmatwane.

nalply
źródło
24

Puryści Rustsa prawdopodobnie będą nazywać mnie heretykiem i nienawidzą tego rozwiązania, ale jest to znacznie prostsze: po prostu zrób każdą rzecz w swoim własnym pliku, a następnie użyj makra „ include! ” W mod.rs:

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

W ten sposób nie masz dodanych zagnieżdżonych modułów i unikniesz skomplikowanych reguł eksportu i przepisywania. Prosty, skuteczny, bez problemów.

hasvn
źródło
1
Właśnie wyrzuciłeś przestrzeń nazw. Zmiana jednego pliku w sposób niezwiązany z innym może teraz uszkodzić inne pliki. Twoje użycie słowa „użyj” staje się nieszczelne (tj. Wszystko jest jak use super::*). Nie możesz ukryć kodu przed innymi plikami (co jest ważne w przypadku niebezpiecznych, bezpiecznych abstrakcji)
Demur Rumed
12
Tak, ale właśnie tego chciałem w tym przypadku: mieć kilka plików, które zachowują się jak jeden dla celów przestrzeni nazw. Nie zalecam tego w każdym przypadku, ale jest to przydatne obejście, jeśli z jakiegoś powodu nie chcesz zajmować się metodą „jeden moduł na plik”.
hasvn
To świetnie, mam część mojego modułu, która jest tylko wewnętrzna, ale samodzielna, i to załatwiło sprawę. Postaram się, aby odpowiednie rozwiązanie modułowe również działało, ale nie jest to tak łatwe.
rjh
6
nie obchodzi mnie bycie heretykiem, twoje rozwiązanie jest wygodne!
sailfish009
21

W porządku, walczyłem przez chwilę z moim kompilatorem i wreszcie udało mi się go uruchomić (dzięki BurntSushi za wskazanie 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};
}

matematyka / mod.rs:

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

matematyka / 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...
}

W ten sam sposób można dodać inne struktury. UWAGA: skompilowane z 0.9, nie master.

starscape
źródło
4
Należy pamiętać, że stosowanie mod math;w main.rspar swoim mainprogramie z biblioteki. Jeśli chcesz, aby Twój mathmoduł był niezależny, musisz go skompilować osobno i połączyć z nim za pomocą extern crate math(jak pokazano w mojej odpowiedzi). W Rust 0.9 możliwe jest, że extern mod mathzamiast tego składnia jest .
BurntSushi5,
20
Oznaczanie odpowiedzi BurntSushi5 jako poprawnej byłoby sprawiedliwe.
IluTov
2
@NSAddict Nie. Aby oddzielić moduły od plików, nie musisz tworzyć osobnej skrzynki. Jest przesadzony.
nalply
1
Dlaczego nie jest to najczęściej wybierana odpowiedź? Pytanie pytało, jak podzielić projekt na kilka plików, co jest tak proste, jak pokazuje ta odpowiedź, a nie jak podzielić go na skrzynki, co jest trudniejsze i na to odpowiedział @ BurntSushi5 (może pytanie zostało zredagowane?). ..
Renato
6
Odpowiedź @ BurntSushi5 powinna być zaakceptowaną odpowiedzią. Jest to niezręczne społecznie, a może nawet oznaczać zadanie pytania, uzyskanie bardzo miłej odpowiedzi, a następnie podsumowanie jej jako osobnej odpowiedzi i oznaczenie podsumowania jako zaakceptowanej odpowiedzi.
hasanyasin
4

Chciałbym tutaj dodać, jak włącza się pliki Rusta, gdy są głęboko zagnieżdżone. Mam następującą strukturę:

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

Jak uzyskujesz dostęp sink.rslub toilet.rsskąd main.rs?

Jak wspominali inni, Rust nie ma wiedzy o plikach. Zamiast tego widzi wszystko jako moduły i podmoduły. Aby uzyskać dostęp do plików w katalogu bathroom, musisz je wyeksportować lub umieścić na górze. Robisz to, określając nazwę pliku z katalogiem, do którego chcesz uzyskać dostęp, i pub mod filename_inside_the_dir_without_rs_extwewnątrz pliku.

Przykład.

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

// toilet.rs
pub fn run() {
    println!("Ahhh... This is sooo relaxing.")
}
  1. Utwórz plik o nazwie bathroom.rswewnątrz homekatalogu:

  2. Wyeksportuj nazwy plików:

    // bathroom.rs
    pub mod sink;
    pub mod toilet;
  3. Utwórz plik o nazwie home.rsobokmain.rs

  4. pub mod plik bathroom.rs

    // home.rs
    pub mod bathroom;
  5. W ciągu 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 można również użyć instrukcji:

    // 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();
    }

Włączanie innych siostrzanych modułów (plików) w podmodułach

W przypadku, gdy chcesz użyć sink.rsfrom toilet.rs, możesz wywołać moduł, określając słowa kluczowe selflub super.

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

Ostateczna struktura katalogów

Skończyłbyś z czymś takim:

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

Powyższa struktura działa tylko od wersji Rust 2018. Poniższa struktura katalogów obowiązuje również w 2018 r., Ale tak było w 2015 r.

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

W którym home/mod.rsjest tym samym co ./home.rsi home/bathroom/mod.rsjest tym samym co home/bathroom.rs. Rust wprowadził tę zmianę, ponieważ kompilator byłby zdezorientowany, gdyby dołączył plik o tej samej nazwie co katalog. Wersja 2018 (ta pokazana jako pierwsza) naprawia tę strukturę.

Zobacz to repozytorium, aby uzyskać więcej informacji, i ten film na YouTube, aby uzyskać ogólne wyjaśnienie.

Ostatnia rzecz ... unikaj myślników! Użyj snake_casezamiast tego.

Ważna uwaga

Państwo musi baryłkę wszystkie pliki na górę, nawet jeśli głębokie pliki nie są wymagane przez nich najwyższego poziomu.

Oznacza to, że aby sink.rsje odkryć toilet.rs, musiałbyś je zlikwidować, stosując powyższe metody aż do main.rs!

Innymi słowy, robi pub mod sink;lub use self::sink; wewnątrz toilet.rsbędzie działać , chyba że wystawiony im przez całą drogę aż do main.rs!

Dlatego zawsze pamiętaj, aby wbić pliki do góry!

Jose A.
źródło
2
... to jest szalenie zagmatwane w porównaniu z C ++, które coś mówi
Joseph Garvin
1
Najlepsza odpowiedź, dzięki.
etech