模块树中引用项的路径
为了告诉 Rust 在模块树中查找某个项的位置,我们使用路径,就像在导航文件系统时使用路径一样。要调用一个函数,我们需要知道它的路径。
路径可以有两种形式:
- 一个绝对路径是从crate根开始的完整路径;对于来自外部crate的代码,绝对路径以crate名称开始,而对于当前crate的代码,它以字面
crate
开始。 - 一个相对路径从当前模块开始,并使用
self
、super
或当前模块中的标识符。
绝对路径和相对路径后面都跟着一个或多个由双冒号 (::
) 分隔的标识符。
回到清单 7-1,假设我们想要调用 add_to_waitlist
函数。
这相当于问:add_to_waitlist
函数的路径是什么?
清单 7-3 包含了清单 7-1 的部分内容,其中一些模块和函数已被移除。
我们将展示两种从新函数eat_at_restaurant
调用add_to_waitlist
函数的方法,这些函数在crate根目录中定义。这些路径是正确的,但还有一个问题会导致此示例无法编译。我们稍后会解释原因。
eat_at_restaurant
函数是我们库crate的公共API的一部分,所以我们用 pub
关键字标记它。在 “使用 pub
关键字暴露路径” 部分,我们将详细介绍 pub
。
第一次在 eat_at_restaurant
中调用 add_to_waitlist
函数时,
我们使用了绝对路径。add_to_waitlist
函数与 eat_at_restaurant
定义在同一个
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
的绝对路径将保持不变,但相对路径需要更新。我们通常更倾向于指定绝对路径,因为更可能我们希望独立地移动代码定义和项调用。
让我们尝试编译清单 7-3 并找出为什么它还不能编译!我们得到的错误如清单 7-4 所示。
错误消息说模块 hosting
是私有的。换句话说,我们有 hosting
模块和 add_to_waitlist
函数的正确路径,但 Rust 不让我们使用它们,因为它无法访问私有部分。在 Rust 中,默认情况下所有项(函数、方法、结构体、枚举、模块和常量)对父模块都是私有的。如果你想让一个项如函数或结构体私有,你可以将其放在一个模块中。
父模块中的项不能使用子模块中的私有项,但子模块中的项可以使用其祖先模块中的项。这是因为子模块封装并隐藏了它们的实现细节,但子模块可以看到它们被定义的上下文。继续我们的比喻,可以把隐私规则想象成餐厅的后台办公室:后台发生的事情对餐厅顾客来说是私密的,但办公室经理可以看到并操作他们经营的餐厅中的一切。
Rust 选择让模块系统以这种方式工作,以便隐藏内部实现细节成为默认行为。这样,你就知道可以在不破坏外部代码的情况下更改内部代码的哪些部分。然而,Rust 确实提供了通过使用 pub
关键字将项公开的选项,以使子模块的内部代码对父模块可见。
使用 pub
关键字暴露路径
让我们回到列表 7-4 中的错误,它告诉我们 hosting
模块是私有的。我们希望父模块中的 eat_at_restaurant
函数能够访问子模块中的 add_to_waitlist
函数,因此我们使用 pub
关键字标记 hosting
模块,如列表 7-5 所示。
不幸的是,清单 7-5 中的代码仍然会导致编译器错误,如 清单 7-6 所示。
发生了什么?在 mod hosting
前面添加 pub
关键字使模块变为公共模块。通过这一更改,如果我们能够访问 front_of_house
,我们就可以访问 hosting
。但是 hosting
的 内容 仍然是私有的;将模块设为公共模块并不会使其内容也变为公共的。pub
关键字在模块上仅允许其祖先模块中的代码引用它,而不能访问其内部代码。由于模块是容器,仅将模块设为公共模块并不能做太多事情;我们需要进一步选择将模块内的一个或多个项设为公共的。
列表 7-6 中的错误表明 add_to_waitlist
函数是私有的。
隐私规则适用于结构体、枚举、函数和方法以及模块。
让我们也通过在定义前添加 pub
关键字来使 add_to_waitlist
函数公开,如清单 7-7 所示。
现在代码可以编译了!要了解为什么添加 pub
关键字让我们能够在 eat_at_restaurant
中使用这些路径,同时遵守隐私规则,让我们来看看绝对路径和相对路径。
在绝对路径中,我们从 crate
开始,这是我们的 crate 模块树的根。 front_of_house
模块在 crate 根中定义。虽然 front_of_house
不是公共的,但由于 eat_at_restaurant
函数与 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
,路径的其余部分也有效,因此这个函数调用是有效的!
如果您计划共享您的库crate,以便其他项目可以使用您的代码,您的公共API就是您与crate用户的契约,决定了他们如何与您的代码交互。在管理公共API的变更时有许多需要考虑的因素,以使人们更容易依赖您的crate。这些考虑超出了本书的范围;如果您对这个主题感兴趣,请参见Rust API Guidelines。
Best Practices for Packages with a Binary and a Library
我们提到,一个包可以同时包含一个 src/main.rs 二进制包根和一个 src/lib.rs 库包根,默认情况下这两个包都会使用包名。通常,具有这种同时包含库和二进制包模式的包,在二进制包中只包含足够的代码来启动一个可执行文件,该文件调用库包中的代码。这使得其他项目可以从包提供的大部分功能中受益,因为库包的代码可以被共享。
模块树应在src/lib.rs中定义。然后,任何公共项都可以通过以包的名称开头的路径在二进制 crate 中使用。 二进制 crate 成为库 crate 的用户,就像完全外部的 crate 使用库 crate 一样:它只能使用公共 API。 这有助于你设计一个良好的 API;不仅是作者,你也是客户端!
在第 12 章,我们将通过一个命令行程序来演示这种组织实践,该程序将同时包含一个二进制包和一个库包。
以 super
开始的相对路径
我们可以使用 super
在路径的开头构造从父模块而不是当前模块或 crate 根开始的相对路径。这类似于使用 ..
语法开始文件系统路径。使用 super
可以让我们引用我们知道在父模块中的项,这可以在模块与父模块紧密相关但父模块将来可能会在模块树中移动到其他位置时,使重新排列模块树更加容易。
考虑清单 7-8 中的代码,该代码模拟了一位厨师修正错误订单并亲自将其送到客户手中的情况。back_of_house
模块中定义的 fix_incorrect_order
函数通过指定从 super
开始的 deliver_order
路径,调用了父模块中定义的 deliver_order
函数。
fix_incorrect_order
函数位于 back_of_house
模块中,因此我们可以使用 super
转到 back_of_house
的父模块,在这种情况下是 crate
,即根模块。从那里,我们查找 deliver_order
并找到它。成功!我们认为 back_of_house
模块和 deliver_order
函数可能会保持彼此之间的关系,并且如果决定重组 crate 的模块树,它们会被一起移动。因此,我们使用了 super
,这样如果这段代码被移动到不同的模块,将来需要更新的代码位置会更少。
使结构体和枚举公开
我们也可以使用 pub
将结构体和枚举指定为公共的,但 pub
与结构体和枚举的使用有一些额外的细节。如果我们在一个结构体定义前使用 pub
,我们会使该结构体公共,但结构体的字段仍将保持私有。我们可以逐个字段地决定是否将其公开。在清单 7-9 中,我们定义了一个公共的 back_of_house::Breakfast
结构体,其中 toast
字段是公共的,但 seasonal_fruit
字段是私有的。这模拟了在餐厅中顾客可以选择随餐提供的面包类型,但厨师根据季节和库存决定随餐提供的水果。可用的水果变化很快,因此顾客不能选择水果,甚至不能看到他们会得到哪种水果。
因为 back_of_house::Breakfast
结构体中的 toast
字段是公开的,在 eat_at_restaurant
中我们可以使用点符号来写入和读取 toast
字段。注意,我们不能在 eat_at_restaurant
中使用 seasonal_fruit
字段,因为 seasonal_fruit
是私有的。尝试取消注释修改 seasonal_fruit
字段值的行,看看你会得到什么错误!
另外,注意因为 back_of_house::Breakfast
有一个私有字段,所以该结构体需要提供一个公共关联函数来构造一个 Breakfast
的实例(在这里我们将其命名为 summer
)。如果 Breakfast
没有这样一个函数,我们就不能在 eat_at_restaurant
中创建一个 Breakfast
的实例,因为在 eat_at_restaurant
中我们不能设置私有字段 seasonal_fruit
的值。
相比之下,如果我们使一个枚举公开,那么它的所有变体都将公开。我们只需要在 enum
关键字前加上 pub
,如清单 7-10 所示。
因为我们将 Appetizer
枚举公开,所以可以在 eat_at_restaurant
中使用 Soup
和 Salad
变体。
枚举类型除非其变体是公开的,否则并不是很实用;如果每次都要为所有枚举变体标注pub
会很麻烦,所以枚举变体的默认状态是公开的。结构体即使其字段不是公开的也经常是有用的,因此结构体字段遵循一切默认为私有的规则,除非标注了pub
。
还有另一种涉及pub
的情况我们尚未讨论,那就是我们模块系统中的最后一个特性:use
关键字。我们首先单独介绍use
,然后展示如何结合使用pub
和use
。