Skip to content

使用包、Crate和模块

https://docs.rs/

https://crates.io/

当你编写大型程序时,组织你的代码显得尤为重要,因为你想在脑海中通晓整个程序,那几乎是不可能完成的。
通过对相关功能进行分组和划分不同功能的代码,你可以清楚在哪里可以找到实现了特定功能的代码,以及在哪里可以改变一个功能的工作方式。

Rust 有许多功能可以让你管理代码的组织,包括哪些内容可以被公开,哪些内容作为私有部分,以及程序每个作用域中的名字。
这些功能。这有时被称为 “模块系统(the module system)”,包括:

  • (Packages): Cargo 的一个功能,它允许你构建、测试和分享 crate。
  • Crates:一个模块的树形结构,它形成了库或二进制项目。
  • 模块(Modules)和use: 允许你控制作用域和路径的私有性。
  • 路径(path):一个命名例如结构体、函数或模块等项的方式

包和 crate

包(package) 是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。

包中所包含的内容由几条规则来确立。一个包中至多 只能 包含一个库 crate(library crate);包中可以包含任意多个二进制 crate(binary crate);包中至少包含一个 crate,无论是库的还是二进制的。

让我们来看看创建包的时候会发生什么。首先,我们输入命令 cargo new:

1
2
3
4
5
6
7
$ cargo new my-project
     Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

当我们输入了这条命令,Cargo 会给我们的包创建一个 Cargo.toml 文件。
查看 Cargo.toml 的内容,会发现并没有提到 src/main.rs,因为 Cargo 遵循的一个约定:src/main.rs 就是一个与包同名的二进制 crate 的 crate 根。
同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。
crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目。

在此,我们有了一个只包含 src/main.rs 的包,意味着它只含有一个名为 my-project 的二进制 crate。
如果一个包同时含有 src/main.rs 和 src/lib.rs,则它有两个 crate:一个库和一个二进制项,且名字都与包相同。
通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。

一个 crate 会将一个作用域内的相关功能分组到一起,使得该功能可以很方便地在多个项目之间共享。

定义模块来控制作用域与私有性

模块 让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。
模块还可以控制项的 私有性,即项是可以被外部代码使用的(public),还是作为一个内部实现的内容,不能被外部代码使用(private)。

在餐饮业,餐馆中会有一些地方被称之为 前台(front of house),还有另外一些地方被称之为 后台(back of house)。
前台是招待顾客的地方,在这里,店主可以为顾客安排座位,服务员接受顾客下单和付款,调酒师会制作饮品。
后台则是由厨师工作的厨房,洗碗工的工作地点,以及经理做行政工作的地方组成。

我们可以将函数放置到嵌套的模块中,来使我们的 crate 结构与实际的餐厅结构相同。
通过执行cargo new --lib restaurant,来创建一个新的名为 restaurant 的库。
然后将示例中的代码放入src/lib.rs中,来定义一些模块和函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn server_order() {}

        fn take_payment() {}
    }
}

我们定义一个模块,是以 mod 关键字为起始,然后指定模块的名字(本例中叫做 front_of_house),并且用花括号包围模块的主体。
在模块内,我们还可以定义其他的模块,就像本例中的 hosting 和 serving 模块。
模块还可以保存一些定义的其他项,比如结构体、枚举、常量、特性、或者函数。

通过使用模块,我们可以将相关的定义分组到一起,并指出他们为什么相关。
程序员可以通过使用这段代码,更加容易地找到他们想要的定义,因为他们可以基于分组来对代码进行导航,而不需要阅读所有的定义。
程序员向这段代码中添加一个新的功能时,他们也会知道代码应该放置在何处,可以保持程序的组织性。

在前面我们提到了,src/main.rs 和 src/lib.rs 叫做 crate 根。
之所以这样叫它们的原因是,这两个文件的内容都是一个从名为 crate 的模块作为根的 crate 模块结构,称为模块树(module tree)。

1
2
3
4
5
6
7
8
9
crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

这个树展示了一些模块是如何被嵌入到另一个模块的(例如,hosting 嵌套在 front_of_house 中)。
这个树还展示了一些模块是互为 兄弟(siblings) 的,这意味着它们定义在同一模块中(hosting 和 serving 被一起定义在 front_of_house 中)。
继续沿用家庭关系的比喻,如果一个模块 A 被包含在模块 B 中,我们将模块 A 称为模块 B 的 子(child),模块 B 则是模块 A 的 父(parent)。
注意,整个模块树都植根于名为 crate 的隐式模块下。

这个模块树可能会令你想起电脑上文件系统的目录树;这是一个非常恰当的比喻!
就像文件系统的目录,你可以使用模块来组织你的代码。
并且,就像目录中的文件,我们需要一种方法来找到模块。

定义一个名为 sound 的模块,其包含名为 guitar 的函数。

文件名:src/main.rs:

1
2
3
4
5
6
7
8
9
mod sound {
    fn guitar() {
        // 函数体
    }
}

fn main() {

}

这里定义了两个函数,guitar 和 main。guitar 函数定义于 mod 块中。这个块定义了 sound 模块。

为了将代码组织到模块层次体系中,可以将模块嵌套进其他模块

文件名: src/main.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
mod sound {
    mod instrument {
        mod woodwind {
            fn clarinet() {
                // 函数体
            }
        }
    }

    mod voice {

    }
}

fn main() {

}

在这个例子中,我们定义了 sound 模块。接着在 sound 模块中定义了 instrument 和 voice 模块。instrument 模块中定义了另一个模块 woodwind,这个模块包含一个函数 clarinet。

src/main.rs 和 src/lib.rs 被称为 crate 根。
他们被称为 crate 根是因为这两个文件在 crate 模块树的根组成了名为 crate 模块

1
2
3
4
5
crate
└── sound
    ├── instrument
    │   └── woodwind
    └── voice

这个树展示了模块如何嵌套在其他模块中(比如 woodwind 嵌套在 instrument 中)以及模块如何作为其他模块的子模块的(instrument 和 voice 都定义在 sound 中)。
整个模块树都位于名为 crate 这个隐式模块的根下。

这个树可能会令你想起电脑上文件系统的目录树;这是一个非常恰当的比喻!就像文件系统的目录,将代码放入任意模块也将创建对应的组织结构体。另一个相似点是为了引用文件系统或模块树中的项,需要使用 路径(path)。

路径用于引用模块树中的项

来看一下 Rust 如何在模块树中找到一个项的位置,我们使用路径的方式,就像在文件系统使用路径一样。
如果我们想要调用一个函数,我们需要知道它的路径。

路径有两种形式:

  • 绝对路径(absolute path)从 crate 根开始,以 crate 名或者字面值 crate 开头。
  • 相对路径(relative path)从当前模块开始,以 self、super 或当前模块的标识符开头。

绝对路径和相对路径都后跟一个或多个由双冒号(::)分割的标识符。

文件名: src/main.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mod sound {
    mod instrument {
        fn clarinet() {
            // 函数体
        }
    }
}

fn main() {
    // 绝对路径
    crate::sound::instrument::clarinet();

    // Relative path
    sound::instrument::clarinet();
}

第一种从 main 函数中调用 clarinet 函数的方式使用绝对路径。
因为 clarinet 与 main 定义于同一 crate 中,我们使用 crate 关键字来开始绝对路径。
接着包含每一个模块直到 clarinet。这类似于指定 /sound/instrument/clarinet 来运行电脑上这个位置的程序;
使用 crate 从 crate 根开始就类似于在 shell 中使用 / 从文件系统根开始。

第二种从 main 函数中调用 clarinet 函数的方式使用相对路径。
该路径以 sound 开始,它是定义于与 main 函数相同模块树级别的模块。
这类似于指定 sound/instrument/clarinet 来运行电脑上这个位置的程序;以名称开头意味着路径是相对的。

它并不能编译:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ cargo build
   Compiling sampleproject v0.1.0 (file:///projects/sampleproject)
error[E0603]: module `instrument` is private
  --> src/main.rs:11:19
   |
11 |     crate::sound::instrument::clarinet();
   |                   ^^^^^^^^^^

error[E0603]: module `instrument` is private
  --> src/main.rs:14:12
   |
14 |     sound::instrument::clarinet();
   |            ^^^^^^^^^^

错误信息说 instrument 模块是私有的。
可以看到 instrument 模块和 clarinet 函数的路径是正确的,不过 Rust 不让我们使用,因为他们是私有的。
现在是学习 pub 关键字的时候了!

使用 pub 关键字使项变为公有

让我们使用 pub 关键字标记 instrument 模块使其可以在 main 函数中使用

文件名: src/main.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mod sound {
    pub mod instrument {
        fn clarinet() {
            // 函数体
        }
    }
}

fn main() {
    // Absolute path
    crate::sound::instrument::clarinet();

    // Relative path
    sound::instrument::clarinet();
}

在 mod instrument 之前增加 pub 关键字使得模块变为公有。
通过这个改变如果允许访问 sound 的话,我们就可以访问 instrument。
instrument 的内容仍然是私有的;使得模块公有并不使其内容也是公有的。
模块上的 pub 关键字允许其父模块引用它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ cargo build
   Compiling sampleproject v0.1.0 (file:///projects/sampleproject)
error[E0603]: function `clarinet` is private
  --> src/main.rs:11:31
   |
11 |     crate::sound::instrument::clarinet();
   |                               ^^^^^^^^

error[E0603]: function `clarinet` is private
  --> src/main.rs:14:24
   |
14 |     sound::instrument::clarinet();
   |                        ^^^^^^^^

现在的错误表明 clarinet 函数是私有的。私有性规则适用于结构体、枚举、函数和方法以及模块。

在 clarinet 函数前增加 pub 关键字使其变为公有

文件名: src/main.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mod sound {
    pub mod instrument {
        pub fn clarinet() {
            // 函数体
        }
    }
}

fn main() {
    // 绝对路径
    crate::sound::instrument::clarinet();

    // 相对路径
    sound::instrument::clarinet();
}

现在可以编译了!

在绝对路径的情况下,我们从 crate,也就是 crate 根开始。
从这开始有 sound,这是一个定义于 crate 根中的模块。
sound 模块不是公有的,不过因为 main 函数与 sound 定义于同一模块中,可以从 main 中引用 sound。
接下来是 instrument,这个模块标记为 pub。我们可以访问 instrument 的父模块,所以可以访问 instrument。
最后,clarinet 函数被标记为 pub 所以可以访问其父模块,所以这个函数调用是有效的!

在相对路径的情况下,其逻辑与绝对路径相同,除了第一步。
不同于从 crate 根开始,路径从 sound 开始。sound 模块与 main 定义于同一模块,所以从 main 所在模块开始定义的路径是有效的。
接下来因为 instrument 和 clarinet 被标记为 pub,路径其余的部分也是有效的,因此函数调用也是有效的!

使用 super 开始相对路径

也可以使用 super 开头来构建相对路径。这么做类似于文件系统中以 .. 开头:该路径从 父 模块开始而不是当前模块。

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
mod instrument {
    fn clarinet() {
        super::breathe_in();
    }
}

fn breathe_in() {
    // 函数体
}

clarinet 函数位于 instrument 模块中,所以可以使用 super 进入 instrument 的父模块,也就是根 crate。
从这里可以找到 breathe_in。成功!

你可能想要使用 super 开头的相对路径而不是以 crate 开头的绝对路径的原因是 super 可能会使修改有着不同模块层级结构的代码变得更容易,如果定义项和调用项的代码被一同移动的话。
例如,如果我们决定将 instrument 模块和 breathe_in 函数放入 sound 模块中,这时我们只需增加 sound 模块即可

文件名: src/lib.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
mod sound {
    mod instrument {
        fn clarinet() {
            super::breathe_in();
        }
    }

    fn breathe_in() {
        // 函数体
    }
}

对结构体和枚举使用 pub

可以以模块与函数相同的方式来设计公有的结构体和枚举,不过有一些额外的细节。

如果在结构体定义中使用 pub,可以使结构体公有。然而结构体的字段仍是私有的。
可以在每一个字段的基准上选择其是否公有。
在示例中定义了一个公有结构体 plant::Vegetable,其包含公有的 name 字段和私有的 id 字段。

文件名: src/main.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
mod plant {
    pub struct Vegetable {
        pub name: String,
        id: i32,
    }

    impl Vegetable {
        pub fn new(name: &str) -> Vegetable {
            Vegetable {
                name: String::from(name),
                id: 1,
            }
        }
    }
}

fn main() {
    let mut v = plant::Vegetable::new("squash");

    v.name = String::from("butternut squash");
    println!("{} are delicious", v.name);

    // 如果将如下行取消注释代码将无法编译:
    // println!("The ID is {}", v.id);
}

因为 plant::Vegetable 结构体的 name 字段使公有的,在 main 中可以使用点号读写 name 字段。
不允许在 main 中使用 id 字段因为其使私有的。
尝试取消注释的行来打印 id 字段的值来看看会出现什么错误!
另外注意因为 plant::Vegetable 有私有字段,需要提供一个公有的关联函数来构建 Vegetable 的实例(这里使用了传统的名称 new)。
如果 Vegetable 没有提供这么一个函数,我们就不能在 main 中创建 Vegetable 的实例,因为在 main 中不允许设置私有字段 id 的值。

相反,如果有一个公有枚举,其所有成员都是公有。只需在 enum 关键词前加上 pub

文件名: src/main.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
mod menu {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

fn main() {
    let order1 = menu::Appetizer::Soup;
    let order2 = menu::Appetizer::Salad;
}

因为 Appetizer 枚举是公有的,可以在 main 中使用 Soup and Salad 成员。

使用 use 关键字将名称引入作用域

当我们选择 clarinet 函数的绝对或相对路径时,每次想要调用 clarinet 时都不得不也指定 sound 和 instrument。
幸运的是,有一次性将路径引入作用域然后就像调用本地项那样的方法:使用 use 关键字

在示例中将 crate::sound::instrument 模块引入了 main 函数的作用域,以便只需指 instrument::clarinet 来调用 clarinet 函数。

文件名: src/main.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mod sound {
    pub mod instrument {
        pub fn clarinet() {
            // 函数体
        }
    }
}

use crate::sound::instrument;

fn main() {
    instrument::clarinet();
    instrument::clarinet();
    instrument::clarinet();
}

在作用域中增加 use 和路径类似于在文件系统中创建软连接(符号连接,symbolic link)。
通过在 crate 根增加 use crate::sound::instrument,现在 instrument 在作用域中就是有效的名称了,如同它被定义于 crate 根一样。
现在,既可以使用老的全路径方式取得 instrument 模块的项,也可以使用新的通过 use 创建的更短的路径。
通过 use 引入作用域的路径也会检查私有性,同其它路径一样。

使用 use 将模块引入作用域

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

通过在 crate 根增加 use crate::front_of_house::hosting,现在 hosting 在作用域中就是有效的名称了,如同 hosting 模块被定义于 crate 根一样。

还可以使用 use 和相对路径来将一个项引入作用域。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

创建惯用的 use 路径

使用 use 将 add_to_waitlist 函数引入作用域,这并不符合习惯

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
    add_to_waitlist();
    add_to_waitlist();
}

要想使用 use 将函数的父模块引入作用域,我们必须在调用函数时指定父模块,这样可以清晰地表明函数不是在本地定义的,同时使完整路径的重复度最小化。

另一方面,使用 use 引入结构体、枚举和其他项时,习惯是指定它们的完整路径。

1
2
3
4
5
6
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

这种习惯用法背后没有什么硬性要求:它只是一种惯例,人们已经习惯了以这种方式阅读和编写 Rust 代码。

这个习惯用法有一个例外,那就是我们想使用 use 语句将两个具有相同名称的项带入作用域,因为 Rust 不允许这样做。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
}

fn function2() -> io::Result<()> {
    // --snip--
}

如你所见,使用父模块可以区分这两个 Result 类型。
如果我们是指定use std::fmt::Resultuse std::io::Result,我们将在同一作用域拥有了两个 Result 类型,当我们使用 Result 时,Rust 则不知道我们要用的是哪个。

使用 as 关键字提供新的名称

使用 use 将两个同名类型引入同一作用域这个问题还有另一个解决办法:在这个类型的路径后面,我们使用 as 指定一个新的本地名称或者别名。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
}

fn function2() -> IoResult<()> {
    // --snip--
}

在第二个 use 语句中,我们选择 IoResult 作为 std::io::Result 的新名称,它与从 std::fmt 引入作用域的 Result 并不冲突。

使用 pub use 重导出名称

当使用 use 关键字将名称导入作用域时,在新作用域中可用的名称是私有的。
如果为了让调用你编写的代码的代码能够像在自己的作用域内引用这些类型,可以结合 pub 和 use。
这个技术被称为 “重导出(re-exporting)”,因为这样做将项引入作用域并同时使其可供其他代码引入自己的作用域。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

通过 pub use,现在可以通过新路径 hosting::add_to_waitlist 来调用 add_to_waitlist 函数。
如果没有指定 pub use,eat_at_restaurant 函数可以在其作用域中调用 hosting::add_to_waitlist,但外部代码则不允许使用这个新路径。

当你的代码的内部结构与调用你的代码的程序员的思考领域不同时,重导出会很有用。
例如,在这个餐馆的比喻中,经营餐馆的人会想到“前台”和“后台”。
但顾客在光顾一家餐馆时,可能不会以这些术语来考虑餐馆的各个部分。
使用 pub use,我们可以使用一种结构编写代码,却将不同的结构形式暴露出来。
这样做使我们的库井井有条,方便开发这个库的程序员和调用这个库的程序员之间组织起来。

使用外部包

文件名: Cargo.toml

1
2
[dependencies]
rand = "0.5.5"

在 Cargo.toml 中加入 rand 依赖告诉了 Cargo 要从 crates.io 下载 rand 和其依赖,并使其可在项目代码中使用。

接着,为了将 rand 定义引入项目包的作用域,我们加入一行 use 起始的包名,它以 rand 包名开头并列出了需要引入作用域的项。

1
2
3
4
5
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 101);
}

注意标准库(std)对于你的包来说也是外部 crate。
因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml 来引入 std,不过需要通过 use 将标准库中定义的项引入项目包的作用域中来引用它们,比如我们使用的 HashMap:

1
use std::collections::HashMap;

嵌套路径来消除大量的 use 行

当需要引入很多定义于相同包或相同模块的项时,为每一项单独列出一行会占用源码很大的空间

1
2
3
use std::cmp::Ordering;
use std::io;
// ---snip---

相反,我们可以使用嵌套路径将相同的项在一行中引入作用域。
这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分

1
2
use std::{cmp::Ordering, io};
// ---snip---

在较大的程序中,使用嵌套路径从相同包或模块中引入很多项,可以显著减少所需的独立 use 语句的数量!

我们可以在路径的任何层级使用嵌套路径,这在组合两个共享子路径的 use 语句时非常有用。

1
2
use std::io;
use std::io::Write;

两个路径的相同部分是 std::io,这正是第一个路径。为了在一行 use 语句中引入这两个路径,可以在嵌套路径中使用 self

1
use std::io::{self, Write};

通过 glob 运算符将所有的公有定义引入作用域

如果希望将一个路径下 所有 公有项引入作用域,可以指定路径后跟 *,glob 运算符:

1
use std::collections::*;

这个 use 语句将 std::collections 中定义的所有公有项引入当前作用域。
使用 glob 运算符时请多加小心!
Glob 会使得我们难以推导作用域中有什么名称和它们是在何处定义的。

glob 运算符经常用于测试模块 tests 中,这时会将所有内容引入作用域

将模块分割进不同文件

当模块变得更大时,你可能想要将它们的定义移动到一个单独的文件中使代码更容易阅读。

从这个文件开始修改, 文件名: src/main.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mod sound {
    pub mod instrument {
        pub fn clarinet() {
            // 函数体
        }
    }
}

fn main() {
    // 绝对路径
    crate::sound::instrument::clarinet();

    // 相对路径
    sound::instrument::clarinet();
}

我们可以将 crate 根文件(这里是 src/main.rs)的一部分拆分到另一个文件,即将 sound 模块移动到其自己的文件 src/sound.rs 中

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
mod sound;

fn main() {
    // 绝对路径
    crate::sound::instrument::clarinet();

    // 相对路径
    sound::instrument::clarinet();
}

在 mod sound 后使用分号而不是代码块告诉 Rust 在另一个与模块同名的文件中加载模块的内容。

文件名: src/sound.rs

1
2
3
4
5
pub mod instrument {
    pub fn clarinet() {
        // 函数体
    }
}

继续重构我们例子,将 instrument 模块也提取到其自己的文件中,修改 src/sound.rs 只包含 instrument 模块的声明:

文件名: src/sound.rs

1
pub mod instrument;

接着创建 src/sound 目录和 src/sound/instrument.rs 文件来包含 instrument 模块的定义:

文件名: src/sound/instrument.rs

1
2
3
pub fn clarinet() {
    // 函数体
}

模块树依然保持相同,main 中的函数调用也无需修改继续保持有效,即使其定义存在于不同的文件中。
这样随着代码增长可以将模块移动到新文件中。

总结

Rust 提供了将包组织进 crate、将 crate 组织进模块和通过指定绝对或相对路径从一个模块引用另一个模块中定义的项的方式。
可以通过 use 语句将路径引入作用域,这样在多次使用时可以使用更短的路径。
模块定义的代码默认是私有的,不过可以选择增加 pub 关键字使其定义变为公有。