使用pub控制可见性

ch07-02-controlling-visibility-with-pub.md
commit e2a129961ae346f726f8b342455ec2255cdfed68

我们通过将networknetwork::server的代码分别移动到 src/network/mod.rssrc/network/server.rs 文件中解决了列表 7-4 中出现的错误信息。现在,cargo build能够构建我们的项目,不过仍然有一些警告信息,表示client::connectnetwork::connectnetwork::server::connect函数没有被使用:

warning: function is never used: `connect`, #[warn(dead_code)] on by default
src/client.rs:1:1
  |
1 | fn connect() {
  | ^

warning: function is never used: `connect`, #[warn(dead_code)] on by default
 --> src/network/mod.rs:1:1
  |
1 | fn connect() {
  | ^

warning: function is never used: `connect`, #[warn(dead_code)] on by default
 --> src/network/server.rs:1:1
  |
1 | fn connect() {
  | ^

那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被用户使用,而不一定要被项目自身使用,所以不应该担心这些函数是未被使用的。创建他们的意义就在于被另一个项目而不是被自己使用。

为了理解为什么这个程序出现了这些警告,尝试作为另一个项目来使用这个connect库,从外部调用他们。为此,通过创建一个包含这些代码的 src/main.rs 文件,在与库 crate 相同的目录创建一个二进制 crate:

Filename: src/main.rs

extern crate communicator;

fn main() {
    communicator::client::connect();
}

使用extern crate指令将communicator库 crate 引入到作用域,因为事实上我们的包包含两个 crate。Cargo 认为 src/main.rs 是一个二进制 crate 的根文件,与现存的以 src/lib.rs 为根文件的库 crate 相区分。这个模式在可执行项目中非常常见:大部分功能位于库 crate 中,而二进制 crate 使用这个库 crate。通过这种方式,其他程序也可以使用这个库 crate,这是一个很好的关注分离(separation of concerns)。

从一个外部 crate 的视角观察communicator库的内部,我们创建的所有模块都位于一个与 crate 同名的模块内部,communicator。这个顶层的模块被称为 crate 的根模块root module)。

另外注意到即便在项目的子模块中使用外部 crate,extern crate也应该位于根模块(也就是 src/main.rssrc/lib.rs)。接着,在子模块中,我们就可以像顶层模块那样引用外部 crate 中的项了。

我们的二进制 crate 如今正好调用了库中client模块的connect函数。然而,执行cargo build会在之前的警告之后出现一个错误:

error: module `client` is private
 --> src/main.rs:4:5
  |
4 |     communicator::client::connect();
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

啊哈!这告诉了我们client模块是私有的,这也正是那些警告的症结所在。这也是我们第一次在 Rust 上下文中涉及到公有私有的概念。Rust 所有代码的默认状态是私有的:除了自己之外别人不允许使用这些代码。如果不在自己的项目中使用一个私有函数,因为程序自身是唯一允许使用这个函数的代码,Rust 会警告说函数未被使用。

一旦我们指定一个像client::connect的函数为公有,不光二进制 crate 中的函数调用会被允许,函数未被使用的警告也会消失。将其标记为公有让 Rust 知道了我们意在使函数在我们程序的外部被使用。现在这个可能的理论上的外部可用性使 Rust 认为这个函数“已经被使用”。因此。当某项被标记为公有,Rust 不再要求它在程序自身被使用并停止警告某项未被使用。

标记函数为公有

为了告诉 Rust 某项为公有,在想要标记为公有的项的声明开头加上pub关键字。现在我们将致力于修复client::connect未被使用的警告,以及二进制 crate 中“模块client是私有的”的错误。像这样修改 src/lib.rs 使client模块公有:

Filename: src/lib.rs

pub mod client;

mod network;

pub写在mod之前。再次尝试构建:

<warnings>
error: function `connect` is private
 --> src/main.rs:4:5
  |
4 |     communicator::client::connect();
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

非常好!另一个不同的错误!好的,不同的错误信息是值得庆祝的(可能是程序员被黑的最惨的一次)。新错误表明“函数connect是私有的”,那么让我们修改 src/client.rsclient::connect也设为公有:

Filename: src/client.rs

pub fn connect() {
}

再再一次运行cargo build

warning: function is never used: `connect`, #[warn(dead_code)] on by default
 --> src/network/mod.rs:1:1
  |
1 | fn connect() {
  | ^

warning: function is never used: `connect`, #[warn(dead_code)] on by default
 --> src/network/server.rs:1:1
  |
1 | fn connect() {
  | ^

编译通过了,关于client::connect未被使用的警告消失了!

未被使用的代码并不总是意味着他们需要被设为公有的:如果你希望这些函数成为公有 API 的一部分,未被使用的代码警告可能是在警告你这些代码不再需要并可以安全的删除他们。这也可能是警告你出 bug,如果你刚刚不小心删除了库中所有这个函数的调用。

当然我们的情况是,确实希望另外两个函数也作为 crate 公有 API 的一部分,所以让我们也将其标记为pub并去掉剩余的警告。修改 src/network/mod.rs 为:

Filename: src/network/mod.rs

pub fn connect() {
}

mod server;

并编译:

warning: function is never used: `connect`, #[warn(dead_code)] on by default
 --> src/network/mod.rs:1:1
  |
1 | pub fn connect() {
  | ^

warning: function is never used: `connect`, #[warn(dead_code)] on by default
 --> src/network/server.rs:1:1
  |
1 | fn connect() {
  | ^

恩,虽然将network::connect设为pub了我们仍然得到了一个未被使用函数的警告。这是因为模块中的函数是公有的,不过函数所在的network模块却不是公有的。这回我们是自内向外修改库文件的,而client::connect的时候是自外向内修改的。我们需要修改 src/lib.rsnetwork 也是公有的:

Filename: src/lib.rs

pub mod client;

pub mod network;

现在再编译的话,那个警告就消失了:

warning: function is never used: `connect`, #[warn(dead_code)] on by default
 --> src/network/server.rs:1:1
  |
1 | fn connect() {
  | ^

只剩一个警告了!尝试自食其力修改它吧!

私有性规则

总的来说,有如下项的可见性规则:

  1. 如果一个项是公有的,它能被任何父模块访问
  2. 如果一个项是私有的,它只能被当前模块或其子模块访问

私有性示例

让我们看看更多例子作为练习。创建一个新的库项目并在新项目的 src/lib.rs 输入列表 7-5 中的代码:

Filename: src/lib.rs
mod outermost {
    pub fn middle_function() {}

    fn middle_secret_function() {}

    mod inside {
        pub fn inner_function() {}

        fn secret_function() {}
    }
}

fn try_me() {
    outermost::middle_function();
    outermost::middle_secret_function();
    outermost::inside::inner_function();
    outermost::inside::secret_function();
}

Listing 7-5: Examples of private and public functions, some of which are incorrect

在尝试编译这些代码之前,猜测一下try_me函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论!

检查错误

try_me函数位于项目的根模块。叫做outermost的模块是私有的,不过第二条私有性规则说明try_me函数允许访问outermost模块,因为outermost位于当前(根)模块,try_me也是。

outermost::middle_function的调用是正确的。因为middle_function是公有的,而try_me通过其父模块访问middle_functionoutermost。根据上一段的规则我们可以确定这个模块是可访问的。

outermost::middle_secret_function的调用会造成一个编译错误。middle_secret_function是私有的,所以第二条(私有性)规则生效了。根模块既不是middle_secret_function的当前模块(outermost是),也不是middle_secret_function当前模块的子模块。

叫做inside的模块是私有的且没有子模块,所以它只能被当前模块访问,outermost。这意味着try_me函数不允许调用outermost::inside::inner_functionoutermost::inside::secret_function任何一个。

修改错误

这里有一些尝试修复错误的代码修改意见。在你尝试他们之前,猜测一下他们哪个能修复错误,接着编译查看你是否猜对了,并结合私有性规则理解为什么。

  • 如果inside模块是公有的?
  • 如果outermost是公有的而inside是私有的?
  • 如果在inner_function函数体中调用::outermost::middle_secret_function()?(开头的两个冒号意味着从根模块开始引用模块。)

请随意设计更多的实验并尝试理解他们!

接下来,让我们讨论一下使用use关键字来将项引入作用域。