这里继续沿用上次工程rust-demo
为了显示在模块树中何处可以找到一个项目,我们使用了一个路径,就像我们在导航文件系统时使用路径一样。如果我们想调用一个函数,我们需要知道它的路径。
路径可以有两种形式:
绝对路径和相对路径后面都跟有一个或多个由双冒号(::)分隔的标识符。
让我们回到之前的例子。我们如何调用add_to_waitlist函数?这就跟问,add_to_waitlist函数的路径是什么一样?下例中包含之前的例子,删除了一些模块和功能。我们将展示两种方法来从在crate根中定义的新函数eat_at_restaurant调用add_to_waitlist函数。eat_at_restaurant函数是我们的库crate的公共API的一部分,所以我们用pub关键字标记它。在“使用pub关键字公开路径”一节中,我们将更详细地讨论pub。注意,这个例子还不能编译;我们稍后会解释原因。
- mod front_of_house {
- mod hosting {
- fn add_to_waitlist() {}
- }
- }
-
- pub fn eat_at_restaurant() {
- // 使用crate绝对和相对路径调用
- // Absolute path
- crate::front_of_house::hosting::add_to_waitlist(); // crate开始绝对路径
-
- // Relative path
- front_of_house::hosting::add_to_waitlist();
- }
我们第一次在eat_at_restaurant中调用add_to_waitlist函数时,使用的是绝对路径。add_to_waitlist函数与eat_at_restaurant在同一个crate中定义,这意味着我们可以使用crate关键字开始一个绝对路径。
在crate之后,我们包括每个连续的模块,直到我们添加到等待列表。您可以想象一个具有相同结构的文件系统,我们将指定路径/front _ of _ house/hosting/add_to_waitlist来运行add _ to _ wait list程序;使用板条箱名称从板条箱根目录开始就像在shell中使用/从文件系统根目录开始一样。
第二次在eat_at_restaurant中调用add_to_waitlist时,我们使用的是相对路径。路径以front_of_house开始,这是在模块树中与eat_at_restaurant相同级别定义的模块的名称。在这里,文件系统等同于使用front _ of _ house/hosting/add _ to _ wait list路径。以名称开头意味着路径是相对的。
选择使用相对路径还是绝对路径取决于您的项目。这个决定应该取决于您是更倾向于将项目定义代码与使用该项目的代码分开移动还是一起移动。例如,如果我们将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();
- | ^^^^^^^ private module
- |
- note: the module `hosting` is defined here
- --> src/lib.rs:2:5
- |
- 2 | mod hosting {
- | ^^^^^^^^^^^
-
- error[E0603]: module `hosting` is private
- --> src/lib.rs:12:21
- |
- 12 | front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^ private module
- |
- note: the module `hosting` is defined here
- --> src/lib.rs:2:5
- |
- 2 | mod hosting {
- | ^^^^^^^^^^^
-
- For more information about this error, try `rustc --explain E0603`.
- error: could not compile `restaurant` due to 2 previous errors
错误消息指出模块hosting是私有的。换句话说,我们有hosting模块和add_to_waitlist函数的正确路径,但是Rust不允许我们使用它们,因为它不能访问私有部分。
模块不仅仅对组织代码有用。它们还定义了Rust的隐私边界:封装实现细节的代码行不允许外部代码知道、调用或依赖。所以,如果你想把一个函数或者结构变成私有的,你可以把它放在一个模块中。
Rust中隐私的工作方式是,默认情况下,所有项目(函数、方法、结构、枚举、模块和常量)都是私有的。父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用其祖先模块中的项。原因是子模块包装并隐藏了它们的实现细节,但是子模块可以看到定义它们的上下文。继续餐厅的比喻,把隐私规则想象成餐厅的后勤办公室:那里发生的事情是餐厅顾客的隐私,但办公室经理可以看到并做他们经营的餐厅中的一切。
Rust选择让模块系统以这种方式运行,因此默认情况下隐藏内部实现细节。这样,您就知道可以在不破坏外部代码的情况下更改内部代码的哪些部分。但是您可以通过使用pub关键字将项目公开,从而将子模块代码的内部部分公开给外部祖先模块。
让我们回到上例中的错误,它告诉我们hosting模块是私有的。我们希望父模块中的eat_at_restaurant函数能够访问子模块中的add_to_waitlist函数,所以我们用pub关键字标记hosting模块,如下所示。
文件名:src/lib.rs
- mod front_of_house {
- pub mod hosting { // 将hosting模块声明为pub,以便从eat_at_restaurant使用它
- 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();
- | ^^^^^^^^^^^^^^^ private function
- |
- note: the function `add_to_waitlist` is defined here
- --> src/lib.rs:3:9
- |
- 3 | fn add_to_waitlist() {}
- | ^^^^^^^^^^^^^^^^^^^^
-
- error[E0603]: function `add_to_waitlist` is private
- --> src/lib.rs:12:30
- |
- 12 | front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^^^^^^^^^ private function
- |
- note: the function `add_to_waitlist` is defined here
- --> src/lib.rs:3:9
- |
- 3 | fn add_to_waitlist() {}
- | ^^^^^^^^^^^^^^^^^^^^
-
- For more information about this error, try `rustc --explain E0603`.
- error: could not compile `restaurant` due to 2 previous errors
发生了什么事?在mod hosting前面添加pub关键字使模块成为公共的。有了这个改变,如果我们可以访问front_of_house,我们就可以访问hosting。但是hosting的内容还是私密的;将模块公开并不会将其内容公开。模块上的pub关键字只允许其祖先模块中的代码引用它。
上述的错误表明add_to_waitlist函数是私有的。隐私规则适用于结构、枚举、函数和方法以及模块。
我们还可以通过在函数定义前添加pub关键字来公开add_to_waitlist函数,如下所示:
文件名:src/lib.rs
- mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {} // 将pub关键字添加到mod hosting和fn add_to_waitlist让我们可以从eat_at_restaurant调用该函数
- }
- }
-
- 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模块不是公共的,但是因为eat_at_restaurant函数是在与front_of_house相同的模块中定义的(也就是说,eat_at_restaurant和front_of_house是兄弟),所以我们可以从eat_at_restaurant引用front_of_house。接下来是标有pub的托管模块。我们可以访问主机的父模块,所以我们可以访问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的变化的考虑,以使人们更容易依赖你的机箱。
具有二进制文件和库的包的最佳实践
我们提到一个包可以包含一个src/main.rs二进制箱根目录和一个src/lib.rs库箱根目录,默认情况下两个箱都有包名。通常,具有这种模式的包在二进制箱中有足够的代码来启动一个可执行程序,该程序调用库箱中的代码。这让其他项目受益于软件包提供的大部分功能,因为库箱的代码可以共享。
应该在src/lib.rs中定义模块树。然后,通过以包的名称开始路径,可以在二进制箱中使用任何公共项目。二进制箱成为箱的用户,就像完全外部的箱使用库箱一样:它只能使用公共API。这有助于你设计一个好的API你不仅是作者,还是客户!
我们还可以通过在路径的开头使用super来构造从父模块开始的相对路径。这就像用..语法。我们为什么要这么做?
考虑下例中的代码,它模拟了一个厨师修正了一个错误的订单并亲自把它带给顾客的情况。back_of_house模块中定义的函数fix_incorrect_order调用父模块中定义的函数deliver_order,方法是指定以super开头的deliver_order路径:
文件名:src/lib.rs
- fn deliver_order() {}
-
- mod back_of_house {
- fn fix_incorrect_order() {
- cook_order();
- super::deliver_order(); // 使用以super开头的相对路径调用函数
- }
-
- fn cook_order() {}
- }
fix_incorrect_order函数在back_of_house模块中,所以我们可以使用super转到back_of_house的父模块,在本例中是crate,即根。从那里,我们寻找deliver_order并找到它。成功!我们认为back_of_house模块和deliver_order函数可能彼此保持相同的关系,如果我们决定重新组织箱的模块树,它们可能会移到一起。因此,我们使用了super,这样如果代码被移动到不同的模块中,我们就有更少的地方来更新代码。
我们也可以使用pub将结构和枚举指定为公共的,但是还有一些额外的细节。如果我们在一个结构定义之前使用pub,我们使该结构成为公共的,但是该结构的字段仍然是私有的。我们可以根据具体情况公开或不公开每个字段。在下例中,我们定义了一个公共的back_of_house::Breakfast结构,它有一个公共的toast字段和一个私有的seasonal _ fruit字段。这模拟了一家餐厅的情况,顾客可以选择随餐面包的类型,但厨师根据当季和库存的水果来决定配餐水果。可供选择的水果变化很快,所以顾客无法选择水果,甚至无法看到他们会得到哪种水果。
文件名:src/lib.rs
- // 具有一些公共字段和一些私有字段的结构
- 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字段值的行的注释,看看会得到什么样的错误!
还要注意,因为back_of_house::Breakfast有一个私有字段,所以该结构需要提供一个公共关联函数来构造一个Breakfast的实例(我们在这里将其命名为summer)。如果早餐没有这样的功能,我们就无法在eat_at_restaurant中创建Breakfast的实例,因为我们无法在eat_at_restaurant中设置私有的seasonal_fruit字段的值。
相反,如果我们使一个枚举成为公共的,那么它的所有变体都是公共的。我们只需要enum关键字前的pub,如下所示。
文件名:src/lib.rs
- 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 enum,所以我们可以在eat_at_restaurant中使用汤和沙拉变体。枚举不是很有用,除非它们的变体是公共的;在每种情况下都必须用pub注释所有的enum变体是很烦人的,所以enum变体的默认设置是public。如果结构的字段不是公共的,那么结构通常是有用的,所以结构字段遵循默认情况下一切都是私有的一般规则,除非用pub注释。
还有一种涉及pub的情况我们还没有涉及到,那就是我们最后一个模块系统特性:use关键字。我们将首先讨论use本身,然后展示如何将pub和use结合起来。