017、使用包、单元包及模块来管理日渐复杂的项目
在编写较为复杂的项目时,合理地对代码进行组织与管理很重要,因为我们不太可能记住代码中所有的细枝末节。只有按照不同的特性来组织或分割相关功能的代码,我们才能够清晰地找到实现指定功能的代码片段,或确定哪些地方需要修改。
到目前为止,我们编写的程序都被放置在了同一个文件下的一个模块中。但随着项目的成熟,你可以将代码拆分为不同的模块并使用不同的文件来管理它们。一个包(package)可以拥有多个二进制单元包及一个可选的库单元包。
而随着包内代码规模的增长,你还可以将部分代码拆分到独立的单元包(crate)中,并将它作为外部依赖进行引用。本篇文章便会讲解这些技术。对于那些特别巨大的、拥有多个相互关联的包的项目,Cargo 提供了另外一种解决方案:工作空间(workspace),我们会在后面文章中学习到。
除了对功能进行分组,对实现的细节进行封装可以使你在更高的层次上复用代码:一旦你实现了某个操作,其他代码就可以通过公共接口来调用这个操作,而无须了解具体的实现过程。
我们编写代码的方式决定了哪些部分会作为公共接口供他人使用,而哪些部分又会作为私有的细节实现,使你可以保留进一步修改的权利。这一过程同样使你可以减轻需要记忆在脑海中的心智负担。
另外一个与组织和封装密切相关的概念被称为作用域(scope):在编写代码的嵌套上下文中有一系列被定义在“作用域内”的名字。当程序员阅读、撰写或编译器编译代码时,都需要借用作用域来确定某个特定区域中的特定名字是否指向了某个变量、函数、结构体、枚举、模块、常量或其他条目,以及这些条目的具体含义。你可以创建作用域并决定某个名字是否处于该作用域中,但是不能在同一作用域中使用相同的名字指向两个不同的条目;有一些工具可以被用来解决命名冲突。
Rust提供了一系列的功能来帮助我们管理代码,包括决定哪些细节是暴露的、哪些细节是私有的,以及不同的作用域内存在哪些名称。这些功能有时被统称为模块系统(module system),它们包括:
⭐ 包(package):一个用于构建、测试并分享单元包的 Cargo 功能。
⭐ 单元包(crate):一个用于生成库或可执行文件的树形模块结构。
⭐ 模块(module)及 use 关键字:它们被用于控制文件结构、作用域及路径的私有性。
⭐ 路径(path):一种用于命名条目的方法,这些条目包括结构体、函数和模块等。
我们会在本篇介绍上述所有功能,讨论它们之间进行交互的方式,并演示如何使用它们来管理作用域。通过阅读本章,你应该会对模块系统有一个深入的理解,并能够像专家一样熟练地使用作用域!
1. 包与单元包
让我们先来看一看模块系统中有关包与单元包的部分。单元包可以被用于生成二进制程序或库。我们将Rust编译时所使用的入口文件称作这个单元包的根节点,它同时也是单元包的根模块(我们会随后详细讨论模块)。
而包则由一个或多个提供相关功能的单元包集合而成,它所附带的配置文件 Cargo.toml 描述了如何构建这些单元包的信息。有几条规则决定了包可以包含哪些东西。
首先,一个包中只能拥有最多一个库单元包。其次,包可以拥有任意多个二进制单元包。最后,包内必须存在至少一个单元包(库单元包或二进制单元包)。
现在,让我们输入命令 cargo new,并观察创建一个包时会发生哪些事情:
$ cargo new my-projectCreated 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.rs,Cargo 也会自动将其视作与包同名的库单元包的根节点。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.rs 与 src/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 buildCompiling 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 buildCompiling 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模块,我们会在后文讨论“如何编写测试”这个话题。通配符还经常被用于预导入模块,你可以阅读官方网站的标准库文档中有关预导入模块的内容来获得更多信息。
相关文章:
017、使用包、单元包及模块来管理日渐复杂的项目
在编写较为复杂的项目时,合理地对代码进行组织与管理很重要,因为我们不太可能记住代码中所有的细枝末节。只有按照不同的特性来组织或分割相关功能的代码,我们才能够清晰地找到实现指定功能的代码片段,或确定哪些地方需要修改。 到…...
Git提交规范详解
在团队协作开发中,Git作为版本控制系统,其提交信息的清晰性和一致性至关重要。通过定义特定的提交类型和格式,我们可以更好地追踪项目历史,提高代码审查效率,并方便生成高质量的变更日志。以下是几种常见的Git提交类型…...
线程与UI操作
子线程中不能执行UI操作。 UI 操作指的是与用户界面(User Interface)相关的操作,包括但不限于以下几种: 更新视图:例如更改 TextView 的文本内容、设置 ImageView 的图片等。处理用户输入:例如响应按钮点…...
ELK企业级日志系统分析系统
目录 一、什么是ELK? 二、ELK三大组件 三、ELK的工作原理 四、完整日志系统基本特征 一、什么是ELK? ELK平台是一套完整的日志集中处理解决方案,将 ElasticSearch、Logstash 和 Kiabana 三个开源工具配合使用, 完成更强大的用…...
11.23 校招 实习 内推 面经
绿*泡*泡: neituijunsir 交流裙 ,内推/实习/校招汇总表格 1、校招&社招&实习丨图森未来传感器标定工程师招聘(内推) 校招&社招&实习丨图森未来传感器标定工程师招聘(内推) 2、校招 | 吉…...
Python战机
基础版 import pygame import random# 设置游戏屏幕大小 screen_width 480 screen_height 600# 定义颜色 WHITE (255, 255, 255) RED (255, 0, 0) GREEN (0, 255, 0) BLUE (0, 0, 255)# 初始化pygame pygame.init()# 创建游戏窗口 screen pygame.display.set_mode((scre…...
外包做了5个月,技术退步一大半了。。。
先说一下自己的情况,本科生,20年通过校招进入深圳某软件公司,干了接近4年的功能测试,今年年初,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试…...
设计模式的艺术P1基础—2.2 类与类的UML图示
设计模式的艺术P1基础—2.2 类与类的UML图示 在UML 2.0的13种图形中,类图是使用频率最高的两种UML图之一(另一种是用于需求建模的用例图),它用于描述系统中所包含的类以及它们之间的相互关系,帮助人们简化对系统的理解…...
PCB 的正片、负片那些事儿
最近在 PCB 打样的过程中遇到了 PCB 的正片层和负片层的问题,故以此记录一下。 问题产生的原因是在投产 PCB 时发现生产稿的 Gerber 图形和 PCB 设计有区别,如图所示,左边为某 PCB 内层,右边为对应层生产稿的 Gerber 图形&#x…...
QT应用篇:QT解析与生成XML文件的四种方式
四种常见的解析 XML 的方式(DOM、SAX、以及基于 Qt 的 XmlStreamReader)各有自己的优缺点,适合不同的应用场景。 DOM 适合小型且结构简单的 XML 文件,需要频繁修改和操作整个文档结构的情况。SAX 适合大型 XML 文件,以及只需读取不需要修改的情况。基于 Qt 的 XmlStreamRe…...
Android 正圆
<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"wrap_content"android:layout_height"wrap_content"android:padding&…...
C#,入门教程(13)——字符(char)及字符串(string)的基础知识
上一篇: C#,入门教程(12)——数组及数组使用的基础知识https://blog.csdn.net/beijinghorn/article/details/123918227 字符串的使用与操作是必需掌握得滚瓜烂熟的编程技能之一!!!!! C#语言实…...
Tracert 与 Ping 程序设计与实现(2024)
1.题目描述 了解 Tracert 程序的实现原理,并调试通过。然后参考 Tracert 程序和计算机网络教材 4.4.2 节, 计算机网络 课程设计指导书 2 编写一个 Ping 程序,并能测试本局域网的所有机器是否在线,运行界面如下图所示的 QuickPing …...
浅谈接口自动化测试
前言 自动化测试,算是近几年比较火热的一个话题,当然,更是软件测试未来的一个发展趋势。未来,功能测试等非核心的测试工作,都将被外包。 想要在软件测试这个行业继续前行,就必须拥有核心竞争力࿰…...
Hyperledger Fabric 核心概念与组件
要理解超级账本 Fabric 的设计,首先要掌握其最基本的核心概念与组件,如节点、交易、排序、共识、通道等。 弄清楚这些核心组件的功能,就可以准确把握 Fabric 的底层运行原理,深入理解其在架构上的设计初衷。知其然,进…...
【C语言题解】 | 101. 对称二叉树
101. 对称二叉树 101. 对称二叉树代码 101. 对称二叉树 这个题目要求判断该二叉树是否为对称二叉树,此题与上一题,即 100. 相同的树 这个题有异曲同工之妙,故此题可借鉴上题。 我们先传入需要判断二叉树的根节点,通过isSameTree()…...
Baumer工业相机堡盟工业相机如何通过NEOAPI SDK实现相机掉线自动重连(C#)
Baumer工业相机堡盟工业相机如何通过NEOAPI SDK实现相机掉线自动重连(C#) Baumer工业相机Baumer工业相机的掉线自动重连的技术背景通过PnP事件函数检查Baumer工业相机是否掉线在NEOAPI SDK里实现相机掉线重连方法:工业相机掉线重连测试演示图…...
[Vulnhub靶机] DriftingBlues: 5
[Vulnhub靶机] DriftingBlues: 5靶机渗透思路及方法(个人分享) 靶机下载地址: https://download.vulnhub.com/driftingblues/driftingblues5_vh.ova 靶机地址:192.168.67.24 攻击机地址:192.168.67.3 一、信息收集 …...
26 数字验证
效果演示 实现了一个简单的数字密码输入表单,用户需要输入一个4位数字密码来验证身份。表单包含一个标题、描述、输入字段、两个按钮和一个关闭按钮。输入字段是一个4位数字密码,用户需要在每个输入框中输入数字来输入密码。两个按钮分别是“验证”和“清…...
echarts - xAxis.type设置time时该如何使用formatter的分级模板
echarts 文档中描述了x轴的多种类型 一、type: ‘value’ ‘value’ 数值轴,适用于连续数据。 此时x轴数据是从零开始,有数据大小的区分。 【注意】 因为xAxis.data是为category服务的,所以xAxis.data里面设置的数据无效。 二、type: ‘ca…...
【代码随想录】刷题笔记Day47
前言 又过了个愉快的周末~大组会终于不用开了,理论上已经可以回家了!但是我多留学校几天吧,回家实在太无聊了,也没太多学习的氛围 198. 打家劫舍 - 力扣(LeetCode) dp[i]含义 考虑下标i(包括…...
6.1 截图工具HyperSnap6简介
图片是组成多媒体作品的基本元素之一,利用图片可以增强多媒体作品的亲和力和说说服力。截取图片最简单的方法是直接按下键盘上的“PrintScreen”键截取整个屏幕或按下“AltPrintScreen”组合键截取当前活动窗口,然后在画笔或者其它的图片处理软件中进行剪…...
stable diffusion 人物高级提示词(二)衣物、身材
一、衣服大类 英文中文Shirt衬衫Blouse女式衬衫Dress连衣裙Skirt裙子Pants裤子Jeans牛仔裤Swimsuit泳衣Underwear内衣Bra文胸Panties内裤Stockings长筒袜Shoes鞋子Socks袜子 二、细分分类 dress 是连衣裙: 英文解释Formal Dress正式礼服,通常用于正式…...
外包做了1个月,技术退步一大半了。。。
先说一下自己的情况,本科生,20年通过校招进入深圳某软件公司,干了接近4年的功能测试,今年年初,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试…...
docker-compose常用命令及.yaml配置模板
1、docker-compose常用命令: docker-compose -f mysql-docker-compose.yaml up -d docker-compose -f mysql-docker-compose.yaml downdocker-compose的常用命令包括: docker-compose up:启动并运行Compose文件中的服务。 docker-compose st…...
工作随机:OEM(13.5)报错代理无法访问
文章目录 前言一、问题排查二、重启主机agent1.定位主机安装位置2.查看并启动agent3.OEM检查 前言 今早接到反馈,在客户部署的OEM(版本 13.5)监控失效,提示代理无法访问,无法访问的除了数据库以外还有主机都显示数据不…...
Pruning Papers
[ICML 2020] Rigging the Lottery: Making All Tickets Winners 整个训练过程中mask是动态的,有drop和grow两步,drop是根据权重绝对值的大小丢弃,grow是根据剩下激活的权重中梯度绝对值生长没有先prune再finetune/retrain的两阶段过程 Laye…...
C#COM对象的资源释放
在C#中使用COM对象时,由于COM对象遵循引用计数(Reference Counting)的管理方式,当COM对象的引用计数为0时,系统才会真正释放该COM对象所占用的资源。然而,在.NET环境下,CLR(Common L…...
了解Apache 配置与应用
本章内容 理解 Apache 连接保持 掌握 Apache 的访问控制 掌握 Apache 日志管理的方法 Apache HTTP Server 之所以受到众多企业的青睐,得益于其代码开源、跨平台、功能 模块化、可灵活定制等诸多优点,不仅性能稳定,在安全性方面的表现也十分…...
悟的复杂度分析
复杂度分析: 时间复杂度(算法中的基本操作的执行次数); 空间复杂度。 时间复杂度: 实际上我们计算时间复杂度时,我们其实并不需要计算准确的执行次数,只需要大概的执行次数,因此我们…...
公司建设网站费用直接列支/百度广告一天多少钱
剑指 Offer 一书中说到: 1、递归有固有的局限性,如果二叉树的深度太大,可能会栈溢出 2、递归的代码太简单,面试的时候希望增加难度 这里手写一个中序遍历,定义节点如下: class TreeNode {int val;TreeNod…...
怎么做一考试网站/最新新闻国内大事件
我今天新建了一个项目,但是在这里却看不到手机的界面效果,如下图: 打开文件res/values/styles.xml, 将下面的代码 <style name"AppTheme" parent"Theme.AppCompat.Light.DarkActionBar"><!-- Custom…...
用ih5做微网站/靠谱的代写平台
URL : 统一资源定位符 (Uniform Resource Locator, URL) 完整的URL由这几个部分构成: scheme://host:port/path?query#fragment scheme 通信协议 (常用的http,ftp,maito等) host 主机 (域名或IP) port 端口号 path 路径 query 查询可选,用于给动态…...
广州网站制作商/宁波seo教程行业推广
题目: 打印出下列图案 * *** ***** ******* ********* 1.程序代码: 2.运行结果:...
可以做网站的魔盒/网址域名ip查询
1.Collection常用接口及子类 Collection 表示一组对象,不包括Map 2.Map :将键映射到值的对象。 3.面试常见问题 3.1 ArrayList和Vector的区别 相同点:这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序集合&…...
临沂建设企业网站/社群营销的十大步骤
Series 一种类似于一维数组的对象 Series 有values 和 index 属性 Series可以看成定长的有序字典 Series对象本身及其索引都有以一个name属性 DataFrame 表格型数据结构 最常用的构建方法:直接传入一个由等长列表或Numpy 数组组成的字典...