UP | HOME

模块

Table of Contents

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

  到目前为止,我们编写的程序都在一个文件的一个模块中

  伴随着项目的增长,可以通过将代码分解为多个模块和多个文件来组织代码:
      一个包可以包含多个二进制 crate 项和一个可选的 crate 库
      伴随着包的增长,还可以将包中的部分代码提取出来,做成独立的 crate,这些 crate 则作为外部依赖项

除了对功能进行分组以外, 封装实现细节 可以更高级地 重用代码 :实现了一个操作后,其他的代码可以通过该代码的公共接口来进行调用,而不需要知道它是如何实现的

  在编写代码时可以定义哪些部分是其他代码可以使用的公共部分,以及哪些部分是有权更改实现细节的私有部分

  这是另一种减少脑海中记住项目内容数量的方法

这里有一个需要说明的概念 作用域代码所在的嵌套上下文 有一组定义为 in scope名称 。当阅读、编写和编译代码时,程序员和编译器需要知道 特定位置的特定名称 是否引用了 变量函数结构体枚举模块常量 或者 其他有意义的项

  可以创建作用域,以及改变哪些名称在作用域内还是作用域外

  同一个作用域内不能拥有两个相同名称的项

  我们可以使用一些工具来解决名称冲突

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

  本章将会涵盖所有这些概念,讨论它们如何交互,并说明如何使用它们来管理作用域

  到最后,会对模块系统有深入的了解,并且能够像专业人士一样使用作用域!

  对于一个由一系列相互关联的包组合而成的超大型项目,Cargo 还提供了 “工作空间” 这一功能,本章暂时不做讲解

包和 crate

crate 是一个 二进制项 或者

  • crate root 是一个 源文件 :Rust 编译器以它为起始点,并构成 crate 的 根模块
  • :提供一系列功能的 一个或者多个 crate
    • 一个包会包含有一个 Cargo.toml 文件,阐述如何去 构建这些 crate

包中所包含的内容由几条规则来确立:

  1. 一个包中 至多只能 包含 一个库 crate
  2. 包中可以包含 任意 多个 二进制 crate
  3. 包中 至少 包含 一个 crate ,无论是库的还是二进制的

当输入命令 cargo new:

$ 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 文件

root

     查看 Cargo.toml 的内容,却会发现并没有提到 src/main.rs

因为 Cargo 遵循的一个约定:

  • src/main.rs : 就是一个与 包同名二进制 cratecrate 根
  • 如果包目录中包含 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 会将一个作用域内的相关功能分组到一起,使得该功能可以很方便地在 多个项目之间共享

举一个例子,曾经使用的 rand crate 提供了生成随机数的功能

通过将 rand crate 加入到我们项目的作用域中,就可以在自己的项目中使用该功能

rand crate 提供的所有功能都可以通过该 crate 的名字:rand 进行访问

将一个 crate 的功能保持在其自身的作用域中,可以知晓一些特定的功能是在自己的 crate 中定义的还是在 rand crate 中定义的,这可以 防止潜在的冲突

     例如,rand crate 提供了一个名为 Rng 的特性(trait),还可以在自己的 crate 中定义一个名为 Rng 的 struct

     因为一个 crate 的功能是在自身的作用域进行命名的,所以编译器会知道:
	 Rng 这个名字指向的是我们自己定义的 struct Rng
	 rand::Rng 这一方式指向的才是 rand crate 中的 Rng 特性

模块

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

可以将 函数 放置嵌套的模块 中,来使 crate 结构与实际的餐厅结构相同。通过执行 cargo new –lib restaurant ,来创建一个新的名为 restaurant 的库

cargo new --lib restaurant 

然后将下面所罗列出来的代码放入 src/lib.rs 中,来定义一些模块和函数:

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 模块
  • 模块还可以 保存 一些定义的其他项,比如 结构体枚举常量特性 、或者 函数
    通过使用模块,可以将相关的定义分组到一起,并指出他们为什么相关

    程序员可以通过使用这段代码,更加容易地找到他们想要的定义,因为他们可以基于分组来对代码进行导航,而不需要阅读所有的定义

    程序员向这段代码中添加一个新的功能时,他们也会知道代码应该放置在何处,可以保持程序的组织性
  • 模块可以将一个 crate 中的 代码分组 ,以提高 可读性重用性
  • 模块还可以控制项的 私有性
    • public: 项是可以被 外部代码使用的
    • private: 作为一个 内部实现 的内容,不能被外部代码使用

模块树

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

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
  • 这个树展示了一些模块是如何被嵌入到另一个模块的:例如,hosting 嵌套在 front_of_house 中
  • 这个树还展示了一些模块是互为 兄弟 的,这意味着它们定义在同一模块中:hosting 和 serving 被一起定义在 front_of_house 中
  • 如果一个模块 A 被包含在模块 B 中,将模块 A 称为模块 B 的 ,模块 B 则是模块 A 的
    • 注意:整个模块树都植根于名为 crate隐式模块
     这个模块树可能会令你想起电脑上文件系统的目录树:这是一个非常恰当的比喻!

     就像文件系统的目录,可以使用模块来组织代码,并且,就像目录中的文件,需要一种方法来找到模块

路径

    来看一下 Rust 如何在模块树中找到一个项的位置:Rust 会使用路径的方式,就像在文件系统使用路径一样

    如果想要调用一个函数,就需要知道它的路径

路径 有两种形式:

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

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

    回到开始的示例:如何调用 add_to_waitlist 函数?

    还是同样的问题,add_to_waitlist 函数的路径是什么?

下面例子里通过删除一些模块和函数,稍微简化了一下代码。在 crate 根定义了一个新函数 eat_at_restaurant,并在其中展示调用 add_to_waitlist 函数的两种方法:

  • eat_at_restaurant 函数是我们 crate 库的一个 公共 API,所以使用 pub 关键字来标记它
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

第一种方式:在 eat_at_restaurant 中调用 add_to_waitlist 函数,使用的是 绝对 路径

  • add_to_waitlist 函数与 eat_at_restaurant 被定义在同一 crate 中,这意味着可以使用 crate 关键字为起始的 绝对 路径
    在 crate 后面,持续地嵌入模块,直到找到 add_to_waitlist

    可以想象出一个相同结构的文件系统,通过指定路径 /front_of_house/hosting/add_to_waitlist 来执行 add_to_waitlist 程序

    我们使用 crate 从 crate 根开始就类似于在 shell 中使用 / 从文件系统根开始

第二种方式:在 eat_at_restaurant 中调用 add_to_waitlist,使用的是相对路径

  • 路径以 front_of_house 为起始,这个模块在模块树中,与 eat_at_restaurant 定义在同一层级
    与之等价的文件系统路径就是 front_of_house/hosting/add_to_waitlist

    以名称为起始,意味着该路径是相对路径

选择使用相对路径还是绝对路径,还是要取决于是更倾向于将 项的定义代码使用该项的代码 分开 移动,还是 一起 移动

    举一个例子,如果我们要将 front_of_house 模块和 eat_at_restaurant 函数一起移动到一个名为 customer_experience 的模块中,我们需要更新 add_to_waitlist 的绝对路径,但是相对路径还是可用的

    然而,如果我们要将 eat_at_restaurant 函数单独移到一个名为 dining 的模块中,还是可以使用原本的绝对路径来调用 add_to_waitlist,但是相对路径必须要更新

    一般更倾向于使用绝对路径,因为它更适合移动代码定义和项调用的相互独立

试着编译一下,却发现无法编译通过:

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^

错误信息说 hosting 模块是 私有

    换句话说,虽然拥有 hosting 模块和 add_to_waitlist 函数的的正确路径,但是 Rust 不让使用,因为它不能访问私有片段

模块不仅对于 组织代码 很有用。他们还定义了 Rust 的 私有性边界 :这条界线不允许外部代码了解、调用和依赖被封装的实现细节

    所以,如果希望创建一个私有函数或结构体,可以将其放入模块
  • Rust 中默认所有项( 函数方法结构体枚举模块常量 )都是 私有
  • 父模块 中的项 不能 使用 子模块 中的 私有项
  • 但是 子模块 中的项 可以 使用他们 父模块 中的项
    这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文

    继续拿餐馆作比喻,把私有性规则想象成餐馆的后台办公室:餐馆内的事务对餐厅顾客来说是不可知的,但办公室经理可以洞悉其经营的餐厅并在其中做任何事情

    因此Rust默认会隐藏内部实现细节:这样一来,就知道可以更改内部代码的哪些部分而不会破坏外部代码

    当然还可以通过使用 pub 关键字来创建公共项,使子模块的内部部分暴露给上级模块

使用 pub 关键字暴露路径

回头看一下前面示例的错误,这次使用 pub 关键字来标记 hosting 模块,如下面所示:

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

不幸的是,上面的代码编译仍然有错误:

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^
     在 mod hosting 前添加了 pub 关键字,使其变成公有的

     伴随着这种变化,如果可以访问 front_of_house,那也可以访问 hosting

     但是 hosting 的内容仍然是私有的;这表明使模块公有并不使其内容也是公有的

     模块上的 pub 关键字只允许其父模块引用它,因此私有性规则不但应用于模块,还应用于结构体、枚举、函数和方法

继续将 pub 关键字放置在 add_to_waitlist 函数的定义之前,使其变成公有:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

现在代码可以编译通过了!

回头看下绝对路径和相对路径,并根据私有性规则,再检查一下为什么增加 pub 关键字使得可以在 add_to_waitlist 中调用这些路径

在绝对路径,从 crate,也就是 crate 根开始:
然后 crate 根中定义了 front_of_house 模块:front_of_house 模块不是公有的,不过因为 eat_at_restaurant 函数与 front_of_house 定义于同一模块中,使得可以从 eat_at_restaurant 中引用 front_of_house
接下来是使用 pub 标记的 hosting 模块。可以访问 hosting 的父模块,所以可以访问 hosting
最后,add_to_waitlist 函数被标记为 pub ,可以访问其父模块,所以这个函数调用是有效的!


在相对路径,其逻辑与绝对路径相同,除了第一步:不同于从 crate 根开始,路径从 front_of_house 开始
front_of_house 模块与 eat_at_restaurant 定义于同一模块,所以从 eat_at_restaurant 中开始定义的该模块相对路径是有效的
接下来因为 hosting 和 add_to_waitlist 被标记为 pub,路径其余的部分也是有效的,因此函数调用也是有效的!

使用 super 起始的相对路径

还可以使用 super 开头来构建从父模块开始的相对路径。这么做类似于文件系统中以 .. 开头的语法

考虑一下下面示例的代码,它模拟了厨师更正了一个错误订单,并亲自将其提供给客户的情况。fix_incorrect_order 函数通过指定的 super 起始的 server_order 路径,来调用 server_order 函数:

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }

    fn cook_order() {}
}
fix_incorrect_order 函数在 back_of_house 模块中,所以可以使用 super 进入 back_of_house 父模块,也就是本例中的 crate 根,在这里可以找到 serve_order

这里我们认为 back_of_house 模块和 server_order 函数之间可能具有某种关联关系,并且,如果要重新组织这个 crate 的模块树,需要一起移动它们

因此,使用 super,这样一来,如果这些代码被移动到了其他模块,只需要更新很少的代码

创建公有的结构体和枚举

以使用 pub 来设计 公有结构体枚举 ,需要注意:

  • 如果在一个结构体定义的前面使用了 pub ,这个结构体会变成公有的,但是这个 结构体的字段仍然是私有的 :可以根据情况决定每个字段是否公有
     在下面模拟的情况是:在一家餐馆中,顾客可以选择随餐附赠的面包类型,但是厨师会根据季节和库存情况来决定随餐搭配的水果

     因为餐馆可用的水果变化是很快的,所以顾客不能选择水果,甚至无法看到他们将会得到什么水果

这里定义了一个 公有结构体 back_of_house:Breakfast,其中有一个 公有字段 toast 和 私有字段 seasonal_fruit :

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // meal.seasonal_fruit = String::from("blueberries");
}
  • 因为 back_of_house::Breakfast 结构体的 toast 字段是公有的,所以可以在 eat_at_restaurant 中使用点号来随意的读写 toast 字段
  • 不能在 eat_at_restaurant 中使用 seasonal_fruit 字段,因为 seasonal_fruit 是私有的

尝试去除那一行修改 seasonal_fruit 字段值的代码的注释,就会得到下面的报错:

error[E0616]: field `seasonal_fruit` of struct `back_of_house::Breakfast` is private
  --> src/lib.rs:27:5
   |
27 |     meal.seasonal_fruit = String::from("blueberries");
   |     ^^^^^^^^^^^^^^^^^^^

还请注意一点:因为 back_of_house::Breakfast 具有私有字段,所以这个结构体需要提供一个 公共的 关联函数 来 构造 示例 Breakfast :这里命名为 summer

     如果 Breakfast 没有这样的函数,将无法在 eat_at_restaurant 中创建 Breakfast 实例,因为不能在 eat_at_restaurant 中设置私有字段 seasonal_fruit 的值

与之相反:如果将 枚举 设为 公有 ,则它的 所有成员都将变为公有 ,只需要在 enum 关键字前面加上 pub

     结构体通常使用时,不必将它们的字段公有化,因此结构体遵循常规,内容全部是私有的,除非使用 pub 关键字

     如果枚举成员不是公有的,那么枚举会显得用处不大;给枚举的所有成员挨个添加 pub 是很令人恼火的,因此枚举成员默认就是公有的

下面创建了名为 Appetizer公有 枚举,就可以在 eat_at_restaurant 中使用 SoupSalad 成员:

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

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}
     还有一种使用 pub 的场景还没有涉及到,那就是最后要讲的模块功能:use 关键字

     将先单独介绍 use,然后展示如何结合使用 pub 和 use

use 导入模块

    到目前为止,似乎编写的用于调用函数的路径都很冗长且重复,并不方便

    例如,无论选择 add_to_waitlist 函数的绝对路径还是相对路径,每次想要调用 add_to_waitlist 时,都必须指定front_of_house 和 hosting

    幸运的是,有一种方法可以简化这个过程。可以一次性将路径引入作用域,然后使用 use 关键字调用该路径中的项,就如同它们是本地项一样

使用 usecrate::front_of_house::hosting 模块引入了 eat_at_restaurant 函数的作用域,这样只需要指定 hosting::add_to_waitlist 即可在 eat_at_restaurant 中调用 add_to_waitlist 函数

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();
}
    在作用域中增加 use 和路径类似于在文件系统中创建符号连接

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

    通过 use 引入作用域的路径也会检查私有性,同其它路径一样

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

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 crate::front_of_house::hosting ,然后在 eat_at_restaurant 中调用 hosting::add_to_waitlist ,而不是通过指定一直到 add_to_waitlist 函数的 use 路径来得到相同的结果?
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();
}
     虽然上面示例也完成了相同的任务,但这样的代码不清楚 add_to_waitlist 究竟是在哪里被定义的

     相比较而言,而在前面实例中,如果想使用 use 将函数的父模块引入作用域,必须在调用函数时指定父模块,这样可以清晰地表明函数不是在本地定义的,同时使完整路径的重复度最小化

另一方面,使用 use 引入 结构体枚举其他项 时,习惯是指定它们的 完整路径 。下面展示了将 HashMap 结构体引入二进制 crate 作用域的习惯用法:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}
     这种习惯用法背后没有什么硬性要求:它只是一种惯例,人们已经习惯了以这种方式阅读和编写 Rust 代码

这个习惯用法有一个例外,那就是想使用 use 语句将 两个具有相同名称的项 带入作用域,因为 Rust 不允许这样做。下面展示了如何将两个具有相同名称但不同父模块的 Result 类型引入作用域,以及如何引用它们:

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

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

fn function2() -> io::Result<()> {
    // --snip--
}
     如你所见,使用父模块可以区分这两个 Result 类型

     如果指定 use std::fmt::Result 和 use std::io::Result,将在同一作用域拥有了两个 Result 类型,当使用 Result 时,Rust 则不知道我们要用的是哪个

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

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

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 关键字将名称导入作用域时,在 新作用域这个名称私有的 。可以结合 pubuse ,这样做将项引入作用域并同时使其可 供其他代码引入自己的作用域 ,这被称为 重导出

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,可以使用一种结构编写代码,却将不同的结构形式暴露出来。这样做使我们的库井井有条,方便开发这个库的程序员和调用这个库的程序员之间组织起来

使用外部包

前面的猜猜看游戏。那个项目使用了一个外部包, rand 来生成随机数。为了在项目中使用 rand,在 Cargo.toml 中加入了如下行:

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

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

use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 101);
}
crates.io 上有很多 Rust 社区成员发布的包,将其引入你自己的项目都需要一道相同的步骤:在 Cargo.toml 列出它们并通过 use 将其中定义的项引入项目包的作用域中

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

use std::collections::HashMap;

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

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

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

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

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

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

可以在路径的任何层级使用嵌套路径,这在 组合 两个 共享子路径 的 use 语句时非常有用。例如,下面展示了两个 use 语句:一个将 std::io 引入作用域,另一个将 std::io::Write 引入作用域:

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

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

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

这便将 std::io 和 std::io::Write 同时引入作用域

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

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

use std::collections::*;

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

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

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

将模块分割进不同文件

    到目前为止,所有的例子都在一个文件中定义多个模块

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

例如:将 front_of_house 模块移动到属于它自己的文件 src/front_of_house.rs 中:

pub mod hosting {
    pub fn add_to_waitlist() {}
}

接下来就可以简化 crate根文件 src/lib.rs

mod front_of_house;

pub use crate::front_of_house::hosting;

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

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

    在这个例子中,crate 根文件是 src/lib.rs,这也同样适用于以 src/main.rs 为 crate 根文件的二进制 crate 项

继续重构,将 hosting 模块也提取到其自己的文件中,仅对 src/front_of_house.rs 包含 hosting 模块的声明进行修改:

pub mod hosting; 

接着创建一个 src/front_of_house 目录和一个包含 hosting 模块定义的 src/front_of_house/hosting.rs 文件:

pub fn add_to_waitlist() {}
    模块树依然保持相同,eat_at_restaurant 中的函数调用也无需修改继续保持有效,即便其定义存在于不同的文件中

    这个技巧可以在模块代码增长时,将它们移动到新文件中

注意,src/lib.rs 中的 pub use crate::front_of_house::hosting 语句是没有改变的:

  • 在文件作为 crate 的一部分而编译时,use 不会有任何影响
  • mod 关键字 声明模块 ,Rust 会在 与模块同名的文件查找 模块的代码

总结

  • Rust 提供了:
    • 将包组织进 crate
    • 将 crate 组织进模块
    • 指定路径从一个模块引用另一个模块中定义的项
  • 通过使用 use 语句将路径引入作用域,在多次使用时可以使用更短的路径
  • 模块定义的代码默认是私有的,不过可以选择增加 pub 关键字使其定义变为公有
    接下来,来看看一些标准库提供的集合数据类型,可以利用它们编写出漂亮整洁的代码

Next: 集合

Previous: 枚举和模式

Home: 目录