017、使用包、单元包及模块来管理日渐复杂的项目

        在编写较为复杂的项目时,合理地对代码进行组织与管理很重要,因为我们不太可能记住代码中所有的细枝末节。只有按照不同的特性来组织或分割相关功能的代码,我们才能够清晰地找到实现指定功能的代码片段,或确定哪些地方需要修改。

        到目前为止,我们编写的程序都被放置在了同一个文件下的一个模块中。但随着项目的成熟,你可以将代码拆分为不同的模块并使用不同的文件来管理它们。一个包(package)可以拥有多个二进制单元包及一个可选的库单元包。

        而随着包内代码规模的增长,你还可以将部分代码拆分到独立的单元包(crate)中,并将它作为外部依赖进行引用。本篇文章便会讲解这些技术。对于那些特别巨大的、拥有多个相互关联的包的项目,Cargo 提供了另外一种解决方案:工作空间(workspace),我们会在后面文章中学习到。

        除了对功能进行分组,对实现的细节进行封装可以使你在更高的层次上复用代码:一旦你实现了某个操作,其他代码就可以通过公共接口来调用这个操作,而无须了解具体的实现过程。

        我们编写代码的方式决定了哪些部分会作为公共接口供他人使用,而哪些部分又会作为私有的细节实现,使你可以保留进一步修改的权利。这一过程同样使你可以减轻需要记忆在脑海中的心智负担。

        另外一个与组织和封装密切相关的概念被称为作用域(scope):在编写代码的嵌套上下文中有一系列被定义在“作用域内”的名字。当程序员阅读、撰写或编译器编译代码时,都需要借用作用域来确定某个特定区域中的特定名字是否指向了某个变量、函数、结构体、枚举、模块、常量或其他条目,以及这些条目的具体含义。你可以创建作用域并决定某个名字是否处于该作用域中,但是不能在同一作用域中使用相同的名字指向两个不同的条目;有一些工具可以被用来解决命名冲突。

        Rust提供了一系列的功能来帮助我们管理代码,包括决定哪些细节是暴露的、哪些细节是私有的,以及不同的作用域内存在哪些名称。这些功能有时被统称为模块系统(module system),它们包括:

        ⭐ package):一个用于构建、测试并分享单元包的 Cargo 功能。

        ⭐ 单元包crate):一个用于生成库或可执行文件的树形模块结构。

        ⭐ 模块module)及 use 关键字:它们被用于控制文件结构、作用域及路径的私有性。

        ⭐ 路径path):一种用于命名条目的方法,这些条目包括结构体、函数和模块等。

        我们会在本篇介绍上述所有功能,讨论它们之间进行交互的方式,并演示如何使用它们来管理作用域。通过阅读本章,你应该会对模块系统有一个深入的理解,并能够像专家一样熟练地使用作用域!

1. 包与单元包

        让我们先来看一看模块系统中有关包与单元包的部分。单元包可以被用于生成二进制程序或库。我们将Rust编译时所使用的入口文件称作这个单元包的根节点,它同时也是单元包的根模块(我们会随后详细讨论模块)。

        而包则由一个或多个提供相关功能的单元包集合而成,它所附带的配置文件 Cargo.toml 描述了如何构建这些单元包的信息。有几条规则决定了包可以包含哪些东西。

        首先,一个包中只能拥有最多一个库单元包。其次,包可以拥有任意多个二进制单元包。最后,包内必须存在至少一个单元包(库单元包或二进制单元包)。

        现在,让我们输入命令 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 文件。观察 Cargo.toml 中的内容,你也许会奇怪它居然没有提到 src/main.rs,这是因为 Cargo 会默认将 src/main.rs 视作一个二进制单元包的根节点而无须指定,这个二进制单元包与包拥有相同的名称。

        同样地,假设包的目录中包含文件 src/lib.rsCargo 也会自动将其视作与包同名的库单元包的根节点。Cargo 会在构建库和二进制程序时将这些单元包的根节点文件作为参数传递给 rustc。最初生成的包只包含源文件 src/main.rs,这也意味着它只包含一个名为 my-project 的二进制单元包。

        而假设包中同时存在 src/main.rs src/lib.rs,那么其中就会分别存在一个二进制单元包与一个库单元包,它们拥有与包相同的名称。我们可以在路径 src/bin 下添加源文件来创建出更多的二进制单元包,这个路径下的每个源文件都会被视作单独的二进制单元包。 

        单元包可以将相关的功能分组,并放到同一作用域下,这样便可以使这些功能轻松地在多个项目中共享。例如,我们之前使用过的 rand 包(rand crate)提供了生成随机数的功能。

        而为了使用这些功能,我们只需要将 rand 包引入当前项目的作用域中即可。所有由rand包提供的功能都可以通过单元包的名称 rand 来访问。

        将单元包的功能保留在它们自己的作用域中有助于指明某个特定功能来源于哪个单元包,并避免可能的命名冲突。例如,rand 包提供了一个名为 Rng trait,我们同样也可以在自己的单元包中定义一个名为 Rng struct

        正是因为这些功能被放置在了各自的作用域中,当我们将rand添加为依赖时,编译器才不会为某个 Rng 的具体含义是什么而困惑。在我们的单元包中,它指向刚刚定义的 struct Rng。我们可以通过 rand::Rng 来访问 rand 包中的 Rng trait

        接着,让我们来聊一聊模块系统。

2. 通过定义模块来控制作用域及私有性

         接下来,我们将会讨论模块及模块系统中的其他部分,它们包括可以为条目命名的路径,可以将路径引入作用域的 use 关键字,以及能够将条目标记为公开的 pub 关键字。

        另外,我们还会学习如何使用 as 关键字、外部项目及通配符。现在,先让我们把注意力集中到模块上!模块允许我们将单元包内的代码按照可读性与易用性来进行分组。与此同时,它还允许我们控制条目的私有性。

        换句话说,模块决定了一个条目是否可以被外部代码使用(公共),或者仅仅只是一个内部的实现细节而不对外暴露(私有)。

        下面举一个例子,让我们编写一个提供就餐服务的库单元包。为了将注意力集中到代码组织而不是实现细节上,这个示例只会定义函数的签名而省略函数体中的具体内容。

        在餐饮业中,店面往往会被划分为前厅与后厨两个部分。其中,前厅会被用于服务客户、处理订单、结账及调酒,而后厨则主要用于厨师与职工们制作料理,以及进行其他一些管理工作。

        为了按照餐厅的实际工作方式来组织单元包,可以将函数放置到嵌套的模块中。运行命令 cargo new --lib restaurant 来创建一个名为 restaurant 的库,并将 示例7-1 中的代码输入 src/lib.rs 中来定义一些模块与函数签名。

// 示例7-1:一个含有其他功能模块的front_of_house模块

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

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

        我们以 mod 关键字开头来定义一个模块,接着指明这个模块的名字(也就是本例中的front_of_house),并在其后使用一对花括号来包裹模块体。模块内可以继续定义其他模块,如本例中的 hosting serving 模块。

        模块内同样也可以包含其他条目的定义,比如结构体、枚举、常量、trait 或如 示例7-1 中所示的函数。通过使用模块,我们可以将相关的定义分到一组,并根据它们的关系指定有意义的名称。

        开发者可以轻松地在此类代码中找到某个定义,因为他们可以根据分组来进行搜索而无须遍历所有定义。开发者可以把新功能的代码按这些模块进行划分并放入其中,从而保持程序的组织结构不变。 

        我们前面提到过,src/main.rssrc/lib.rs 被称作单元包的根节点,因为这两个文件的内容各自组成了一个名为 crate 的模块,并位于单元包模块结构的根部。这个模块结构也被称为模块树(module tree)。

// 示例7-2:示例7-1中代码的树状模块结构

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的子节点(child),并将模块B称作模块A的父节点(parent)。注意,整个模块树都被放置在一个名为 crate 的隐式根模块下。

        模块树也许会让你想起文件系统的目录树,实际上这是一个非常恰当的对比!正如文件系统中的目录一样,我们可以使用模块来组织代码;也正如目录中的文件一样,我们也需要对应的方法来定位模块。 

3. 用于在模块树中指明条目的路径

        类似于在文件系统中使用路径进行导航的方式,为了在Rust的模块树中找到某个条目,我们同样需要使用路径。比如,在调用某个函数时,我们必须要知晓它的路径。

        路径有两种形式: 

        ⭐ 使用单元包名或字面量 crate 从根节点开始的绝对路径。

        ⭐ 使用 super 或内部标识符从当前模块开始的相对路径。

        绝对路径与相对路径都由至少一个标识符组成,标识符之间使用双冒号(::)分隔。

        回到 示例7-1 中的例子,我们应该如何调用 add_to_waitlist 函数呢?这个问题实际上等价于:add_to_waitlist 函数的路径是什么呢?示例7-3 中新定义了一个位于根模块的 eat_at_restaurant 函数,并在函数体内展示了两种调用 add_to_waitlist 的方法。

        因为 eat_at_restaurant 函数属于公共接口的一部分,所以我们使用了 pub 关键字来标记它(pub 的细节后面讨论)。注意,这段代码还无法通过编译,稍后可以看到具体的原因。

// 示例7-3:分别使用绝对路径和相对路径来调用add_to_waitlist函数

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

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

        eat_at_restaurant第一次调用add_to_waitlist函数时使用了绝对路径。因为add_to_waitlist函数与eat_at_restaurant被定义在相同的单元包中,所以我们可以使用crate关键字来开始一段绝对路径。

        在crate之后,我们还填写了一系列连续的模块名称,直到最终的add_to_waitlist。你可以想象一个拥有相同结构的文件系统,这个过程类似于指定路径/front_to_house/hosting/add_to_waitlist来运行add_to_waitlist程序。

        使用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的绝对路径会保持不变,但对应的相对路径则需要手动更新。大部分的Rust开发者会更倾向于使用绝对路径,因为我们往往会彼此独立地移动代码的定义与调用代码。

        现在,让我们试着编译示例7-3中的代码并找出它无法编译的原因!此时产生的错误如示例7-4所示。

// 示例7-4:构建示例7-3中的代码后产生的编译错误

$ 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中的私有边界(privacy boundary):外部代码无法知晓、调用或依赖那些由私有边界封装了的实现细节。因此,当你想要将一个条目(比如函数或结构体)声明为私有时,你可以将它放置到某个模块中。

        Rust中的所有条目(函数、方法、结构体、枚举、模块及常量)默认都是私有的。处于父级模块中的条目无法使用子模块中的私有条目,但子模块中的条目可以使用它所有祖先模块中的条目。

        虽然子模块包装并隐藏了自身的实现细节,但它却依然能够感知当前定义环境中的上下文。还是使用餐厅作为比喻,你可以将私有性规则想象为餐厅的后勤办公室:其中的工作细节对于餐厅的客户而言自然是不可见的,但后勤经理却依然能够观察并使用自己餐厅中的任何东西。

        Rust之所以选择让模块系统这样运作,是因为我们希望默认隐藏内部的实现细节。这样,你就能够明确地知道修改哪些内部实现不会破坏外部代码。

        同时,你也可以使用pub关键字来将某些条目标记为公共的,从而使子模块中的这些部分被暴露到祖先模块中。

4. 使用pub关键字来暴露路径

        让我们回到示例7-4中的错误,它指出hosting模块是私有的。为了让父模块中的eat_at_restaurant函数正常访问子模块中的add_to_waitlist函数,我们可以使用pub关键字来标记hosting模块,如示例7-5所示。

src/lib.rs 

// 示例7-5:将hosting模块标记为pub以便在eat_at_restaurant中使用它

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

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

        不幸的是,编译示例7-5中的代码依然会导致错误,如示例7-6所示。 

// 示例7-6:构建示例7-5中的代码后产生的编译错误

$ 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关键字仅仅意味着祖先模块拥有了指向该模块的权限。示例7-6中的错误指出,add_to_waitlist函数是私有的。

        私有性规则不仅作用于模块,也同样作用于结构体、枚举、函数及方法。让我们以同样的方式为add_to_waitlist函数添加pub关键字,如示例7-7所示。 

src/lib.rs

/* 示例7-7:为mod hosting与fn add_to_waitlist
添加的pub关键字使我们可以在eat_at_restaurant中调用这一函数 */

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

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

        现在,代码可以通过编译了!在了解了私有性规则后,让我们再来看一看这里的绝对路径与相对路径,并重新检查一下为什么添加的pub关键字能够使我们使用指向add_to_waitlist的路径。

        在绝对路径中,我们从crate,也就是单元包的模块树的根节点开始。接着,在根节点中定义front_of_house模块。

        虽然front_of_house模块并没有被公开,但是因为eat_at_restaurant函数被定义在与front_of_house相同的模块中(也就是说eat_at_restaurant与front_of_house属于同级节点),所以我们可以直接在eat_at_restaurant中引用front_of_house。

        随后,hosting模块被pub关键字标记。由于我们拥有访问hosting父模块的权利,所以我们也可以访问hosting。最后,add_to_waitlist函数被pub关键字标记,同样因为我们能够访问它的父模块,所以这个函数能够被正常地访问并调用。 

        在相对路径中,除了第一步,大部分逻辑都与绝对路径中的相同:相对路径从front_of_house开始而不是从单元包的根节点开始。

        因为front_of_house模块被定义在与eat_at_restaurant相同的模块下,所以相对路径能够在eat_at_restaurant中从这个模块开始寻址。

        接着,由于hosting和add_to_waitlist都被标记为了pub,所以路径中的其余部分也同样合法,并最终保证函数调用的有效性。

5. 使用super关键字开始构造相对路径

        我们同样也可以从父模块开始构造相对路径,这一方式需要在路径起始处使用super关键字。它有些类似于在文件系统中使用..语法开始一段路径。我们为什么想要这样做呢?

        考虑一下示例7-8中涉及的情形:某个大厨需要修正一份错误的订单,并亲自将它送给外面的客户。其中的函数fix_incorrect_order通过super关键字来指定路径并调用serve_order函数。

src/lib.rs

// 示例7-8:使用super开头构建相对路径来调用函数

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的父模块,也就是根模块处。从它开始,可以成功地找到serve_order。

        考虑到back_of_house模块与serve_order函数联系较为紧密,当我们需要重新组织单元包的模块树时应该会同时移动它们,所以本例使用了super。当未来需要将代码移动至其他模块时,可以避免更新这部分相对路径。 

6. 将结构体或枚举声明为公共的

        结构体与枚举都可以使用pub来声明为公共的,但需要注意其中存在一些细微差别。当我们在结构体定义前使用pub时,结构体本身就成为了公共结构体,但它的字段依旧保持了私有状态。

        我们可以逐一决定是否将某个字段公开。在示例7-9中,我们定义了一个公共的back_of_house::Breakfast结构体,并使它的toast字段公开,而使seasonal_fruit字段保持私有。

        这段代码描述了餐厅中的早餐模型,客户可以自行选择想要的面包,但只有厨师才能根据季节与存货决定配餐水果。这是因为当前可用的水果总是处于变化中,客户无法选择甚至无法知晓他们能够获得的水果种类。 

src/lib.rs

// 示例7-9:一个拥有部分公共字段、部分私有字段的结构体

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() {
    // 选择黑麦面包作为夏季早餐
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // 修改我们想要的面包类型
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // 接下来的这一行无法通过编译,我们不能看到或更换随着食物附带的季节性水果
    // meal.seasonal_fruit = String::from("blueberries");
}

        因为back_of_house::Breakfast结构体中的toast字段是公共的,所以我们才能够在eat_at_restaurant中使用点号读写toast字段。同样由于seasonal_fruit是私有的,所以我们依然不能在eat_at_restaurant中使用它。

        试着取消上面的那段修改seasonal_fruit字段的代码注释,并看一下会得到什么样的编译错误!另外还需要注意的是,因为back_of_house::Breakfast拥有了一个私有字段,所以这个结构体需要提供一个公共的关联函数来构造Breakfast的实例(也就是本例中的summer)。

        如果缺少了这样的函数,我们将无法在eat_at_restaurant中创建任何的Breakfast实例,因为我们不能在eat_at_restaurant中设置私有seasonal_fruit字段的值。

        相对应地,当我们将一个枚举声明为公共的时,它所有的变体都自动变为了公共状态。我们仅需要在enum关键字前放置pub,如示例7-10所示。 

src/lib.rs

// 示例7-10:公开一个枚举会同时将它的所有字段公开

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

        因为Appetizer枚举具有公共属性,所以我们能够在eat_at_restaurant中使用Soup与Salad变体。

        枚举与结构体之所以不同,是由于枚举只有在所有变体都公共可用时才能实现最大的功效,而必须为所有枚举变体添加pub则显得烦琐了一些,因此所有的枚举变体默认都是公共的。

        对于结构体而言,即便部分字段是私有的也不会影响到它自身的使用,所以结构体字段遵循了默认的私有性规则,除非被标记为pub,否则默认是私有的。

        除了上述情形,本节还遗留了一处与pub有关的使用场景没有介绍,它涉及模块系统的最后一个功能:use关键字。我们会首先介绍use本身,然后再演示如何组合使用pub与use。 

7. 用use关键字将路径导入作用域

        基于路径来调用函数的写法看上去会有些重复与冗长。例如在示例7-7中,无论我们使用绝对路径还是相对路径来指定add_to_waitlist函数,都必须在每次调用add_to_waitlist的同时指定路径上的节点front_of_house与hosting。

        幸运的是,有一种方法可以简化该步骤。我们可以借助use关键字来将路径引入作用域,并像使用本地条目一样来调用路径中的条目。

        示例7-11中的代码将crate::front_of_house::hosting模块引入了eat_at_restaurant函数所处的作用域,从而使我们可以在eat_at_restaurant中通过指定hosting::add_to_waitlist来调用add_to_waitlist函数。

src/lib.rs 

// 示例7-11:使用use将模块引入作用域

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();
}
# fn main() {}

        在作用域中使用use引入路径有些类似于在文件系统中创建符号链接。通过在单元包的根节点下添加use crate::front_of_house::hosting,hosting成为了该作用域下的一个有效名称,就如同hosting模块被定义在根节点下一样。

        当然,使用use将路径引入作用域时也需要遵守私有性规则。使用use来指定相对路径稍有一些不同。我们必须在传递给use的路径的开始处使用关键字self,而不是从当前作用域中可用的名称开始。示例7-12中的代码演示了如何使用相对路径来获得与示例7-11中代码相同的行为。

src/lib.rs 

// 示例7-12:使用use与以self开头的相对路径来将模块引入作用域

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

use self::front_of_house::hosting;

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

        需要注意的是,Rust开发者们正在尝试去掉self前缀,也许在不久的将来我们能够避免在代码中使用它。

8. 创建use路径时的惯用模式

        在示例7-11中,你也许会好奇为什么我们使用了use crate::front_ of_house::hosting并接着调用hosting::add_to_waitlist,而没有直接使用use来指向add_to_waitlist函数的完整路径,正如示例7-13所示。

src/lib.rs

// 示例7-13:使用use将add_to_waitlist函数引入作用域的非惯用方式

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

        尽管示例7-11与示例7-13都完成了相同的工作,但相对而言,示例7-11中将函数引入作用域的方式要更加常用一些。

        使用use将函数的父模块引入作用域意味着,我们必须在调用函数时指定这个父模块,从而更清晰地表明当前函数没有被定义在当前作用域中。

        当然,这一方式同样也尽可能地避免了重复完整路径。示例7-13中的代码则无法清晰地传达出add_to_waitlist的定义区域。

        另一方面,当使用use将结构体、枚举和其他条目引入作用域时,我们习惯于通过指定完整路径的方式引入。示例7-14中的二进制单元包展示了将标准库HashMap结构体引入作用域时的惯用方式。

src/main.rs 

// 示例7-14:通过惯用方式将HashMap引入作用域

use std::collections::HashMap;

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

        我们并没有特别强有力的论据来支持这一写法,但它已经作为一种约定俗成的习惯被开发者们接受并应用在阅读和编写Rust代码中了。

        当然,假如我们需要将两个拥有相同名称的条目引入作用域,那么就应该避免使用上述模式,因为Rust并不支持这样的情形。示例7-15展示了如何将来自不同模块却拥有相同名称的两个Result类型引入作用域,并分别指向不同的Result。

src/lib.rs 

// 示例7-15:将两个拥有相同名称的类型引入作用域时需要使用它们的父模块

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

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

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

        正如以上代码所示,我们可以使用父模块来区分两个不同的Result类型。但是,假设我们直接指定了use std::fmt::Result与use std::io::Result,那么同一作用域内就会出现两个Result类型,这时Rust便无法在我们使用Result时确定使用的是哪一个Result。

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

        使用use将同名类型引入作用域时所产生的问题还有另外一种解决办法:我们可以在路径后使用as关键字为类型指定一个新的本地名称,也就是别名。示例7-16使用了这种方法来编写示例7-15中的代码,它使用as将其中一个Result类型进行了重命名。

src/lib.rs 

// 示例7-16:使用as关键字将引入作用域的类型进行重命名

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

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

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

        在第二段use语句中,我们为std::io::Result类型选择了新的名称IoResult,避免了它与同样引入该作用域的std::fmt::Result发生冲突。示例7-15与示例7-16中的写法都是惯用的方法,你可以根据自己的喜好进行选择。

10. 使用pub use重导出名称

        当我们使用use关键字将名称引入作用域时,这个名称会以私有的方式在新的作用域中生效。为了让外部代码能够访问到这些名称,我们可以通过组合使用pub与use实现。

        这项技术也被称作重导出(re-exporting),因为我们不仅将条目引入了作用域,而且使该条目可以被外部代码从新的作用域引入自己的作用域。

示例7-17将示例7-11中根模块下的use修改为了pub use。

src/lib.rs 

// 示例7-17:通过pub use使一个名称可以在新作用域中被其他任意代码使用

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,我们可以在编写代码时使用一种结构,而在对外部暴露时使用另外一种不同的结构。这一方法可以让我们的代码库对编写者与调用者同时保持良好的组织结构。

11. 使用外部包

        我们在第2章编写过一个猜数游戏,并在程序中使用了外部包rand来获得随机数。为了在项目中使用rand,我们需要在Cargo.toml中添加下面的内容:

Cargo.toml 

[dependencies]
rand = "0.5.5"

        在Cargo.toml中添加rand作为依赖会指派Cargo从crates.io上下载rand及相关的依赖包,并使rand对当前的项目可用。

        接着,为了将rand定义引入当前包的作用域,我们以包名rand开始添加了一行use语句,并在包名后列出了我们想要引入作用域的条目。回忆一下之前文章中“生成一个随机数”的内容,我们当时引入了Rng trait,接着又调用了rand::thread_rng函数: 

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

        Rust社区的成员已经在crates.io上上传了许多可用的包,你可以按照类似的步骤将它们引入自己的项目:首先将它们列入Cargo.toml文件,接着使用use来将特定条目引入作用域。

        注意,标准库(std)实际上也同样被视作当前项目的外部包。由于标准库已经被内置到了Rust语言中,所以我们不需要特意修改Cargo.toml来包含std。但是,我们同样需要使用use来将标准库中特定的条目引入当前项目的作用域。例如,我们可以通过如下所示的语句来引入HashMap: 

use std::collections::HashMap;

        这段绝对路径以std开头,std是标准库单元包的名称。

12. 使用嵌套的路径来清理众多use语句

        当我们想要使用同一个包或同一个模块内的多个条目时,将它们逐行列出会占据较多的纵向空间。例如,猜数游戏中的示例2-4使用了两行use语句来将std中的条目引入作用域:

src/main.rs 

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

        然而,我们还可以在同一行内使用嵌套路径来将上述条目引入作用域。这一方法需要我们首先指定路径的相同部分,再在后面跟上两个冒号,接着用一对花括号包裹路径差异部分的列表,如示例7-18所示。

src/main.rs 

// 示例7-18:指定嵌套的路径来将拥有共同路径前缀的条目引入作用域

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

        在一些更复杂的项目里,使用嵌套路径来将众多条目从同一个包或同一个模块引入作用域可以节省大量的独立use语句!

        我们可以在路径的任意层级使用嵌套路径,这一特性对于合并两行共享子路径的use语句十分有用。例如,示例7-19展示了两行use语句:其中一行用于将std::io引入作用域,而另一行则用于将std::io::Write引入作用域。

src/lib.rs 

// 示例7-19:两行使用了use的语句,其中一行是另一行的子路径

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

        这两条路径拥有共同的std::io前缀,该前缀还是第一条路径本身。为了将这两条路径合并至一行use语句中,我们可以在嵌套路径中使用self,如示例7-20所示。

src/lib.rs 

// 示例7-20:将示例7-19中的路径合并至一行use语句中

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

        上述语句会将std::io与std::io::Write引入作用域。 

13. 通配符

        假如你想要将所有定义在某个路径中的公共条目都导入作用域,那么可以在指定路径时在后面使用*通配符:

use std::collections::*;

        上面这行use语句会将定义在std::collections内的所有公共条目都导入当前作用域。请小心谨慎地使用这一特性!通配符会使你难以确定作用域中存在哪些名称,以及某个名称的具体定义位置。

        测试代码常常会使用通配符将所有需要测试的东西引入tests模块,我们会在后文讨论“如何编写测试”这个话题。通配符还经常被用于预导入模块,你可以阅读官方网站的标准库文档中有关预导入模块的内容来获得更多信息。 

你可能感兴趣的:(Rust,编程语言基础,rust,windows,笔记,vscode,后端)