You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trpl-zh-cn/src/ch07-02-modules-and-use-to-...

30 KiB

模块系统用来控制作用域和私有性

ch07-01-mod-and-the-filesystem.md
commit 9d431b25ca6c7200eafe4107eb0765c088923027

Rust 的此部分功能通常被引用为 “模块系统”“the module system”不过其包括了一些除模块之外的功能。在本部分我们会讨论

  • 模块,一个组织代码和控制路径私有性的方式
  • 路径一个命名项item的方式
  • use 关键字用来将路径引入作用域
  • pub 关键字使项变为公有
  • as 关键字用于将项引入作用域时进行重命名
  • 使用外部包
  • 嵌套路径用来消除大量的 use 语句
  • 使用 glob 运算符将模块的所有内容引入作用域
  • 如何将不同模块分割到单独的文件中

首先讲讲模块。模块允许我们将代码组织起来。示例 7-1 是一个例子,这些代码定义了名为 sound 的模块,其包含名为 guitar 的函数。

文件名: src/main.rs

mod sound {
    fn guitar() {
        // 函数体
    }
}

fn main() {

}

示例 7-1: 包含 guitar 函数和 main 函数的 sound 模块

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

为了将代码组织到模块层次体系中,可以将模块嵌套进其他模块,如示例 7-2 所示:

文件名: src/main.rs

mod sound {
    mod instrument {
        mod woodwind {
            fn clarinet() {
                // 函数体
            }
        }
    }

    mod voice {

    }
}

fn main() {

}

示例 7-2: 模块中的模块

在这个例子中,我们像示例 7-1 一样定义了 sound 模块。接着在 sound 模块中定义了 instrumentvoice 模块。instrument 模块中定义了另一个模块 woodwind,这个模块包含一个函数 clarinet

在 “包和 crate 用来创建库和二进制项目” 部分提到 src/main.rssrc/lib.rs 被称为 crate 根。他们被称为 crate 根是因为这两个文件在 crate 模块树的根组成了名为 crate 模块。所以示例 7-2 中,有如示例 7-3 所示的模块树:

crate
└── sound
    ├── instrument
    │   └── woodwind
    └── voice

示例 7-3: 示例 7-2 中代码的模块树

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

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

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

如果想要调用函数,需要知道其 路径。“路径” 是 “名称”“name” 的同义词,不过它用于文件系统语境。另外,函数、结构体和其他项可能会有多个指向相同项的路径,所以 “名称” 这个概念不太准确。

路径 可以有两种形式:

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

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

如何在示例 7-2 的 main 函数中调用 clarinet 函数呢?也就是说,clarinet 函数的路径是什么呢?在示例 7-4 中稍微简化了代码,移除了一些模块,并展示了两种在 main 中调用 clarinet 函数的方式。这个例子还不能编译,我们会解释为什么。

文件名: src/main.rs

mod sound {
    mod instrument {
        fn clarinet() {
            // 函数体
        }
    }
}

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

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

示例 7-4: 在简化的模块树中,分别使用绝对路径和相对路径在 main 中调用 clarinet 函数

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

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

示例 7-4 提到了它并不能编译,让我们尝试编译并看看为什么不行!示例 7-5 展示了错误。

$ 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();
   |            ^^^^^^^^^^

示例 7-5: 构建示例 7-4 出现的编译器错误

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

模块作为私有性的边界

之前我们讨论到模块的语法和组织代码的用途。Rust 采用模块还有另一个原因:模块是 Rust 中的 私有性边界privacy boundary)。如果你希望函数或结构体是私有的,将其放入模块。私有性规则有如下:

  • 所有项(函数、方法、结构体、枚举、模块和常量)默认是私有的。
  • 可以使用 pub 关键字使项变为公有。
  • 不允许使用定义于当前模块的子模块中的私有代码。
  • 允许使用任何定义于父模块或当前模块中的代码。

换句话说,对于没有 pub 关键字的项,当你从当前模块向 “下” 看时是私有的,不过当你向 “上” 看时是公有的。再一次想象一下文件系统:如果你没有某个目录的权限,则无法从父目录中查看其内容。如果有该目录的权限,则可以查看其中的目录和任何父目录。

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

示例 7-5 中的错误说 instrument 模块是私有的。让我们使用 pub 关键字标记 instrument 模块使其可以在 main 函数中使用。这些改变如示例 7-6 所示,它仍然不能编译,不过会产生一个不同的错误:

文件名: src/main.rs

mod sound {
    pub mod instrument {
        fn clarinet() {
            // 函数体
        }
    }
}

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

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

示例 7-6: 将 instrument 模块声明为 pub 以便可以在 main 中使用

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

不过示例 7-6 中的代码仍然产生错误,如示例 7-7 所示:

$ 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();
   |                        ^^^^^^^^

示例 7-7: 构建示例 7-6 时产生的编译器错误

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

clarinet 函数前增加 pub 关键字使其变为公有,如示例 7-8 所示:

文件名: src/main.rs

mod sound {
    pub mod instrument {
        pub fn clarinet() {
            // 函数体
        }
    }
}

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

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

示例 7-8: 在 mod instrumentfn clarinet 之前都增加 pub 关键字使我们可以在 main 中调用此函数

现在可以编译了!让我们看看绝对路径和相对路径再次检查为什么增加 pub 关键字使得我们可以在 main 中调用这些路径。

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

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

使用 super 开始相对路径

也可以使用 super 开头来构建相对路径。这么做类似于文件系统中以 .. 开头:该路径从 模块开始而不是当前模块。这在例如示例 7-9 这样的情况下有用处,在这里 clarinet 函数通过指定以 super 开头的路径调用 breathe_in 函数:

文件名: src/lib.rs

# fn main() {}
#
mod instrument {
    fn clarinet() {
        super::breathe_in();
    }
}

fn breathe_in() {
    // 函数体
}

示例 7-9: 使用以 super 开头的路径从父目录开始调用函数

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

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

文件名: src/lib.rs

mod sound {
    mod instrument {
        fn clarinet() {
            super::breathe_in();
        }
    }

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

示例 7-10: 增加一个名为 sound 的父模块并不影响相对路径 super::breathe_in

示例 7-10 在 clarinet 函数中调用 super::breathe_in 将如示例 7-9 一样继续有效,无需更新路径。如果在 clarinet 函数不使用 super::breathe_in 而是使用 crate::breathe_in 的话,当增加父模块 sound 后,则需要更新 clarinet 函数使用 crate::sound::breathe_in 路径。使用相对路径可能意味着重新布局模块时需要更少的必要修改。

对结构体和枚举使用 pub

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

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

文件名: src/main.rs

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

示例 7-11: 结构体带有公有和私有的字段

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

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

文件名: src/main.rs

mod menu {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

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

示例 7-12: 将枚举设计为公有会使其所有成员公有

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

还有一种使用 pub 的场景我们还没有涉及到,而这是我们最后要讲的模块功能:use 关键字。我们先单独介绍 use,然后展示如何结合使用 pubuse

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

你可能考虑过本章很多的函数调用的路径是冗长和重复的。例如示例 7-8 中,当我们选择 clarinet 函数的绝对或相对路径时,每次想要调用 clarinet 时都不得不也指定 soundinstrument。幸运的是,有一次性将路径引入作用域然后就像调用本地项那样的方法:使用 use 关键字。在示例 7-13 中将 crate::sound::instrument 模块引入了 main 函数的作用域,以便只需指定 instrument::clarinet 来调用 clarinet 函数。

文件名: src/main.rs

mod sound {
    pub mod instrument {
        pub fn clarinet() {
            // 函数体
        }
    }
}

use crate::sound::instrument;

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

示例 7-13: 使用 use 将模块引入作用域并使用绝对路径来缩短在模块中调用项所必须的路径

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

如果希望通过 use 和相对路径来将项引入作用域,则与直接通过相对路径调用项有些小的区别:不同于从当前作用域的名称开始,use 中的路径必须以 self 示例 7-14 展示了如何指定相对路径来取得与示例 7-13 中使用绝对路径一样的行为。

文件名: src/main.rs

mod sound {
    pub mod instrument {
        pub fn clarinet() {
            // 函数体
        }
    }
}

use self::sound::instrument;

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

示例 7-14: 通过 use 和相对路径将模块引入作用域

当指定 use 后以 self 开头的相对路径在未来可能不是必须的;这是一个开发者正在尽力消除的语言中的不一致。

如果调用项目的代码移动到模块树的不同位置但是定义项目的代码却没有,那么选择使用 use 指定绝对路径可以使更新更轻松,这与示例 7-10 中同时移动的情况相对。例如,如果我们决定采用示例 7-13 的代码,将 main 函数的行为提取到函数 clarinet_trio 中,并将该函数移动到模块 performance_group 中,这时 use 所指定的路径无需变化,如示例 7-15 所示。

文件名: src/main.rs

mod sound {
    pub mod instrument {
        pub fn clarinet() {
            // 函数体
        }
    }
}

mod performance_group {
    use crate::sound::instrument;

    pub fn clarinet_trio() {
        instrument::clarinet();
        instrument::clarinet();
        instrument::clarinet();
    }
}

fn main() {
    performance_group::clarinet_trio();
}

示例 7-15: 移动调用项的代码时绝对路径无需移动

相反,如果对示例 7-14 中指定了相对路径的代码做同样的修改,则需要将 use self::sound::instrument 变为 use super::sound::instrument。如果你不确定将来模块树会如何变化,那么选择采用相对或绝对路径是否会减少修改可能全靠猜测,不过本书的作者倾向于通过 crate 指定绝对路径,因为定义和调用项的代码更有可能相互独立的在模块树中移动,而不是像示例 7-10 那样一同移动。

use 函数路径使用习惯 VS 其他项

示例 7-13 中,你可能会好奇为什么指定 use crate::sound::instrument 接着在 main 中调用 instrument::clarinet,而不是如示例 7-16 所示的有相同行为的代码:

文件名: src/main.rs

mod sound {
    pub mod instrument {
        pub fn clarinet() {
            // 函数体
        }
    }
}

use crate::sound::instrument::clarinet;

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

示例 7-16: 通过 useclarinet 函数引入作用域,这是不推荐的

对于函数来说,通过 use 指定函数的父模块接着指定父模块来调用方法被认为是习惯用法。这么做而不是像示例 7-16 那样通过 use 指定函数的路径,清楚的表明了函数不是本地定义的,同时仍最小化了指定全路径时的重复。

对于结构体、枚举和其它项,通过 use 指定项的全路径是习惯用法。例如,示例 7-17 展示了将标准库中 HashMap 结构体引入作用域的习惯用法。

文件名: src/main.rs

use std::collections::HashMap;

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

示例 7-17: 将 HashMap 引入作用域的习惯用法

相反,示例 7-18 中的代码将 HashMap 的父模块引入作用域不被认为是习惯用法。这个习惯并没有很强制的理由;这是慢慢形成的习惯同时人们习惯于这么读写。

文件名: src/main.rs

use std::collections;

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

示例 7-18: 将 HashMap 引入作用域的非习惯方法

这个习惯的一个例外是如果 use 语句会将两个同名的项引入作用域时,这是不允许的。示例 7-19 展示了如何将两个不同父模块的 Result 类型引入作用域并引用它们。

文件名: src/lib.rs

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
#     Ok(())
}
fn function2() -> io::Result<()> {
#     Ok(())
}

示例 7-19: 将两个同名类型引入作用域必须使用父模块

因为如果我们指定 use std::fmt::Resultuse std::io::Result,则作用域中会有两个 Result 类型Rust 无法知道我们想用哪个 Result。尝试这么做并看看编译器错误!

通过 as 关键字重命名引入作用域的类型

将两个同名类型引入同一作用域这个问题还有另一个解决办法:可以通过在 use 后加上 as 和一个新名称来为此类型指定一个新的本地名称。示例 7-20 展示了另一个编写示例 7-19 中代码的方法,通过 as 重命名了其中一个 Result 类型。

文件名: src/lib.rs

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
#     Ok(())
}
fn function2() -> IoResult<()> {
#     Ok(())
}

示例 7-20: 通过 as 关键字重命名引入作用域的类型

在第二个 use 语句中,我们选择 IoResult 作为 std::io::Result 的新名称,它与从 std::fmt 引入作用域的 Result 并不冲突。这样做也被认为是惯用的;示例 7-19 还是示例 7-20 全看你的选择。

通过 pub use 重导出名称

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

例如,示例 7-21 展示了示例 7-15 中的代码将 performance_groupuse 变为 pub use 的版本。

文件名: src/main.rs

mod sound {
    pub mod instrument {
        pub fn clarinet() {
            // 函数体
        }
    }
}

mod performance_group {
    pub use crate::sound::instrument;

    pub fn clarinet_trio() {
        instrument::clarinet();
        instrument::clarinet();
        instrument::clarinet();
    }
}

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

示例 7-21: 通过 pub use 使名称可引入任何代码的作用域中

通过 pub use,现在 main 函数可以通过新路径 performance_group::instrument::clarinet 来调用 clarinet 函数。如果没有指定 pub useclarinet_trio 函数可以在其作用域中调用 instrument::clarinetmain 则不允许使用这个新路径。

使用外部包

在第二章中我们编写了一个猜猜看游戏。那个项目使用了一个外部包,rand,来生成随机数。为了在项目中使用 rand,在 Cargo.toml 中加入了如下行:

文件名: Cargo.toml

[dependencies]
rand = "0.5.5"

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

接着,为了将 rand 定义引入项目包的作用域,加入一行 use,它以 rand 包名开头并列出了需要引入作用域的项。回忆一下第二章的 “生成一个随机数” 部分,我们曾将 Rng trait 引入作用域并调用了 rand::thread_rng 函数:

use rand::Rng;

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

https://crates.io 上有很多社区成员发布的包,将其引入你自己的项目涉及到相同的步骤:在 Cargo.toml 列出它们并通过 use 将其中定义的项引入项目包的作用域中。

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

use std::collections::HashMap;

这是一个以标准库 crate 名 std 开头的绝对路径。

嵌套路径来消除大量的 use

当需要引入很多定义于相同包或相同模块的项时,为每一项单独列出一行会占用源码很大的空间。例如猜猜看章节示例 2-4 中有两行 use 语句都从 std 引入项到作用域:

文件名: src/main.rs

use std::cmp::Ordering;
use std::io;
// ---snip---

可以使用嵌套的路径将同样的项在一行中引入而不是两行,这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分,如示例 7-22 所示。

文件名: src/main.rs

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

示例 7-22: 指定嵌套的路径在一行中将多个带有相同前缀的项引入作用域

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

也可以剔除掉完全包含在另一个路径中的路径。例如,示例 7-23 中展示了两个 use 语句:一个将 std::io 引入作用域,另一个将 std::io::Write 引入作用域:

文件名: src/lib.rs

use std::io;
use std::io::Write;

示例 7-23: 通过两行 use 语句引入两个路径,其中一个是另一个的子路径

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

文件名: src/lib.rs

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

示例 7-24: 将示例 7-23 中部分重复的路径合并为一个 use 语句

这将 std::iostd::io::Write 同时引入作用域。

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

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

use std::collections::*;

这个 use 语句将 std::collections 中定义的所有公有项引入当前作用域。

使用 glob 运算符时请多加小心!如此难以推导作用域中有什么名称和它们是在何处定义的。

glob 运算符经常用于测试模块 tests 中,这时会将所有内容引入作用域;我们将在第十一章 “如何编写测试” 部分讲解。glob 运算符有时也用于 prelude 模式;查看 标准库中的文档 了解这个模式的更多细节。

将模块分割进不同文件

目前本章所有的例子都在一个文件中定义多个模块。当模块变得更大时,你可能想要将它们的定义移动到一个单独的文件中使代码更容易阅读。

例如从示例 7-8 的代码开始,我们可以通过修改 crate 根文件(这里是 src/main.rs)将 sound 模块移动到其自己的文件 src/sound.rs 中,如示例 7-25 所示。

文件名: src/main.rs

mod sound;

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

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

示例 7-25: 声明 sound 模块,其内容位于 src/sound.rs 文件

src/sound.rs 中会包含 sound 模块的内容,如示例 7-26 所示。

文件名: src/sound.rs

pub mod instrument {
    pub fn clarinet() {
        // 函数体
    }
}

示例 7-26: sound 模块中的定义位于 src/sound.rs

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

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

文件名: src/sound.rs

pub mod instrument;

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

文件名: src/sound/instrument.rs

pub fn clarinet() {
    // 函数体
}

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

总结

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

接下来,让我们看看一些标准库提供的集合数据类型,你可以利用它们编写出漂亮整洁的代码。