将一个模块拆分到多个文件中

145

我希望有一个包含多个结构体的模块,每个结构体都在自己的文件中。Math模块为例:

Math/
  Vector.rs
  Matrix.rs
  Complex.rs
我希望每个结构体都在同一个模块中,这样我就可以从我的主文件中使用它们,像这样:
use Math::Vector;

fn main() {
  // ...
}

但是Rust的模块系统(一开始有点令人困惑)似乎没有提供明显的方法来实现这一点。它似乎只允许你将整个模块放在一个文件中。这是否不符合Rust的特性?如果不是,我该怎么做呢?


1
我理解为您想要创建一个模块,其中包含多个结构体,每个结构体都在自己的文件中。 - BurntSushi5
1
这并不被认为是过时的,虽然模块系统肯定允许这样的结构。通常最好让模块路径直接对应于文件系统路径,例如,foo::bar::Baz 结构体应该定义在 foo/bar.rsfoo/bar/mod.rs 中。 - Chris Morgan
似乎每次都变成了crate_name::module::foo::Foo,反复出现,而且还带着过于冗长、无用、重复的描述…… 在Rust领域中,由于难以理解的原因,希望使用目录来组织文件,并将类/结构体/特性/接口等逻辑分割到不同文件中被认为是“异常”的事实,实在令人不安,我个人觉得。 - Joey Sabey
7个回答

137
Rust的模块系统非常灵活,可以让你暴露任何你想要的结构,同时隐藏代码在文件中的结构。关键在于利用pub use,它允许您从其他模块重新导出标识符。在Rust的std::io crate中有先例,其中一些类型从子模块re-exported for use in std::io
为了适应您的示例,我们可以从以下目录结构开始:
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![] }
    }
}

从客户端的角度来看,VectorAVectorB在两个不同的模块和文件中定义这一事实是完全不透明的。

如果你在与main.rs相同的目录中,你应该能够通过以下方式运行它:

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

一般来说,《Rust编程语言》书中的"Crates and Modules"章节相当不错,有很多例子。
最后,Rust编译器还会自动查找子目录。例如,上面的代码在以下目录结构中也可以正常工作:
src/
  lib.rs
  vector/
      mod.rs
      vector_b.rs
main.rs

编译和运行的命令也保持不变。

我相信你误解了我所说的“向量”的意思。我指的是数学量,而不是数据结构。另外,我没有运行最新版本的Rust,因为在Windows上构建有点麻烦。 - starscape
+1 不完全是我需要的,但指引了我正确的方向。 - starscape
@EpicPineapple 当然!而且Vec可以用来表示这样的向量。(当然,对于更大的N。) - BurntSushi5
1
@EpicPineapple,您能否解释一下我的答案缺少了什么,以便我可以更新它?我很难看出您的答案和我的区别,除了使用math::Vec2而不是math::vector::Vec2。(即相同的概念但深入一个模块。) - BurntSushi5
1
我在你的问题中没有看到那个标准。就我所知,我已经回答了所提出的问题。(实际上是在询问如何将模块与文件分离。)很抱歉它不能在Rust 0.9上工作,但这是使用不稳定语言的领域所带来的。 - BurntSushi5
显示剩余3条评论

66

Rust模块规则如下:

  1. 源文件就是它自己的模块(除了特殊文件main.rs、lib.rs和mod.rs)。
  2. 目录就是模块路径组件。
  3. 文件mod.rs 就是 目录的模块。

因此,在math目录中的文件matrix.rs1就是 模块math::matrix。很简单。您在文件系统中看到的也可以在源代码中找到。这是由文件路径和模块路径之间的一对一对应关系实现的。2

因此,您可以使用use math::matrix::Matrix导入结构体Matrix,因为该结构体位于math目录中的文件matrix.rs中。不满意?您更喜欢使用use math::Matrix;吗?这也是可能的。通过以下方式在math/mod.rs中重新导出标识符math::matrix::Matrix

pub use self::math::Matrix;

还有一步需要完成才能使其工作。Rust需要一个模块声明来加载这个模块。在main.rs中添加mod math;。如果您不这样做,当像这样导入时,编译器会提示错误信息:

error: unresolved import `math::Matrix`. Maybe a missing `extern crate math`?
这里的提示有误导性。除非你真的想编写一个单独的库,否则不需要额外的crate。
请将以下代码添加到main.rs文件的顶部:
mod math;
pub use math::Matrix;

模块声明对子模块vectormatrixcomplex也是必要的,因为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;

完成啦。


1Rust中源文件名通常以小写字母开头。这就是为什么我使用matrix.rs而不是Matrix.rs的原因。

2Java则不同。你还需要用package声明路径。这太冗余了。文件系统中源文件的位置已经很明显了,为什么还要在文件顶部重复声明呢?当然有时候查看源代码比查找文件系统中的位置更容易。我理解那些认为这样做会更少让人困惑的人们。


3
顶部的 tl;dr 应该在 Rust 文档中! - YvesgereY
我非常感激周围有人能解释这些东西,因为天哪,Rust的模块系统真是一团糟...抛开还没来得及坐下就有crate、package和module这些事实不谈,还有一堆令人困惑的语法,而其他大多数语言似乎都没有遇到这个问题。extern crate,mod这个,use那个,pub use另一个,再加上cargo文件中一整套独立的语法... - Joey Sabey
这不是一个糟糕的表演,但如果你告诉我它有点奇怪,我同意。 - nalply

33

大部分 Rust 纯粹主义者可能会称我为异端并且憎恨这个解决方案,但这样做更加简单:只需将每个事物放在它自己的文件中,然后在 mod.rs 中使用 "include!" 宏:

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

这样做可以避免添加嵌套模块,并且避免复杂的导出和重写规则。 简单、有效,没有麻烦。


4
您刚才提到了命名空间。现在,对一个文件进行的与其他文件无关的更改可能会导致其他文件出现问题。您对“use”的使用变得不可靠(即所有内容都像“use super::*”)。您无法将代码隐藏在其他文件中(这对于使用不安全的安全抽象很重要)。 - Demur Rumed
17
没错,但那正是我在那种情况下想要的:拥有几个文件,使它们在命名空间方面表现得像一个文件。我不是为每种情况都提倡这种方法,但如果由于某种原因你不想使用“一个文件对应一个模块”的方法,那么这是一个有用的解决方法。 - hasvn
太好了,我的模块中有一部分是仅限内部使用但是自包含的,这个方法解决了问题。我也会尝试让适当的模块解决方案起作用,但这并不容易。 - rjh
11
没关系被称为异端也无所谓,你的解决方案很方便! - sailfish009
此解决方案仅在避免在文件中使用相同的模块时才有效。也就是说,以您的示例为参考,meth/Matrix.rsmath/Vector.rs都不能有use rand;。此外,您不能在各自的文件中有tests子模块。 - Momchil Atanasov
显示剩余2条评论

28

好的,我和编译器搏斗了一段时间,最终让它工作了(感谢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;

数学/向量.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
请注意,在 main.rs 中使用 mod math; 会使您的 main 程序与您的库耦合在一起。如果您希望您的 math 模块独立,您需要单独编译它,并使用 extern crate math 进行链接(如我的答案所示)。在 Rust 0.9 中,语法可能是 extern mod math - BurntSushi5
21
公正的做法应该是将 BurntSushi5 的答案标为正确答案。 - IluTov
2
@NSAddict 不需要创建一个单独的 crate 来将模块与文件分离。那样做太过工程化了。 - nalply
1
为什么这不是最高票答案?问题是如何将项目分成几个文件,这正如这个答案所展示的那样简单,而不是如何将其拆分成板条箱,这更难,也是@BurntSushi5回答的(也许问题已经被编辑了?)... - Renato
8
@BurntSushi5的回答应该被接受为正确答案。在提出问题后得到一个很好的答案,然后将其总结成单独的答案并将您的总结标记为被接受的答案可能会让人感到社交上的尴尬,甚至是不友善的行为。 - hasanyasin
显示剩余2条评论

17

我想在这里补充一下如何在Rust中包含深度嵌套的文件。 我有以下结构:

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

如何从main.rs访问sink.rstoilet.rs

正如其他人提到的,Rust不知道文件。相反,它将所有内容视为模块和子模块。要访问浴室目录中的文件,您需要导出它们或在顶部加上barrel。您可以通过在文件中指定文件名以及不带rs扩展名的目录中的文件名 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. home目录下创建一个名为bathroom.rs的文件:

  2. 导出文件名:

// bathroom.rs
pub mod sink;
pub mod toilet;
  • 创建一个名为home.rs的文件,放在main.rs旁边

  • pub mod bathroom.rs文件

  • // home.rs
    pub mod bathroom;
    
  • 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();
    }
    

    在子模块中包含其他同级模块(文件)

    如果你想在toilet.rs中使用sink.rs,可以通过指定selfsuper关键字来调用模块。

    // 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.rshome/bathroom.rs相同。Rust进行了此更改,因为编译器会因为包含与目录同名的文件而感到困惑。2018年版(首先显示的版本)修复了这个结构。

    有关更多信息,请参见此存储库,并查看此YouTube视频以获取整体说明。

    还有一件事...避免使用连字符!请改用snake_case

    重要提示

    您必须将所有文件都列在最上面,即使顶层文件不需要深度文件。

    这意味着,为了让sink.rs发现toilet.rs,您需要通过上述方法将它们全部列出,直到main.rs

    换句话说,在toilet.rs内执行pub mod sink;use self::sink;将不起作用,除非您已将它们全部公开到main.rs

    因此,请记得将您的文件罗列到顶部!


    12
    与 C++ 相比,这个东西相当复杂。这是在说些什么。 - Joseph Garvin
    另外一件事,记得将你的文件和文件夹放在 src 文件夹内。 - Jose A

    5

    我从Github上学到了一种更加高效的导出模块方法。

    mod foo {
        //! inner docstring comment 1
        //! inner docstring comment 2
    
        mod a;
        mod b;
    
        pub use a::*;
        pub use b::*;
    }
    

    作为 Rust 的新手,这对我来说已经足够好了。谢谢! - Gabriel

    1

    将问题示例目录和文件名调整为符合Rust命名约定:

    main.rs
    math.rs
    math/
      vector.rs
      matrix.rs
      complex.rs
    

    请确保在math目录中的每个文件中导出公共符号(类型、函数等),方法是在它们之前加上关键字pub

    定义math.rs

    mod vector;
    pub use vector::*;
    
    mod matrix;
    pub use matrix::*;
    
    mod complex;
    pub use complex::*;
    

    上述文件将math的子模块保持为私有,但子模块的公共符号从模块math中导出。这有效地扁平化了模块结构。

    main.rs中使用math::Vector

    mod math;
    
    use crate::math::Vector;
    
    fn main() {
      // ...
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接