箱和模块

欢马劈雪     最近更新时间:2020-08-04 05:37:59

190

当一个项目开始变大时,我们通常认为的良好的软件工程实践是把它分割成小块,然后把它们组合在一起。同样重要的是有一个定义良好的接口,这样你的一些功能可以是私人的,另外一些可以是公开的。为了促进这些事情,Rust 使用了模块系统。

基本术语:箱和模块

关与模块系统,Rust 有两个不同的术语:“箱” 和“模块”。 在其他语言里箱的代名词是 “库” 或 “包”。因此 “Cargo” 就是 Rust语言的的包管理工具:你可以用Cargo 装载你的箱转运给其他程序。根据不同的项目,箱可以产生一个可执行文件或库。

每个箱有一个包含箱代码的隐式根模块。然后,您可以定义一个根模块下的子树模块。 模块允许你分区箱内箱外代码。

作为一个例子,让我们做一个短语箱,它将在不同的语言中给我们不同的词语。为简单起见,我们将使用 “问候” 和 “告别” 两种类型的短语,并使用英语和日语 (日本语) 两种语言。我们将使用下面这个模块布局:

image

在这个例子中,短语是我们箱的名字。其余都是模块。你可以看到,他们形成一个树,分支从箱根发出,根指的是树的根:短语本身。

现在我们有一个计划,让我们来在代码中定义这些模块。首先,用 Cargo 生成一个新的箱:

    $ cargo new phrases
    $ cd phrases

如果你记得以前所讲的,这将为我们生成一个简单的项目:

    $ tree .
    .
    ├── Cargo.toml
    └── src
    └── lib.rs

    1 directory, 2 files

src/lib.rs 是我们箱根,对应于我们在上图中的短语。

定义模块

我们使用 mod 关键字来定义我们的每个模块。让我们使我们的 src/lib.rs,看起来就像这样:

    mod english {
    mod greetings {
    }

    mod farewells {
    }
    }

    mod japanese {
    mod greetings {
    }

    mod farewells {
    }
    }

在 mod 关键字后,我们给出模块的名称。模块名称遵守 Rus t规定的标识符命名规则:lower_snake_case。每个模块的内容在花括号 ({ }) 里面。

在一个给定的模式下,您可以声明 sub-mods。我们可以用双冒号 (::) 符号引用子模块:我们的四个嵌套模块是 english::greetings, english::farewells, japanese::greetings, 还有 japanese::farewells。因为这些子模块是在他们父模块命名空间命名的,名字不冲突: english::greetings 和 japanese::greetings 是不同的,尽管他们的名字都是问候。

因为这个箱子没有 main() 函数,并且被称为 lib.rs, Cargo 将把这个箱建成一个库:

    $ cargo build
       Compiling phrases v0.0.1 (file:///home/you/projects/phrases)
    $ ls target/debug
    build  deps  examples  libphrases-a7448e02a0468eaa.rlib  native

libphrase-hash.rlib 是编译后的箱。在我们知道如何在另一个箱里面使用这个箱之前,让我们把它分成多个文件。

多个文件箱

如果每个箱只是一个文件,那么这些文件会很大。我们常常很容易将箱分成多个文件,并且 Rust 从两个方面来支持这样做。

不是像下面这样声明一个模块:

    mod english {
    // contents of our module go here
    }
    ⇱

相反我们可以这样声明我们的模块:

    mod english;

如果我们这样做,Rust 将期望找到一个 english.rs 文件,或者是含有我们的模块的内容的 english/mod.rs 文件。

注意,在这些文件中,您不需要 re-declare 模块:这些已经由最初的模块的声明了。

使用这两种技巧,我们可以把箱子拆分成两个目录和七个文件:

image

src/lib.rs 是我们箱根,看起来像这样:

    mod english;
    mod japanese;

这两个声明告诉 Rust 根据我们的偏好去寻找 src/english.rssrc/japanese.rs, 或者 src/english/mod.rssrc/japanese/mod.rs。在这种情况下,由于我们的模块有子模块,我们就选择第二个。src/english/mod.rssrc/japanese/mod.rs 看起来都像这样:

    mod greetings;
    mod farewells; 

再一次,这些声明告诉 Rust 去寻找 src/english/greetings.rssrc/japanese/greetings.rs 或者 src/english/farewells/mod.rssrc/japanese/farewells/mod.rs。因为这些子模块没有自己的子模块,我们选择让他们 src/english/greetings.rssrc/japanese/farewells.rs

src/english/greetings.rssrc/japanese/farewells.rs 的内容在此时都是空的。让我们添加一些函数。

把下面这些放到 src/english/greetings.rs 里面:

    fn hello() -> String {
    "Hello!".to_string()
    }
    把下面这些放到src/english/farewells.rs:
    fn goodbye() -> String {
    "Goodbye.".to_string()
    }
    src/japanese/greetings.rs:
    fn hello() -> String {
    "こんにちは".to_string()
    }

当然,你可以从这个网页复制和粘贴这些或者自己敲一些其他的东西。你用 “konnichiwa” 还是其他的什么学习模块系统实际上并不重要。

把下面这些放到 src /日本/ farewells.rs

    fn goodbye() -> String {
    "さようなら".to_string()
    }

(如果你好奇的话,可以告诉你这是 “Sayōnara”。)

现在,我们的箱具有一些功能,让我们试着从另一个箱使用这些功能。

导入外部箱

我们有一个库箱。让我们做一个可执行的箱,这个箱导入和并且使用我们的库。

生成一个 src/main.rs 并且把下面这些代码输进去(此时还不会完全编译):

    extern crate phrases;

    fn main() {
    println!("Hello in English: {}", phrases::english::greetings::hello());
    println!("Goodbye in English: {}", phrases::english::farewells::goodbye());

    println!("Hello in Japanese: {}", phrases::japanese::greetings::hello());
    println!("Goodbye in Japanese: {}", phrases::japanese::farewells::goodbye());
    }

外面的箱声明告诉,我们需要编译和链接短语箱。我们可以使用短语“模块。如前所述,您可以使用双冒号来引用子模块的内部功能。

另外, Cargo 假设 src/main.rs 是一个二进制箱的根箱,而不是一个箱库。我们的包现在有两个箱: src/lib.rs 以及 src/main.rs。对可执行文件箱来说,这种模式是很常见的:大多数功能都是在库箱里面,并且可执行箱将使用这个库。在这种方式下,其他程序也可以使用库箱,这也是一个不错的关注点分离方法。

然而这并不管用。我们得到了类似下面四个的错误:

    $ cargo build
       Compiling phrases v0.0.1 (file:///home/you/projects/phrases)
    src/main.rs:4:38: 4:72 error: function `hello` is private
    src/main.rs:4 println!("Hello in English: {}", phrases::english::greetings::hello());

    note: in expansion of format_args!
    <std macros>:2:25: 2:58 note: expansion site
    <std macros>:1:1: 2:62 note: in expansion of print!
    <std macros>:3:1: 3:54 note: expansion site
    <std macros>:1:1: 3:58 note: in expansion of println!
    phrases/src/main.rs:4:5: 4:76 note: expansion site

默认情况下,在 Rus t语言里面一切都是非公开的。让我们从更深的层次来谈一下。

导出一个公共接口

在默认情况下,Rust 可以精确地控制你的接口的哪些方面是公开的,哪些方面是非公开的。要把某些事物公开,你需要使用使用 pub 关键字。让我们首先关注 english 模块,然后让我们减小我们的 src/main.rs 到下面这样:

    extern crate phrases;

    fn main() {
    println!("Hello in English: {}", phrases::english::greetings::hello());
    println!("Goodbye in English: {}", phrases::english::farewells::goodbye());
    }

在我们的 src/english/mod.rs 里面,让我们添加 pub 到英语模块声明里面:

    pub mod english;
    mod japanese;

并且在我们的 src/english/mod.rs 里面,我们写两个 pub 语句:

    pub mod greetings;
    pub mod farewells;

在我们的 src/english/greetings.rs 里面,我们添加 pub 到 fn 的声明里面:

pub fn hello() -> String {
"Hello!".to_string()
}

也在 src/english/farewells.rs 里面这样做:

    pub fn goodbye() -> String {
    "Goodbye.".to_string()
    }

现在,虽然有警告告诉我们不能使用带有日语的函数,我们的箱依然进行编译:

    $ cargo run
       Compiling phrases v0.0.1 (file:///home/you/projects/phrases)
    src/japanese/greetings.rs:1:1: 3:2 warning: function is never used: `hello`, #[warn(dead_code)] on by default
    src/japanese/greetings.rs:1 fn hello() -> String {
    src/japanese/greetings.rs:2 "こんにちは".to_string()
    src/japanese/greetings.rs:3 }
    src/japanese/farewells.rs:1:1: 3:2 warning: function is never used: `goodbye`, #[warn(dead_code)] on by default
    src/japanese/farewells.rs:1 fn goodbye() -> String {
    src/japanese/farewells.rs:2 "さようなら".to_string()
    src/japanese/farewells.rs:3 }
     Running `target/debug/phrases`
    Hello in English: Hello!
    Goodbye in English: Goodbye.

现在,我们的函数是公开的,我们可以使用它们。太棒了!然而,输入 phrases::english::greetings::hello() 太长而且重复。Rust 还有另一个关键字可以导入名称到当前的范围,这样你可以用更短的名字来引用他们。让我们谈谈 use。

用 use 导入模块

Rust 有一个 use 关键字,它允许我们将名称导入本地范围。让我们改变我们的 src/main.rs 成下面这样:

    extern crate phrases;

    use phrases::english::greetings;
    use phrases::english::farewells;

    fn main() {
    println!("Hello in English: {}", greetings::hello());
    println!("Goodbye in English: {}", farewells::goodbye());
    }

两个 use 行将每个模块导入到本地范围,所以我们可以用更短的名称来调用函数。按照惯例,在导入功能时,通常认为最好的做法是导入模块而不是直接导入函数。换句话说,你可以这样做:

    extern crate phrases;

    use phrases::english::greetings::hello;
    use phrases::english::farewells::goodbye;

    fn main() {
    println!("Hello in English: {}", hello());
    println!("Goodbye in English: {}", goodbye());
    }

但它不是惯用的方法。这是更有可能引入命名冲突。在我们的短程序里面,这不是一个大问题,但是当它在大型程序里面就变成一个问题了。如果我们有相互矛盾的名字,Rust 会给出一个编译错误。例如,如果我们用 public 修饰日语函数,并试图做到这一点:

    extern crate phrases;

    use phrases::english::greetings::hello;
    use phrases::japanese::greetings::hello;

    fn main() {
    println!("Hello in English: {}", hello());
    println!("Hello in Japanese: {}", hello());
    }

Rust 将给我们一个编译时的错误:

    Compiling phrases v0.0.1 (file:///home/you/projects/phrases)
    src/main.rs:4:5: 4:40 error: a value named `hello` has already been imported in this module [E0252]
    src/main.rs:4 use phrases::japanese::greetings::hello;

    error: aborting due to previous error
    Could not compile `phrases`.

如果我们从相同的模块导入多个名称,我们不需要输入两次。不必像下面这样:

    use phrases::english::greetings;
    use phrases::english::farewells;

我们可以使用这个快捷键:

    use phrases::english::{greetings, farewells};

用 pub use 重新导出

你不要只是使用 use 关键字来缩短标识符。您还可以在你的箱里使用它去再次导入一个在另一个模块里的函数。这允许您呈现一个外部接口,并且这个接口可以并不直接映射到您的内部代码组织。

让我们来看一个例子。修改您的 src/main.rs 像下面这样:

展开阅读全文