mod和文件系统

ch07-01-mod-and-the-filesystem.md
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的rand就是这样的 crate。

我们将创建一个提供一些通用网络功能的项目的骨架结构;我们将专注于模块和函数的组织,而不担心函数体中的具体代码。这个项目叫做communicator。Cargo 默认会创建一个库 crate 除非指定其他项目类型,所以如果不像一直以来那样加入--bin参数则项目将会是一个库:

$ cargo new communicator
$ cd communicator

注意 Cargo 生成了 src/lib.rs 而不是 src/main.rs。在 src/lib.rs 中我们会找到这些:

Filename: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
    }
}

Cargo 创建了一个空的测试来帮助我们开始库项目,不像使用--bin参数那样创建一个“Hello, world!”二进制项目。稍后一点会介绍#[]mod tests语法,目前只需确保他们位于 src/lib.rs 中。

因为没有 src/main.rs 文件,所以没有可供 Cargo 的cargo run执行的东西。因此,我们将使用cargo build命令只是编译库 crate 的代码。

我们将学习根据编写代码的意图来选择不同的织库项目代码组织来适应多种场景。

模块定义

对于communicator网络库,首先要定义一个叫做network的模块,它包含一个叫做connect的函数定义。Rust 中所有模块的定义以关键字mod开始。在 src/lib.rs 文件的开头在测试代码的上面增加这些代码:

Filename: src/lib.rs

mod network {
    fn connect() {
    }
}

mod关键字的后面是模块的名字,network,接着是位于大括号中的代码块。代码块中的一切都位于network命名空间中。在这个例子中,只有一个函数,connect。如果想要在network模块外面的代码中调用这个函数,需要指定模块名并使用命名空间语法::,像这样:network::connect(),而不是只是connect()

也可以在 src/lib.rs 文件中同时存在多个模块。例如,再拥有一个client模块,它也有一个叫做connect的函数,如列表 7-1 中所示那样增加这个模块:

Filename: src/lib.rs

mod network {
    fn connect() {
    }
}

mod client {
    fn connect() {
    }
}

Listing 7-1: The network module and the client module defined side-by-side in src/lib.rs

现在我们有了network::connect函数和client::connect函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。

虽然在这个例子中,我们构建了一个库,但是 src/lib.rs 并没有什么特殊意义。也可以在 src/main.rs 中使用子模块。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,client模块和它的函数connect可能放在network命名空间里显得更有道理,如列表 7-2 所示:

Filename: src/lib.rs

mod network {
    fn connect() {
    }

    mod client {
        fn connect() {
        }
    }
}

Listing 7-2: Moving the client module inside of the network module

src/lib.rs 文件中,将现有的mod networkmod client的定义替换为client模块作为network的一个内部模块。现在我们有了network::connectnetwork::client::connect函数:又一次,这两个connect函数也不相冲突,因为他们在不同的命名空间中。

这样,模块之间形成了一个层次结构。src/lib.rs 的内容位于最顶层,而其子模块位于较低的层次。这是列表 7-1 中的例子以这种方式考虑的组织结构:

communicator
 ├── network
 └── client

而这是列表 7-2 中例子的的结构:

communicator
 └── network
     └── client

可以看到列表 7-2 中,clientnetwork的子模块,而不是它的同级模块。更为负责的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中“符合逻辑”的意义全凭你得理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块将是你会喜欢的结构。

将模块移动到其他文件

位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不是所有的内容都落到 src/lib.rs 中了。作为例子,我们将从列表 7-3 中的代码开始:

Filename: src/lib.rs

mod client {
    fn connect() {
    }
}

mod network {
    fn connect() {
    }

    mod server {
        fn connect() {
        }
    }
}

Listing 7-3: Three modules, client, network, and network::server, all defined in src/lib.rs

这是模块层次结构:

communicator
 ├── client
 └── network
     └── server

如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将clientnetworkserver每一个模块从 src/lib.rs 抽出并放入他们自己的文件中。

让我们开始把client模块提取到另一个文件中。首先,将 src/lib.rs 中的client模块代码替换为如下:

Filename: src/lib.rs

mod client;

mod network {
    fn connect() {
    }

    mod server {
        fn connect() {
        }
    }
}

这里我们仍然定义client模块,不过去掉了大括号和client模块中的定义并替换为一个分号,这使得 Rust 知道去其他地方寻找模块中定义的代码。

那么现在需要创建对应模块名的外部文件。在 src/ 目录创建一个 client.rs 文件,接着打开它并输入如下内容,它是上一步client模块中被去掉的connect函数:

Filename: src/client.rs

fn connect() {
}

注意这个文件中并不需要一个mod声明;因为已经在 src/lib.rs 中已经使用mod声明了client模块。这个文件仅仅提供client模块的内容。如果在这里加上一个mod client,那么就等于给client模块增加了一个叫做client的子模块了!

Rust 默认只知道 src/lib.rs 中的内容。如果想要对项目加入更多文件,我们需要在 src/lib.rs 中告诉 Rust 去寻找其他文件;这就是为什么mod client需要被定义在 src/lib.rs 而不是在 src/client.rs

现在,一切应该能成功编译,虽然会有一些警告。记住使用cargo build而不是cargo run因为这是一个库 crate 而不是二进制 crate:

$ cargo build
   Compiling communicator v0.1.0 (file:///projects/communicator)

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/lib.rs:4:5
  |
4 |     fn connect() {
  |     ^

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

这些警告提醒我们有从未被使用的函数。目前不用担心这些警告;在本章的后面会解决他们。好消息是,他们仅仅是警告;我们的项目能够被成功编译。

下面使用相同的模式将network模块提取到它自己的文件中。删除 src/lib.rsnetwork模块的内容并在声明后加上一个分号,像这样:

Filename: src/lib.rs

mod client;

mod network;

接着新建 src/network.rs 文件并输入如下内容:

Filename: src/network.rs

fn connect() {
}

mod server {
    fn connect() {
    }
}

注意这个模块文件中我们也使用了一个mod声明;这是因为我们希望server成为network的一个子模块。

现在再次运行cargo build。成功!不过我们还需要再提取出另一个模块:server。因为这是一个子模块————也就是模块中的模块————目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 src/network.rs 的第一个修改是用mod server;替换server模块的内容:

Filename: src/network.rs

fn connect() {
}

mod server;

接着创建 src/server.rs 文件并输入需要提取的server模块的内容:

Filename: src/server.rs

fn connect() {
}

当尝试运行cargo build时,会出现如列表 7-4 中所示的错误:

$ cargo build
   Compiling communicator v0.1.0 (file:///projects/communicator)
error: cannot declare a new module at this location
 --> src/network.rs:4:5
  |
4 | mod server;
  |     ^^^^^^
  |
note: maybe move this module `network` to its own directory via `network/mod.rs`
 --> src/network.rs:4:5
  |
4 | mod server;
  |     ^^^^^^
note: ... or maybe `use` the module `server` instead of possibly redeclaring it
 --> src/network.rs:4:5
  |
4 | mod server;
  |     ^^^^^^

Listing 7-4: Error when trying to extract the server submodule into src/server.rs

这个错误说明“不能在这个位置新声明一个模块”并指出 src/network.rs 中的mod server;这一行。看来 src/network.rssrc/lib.rs 在某些方面是不同的;让我们继续阅读以理解这是为什么。

列表 7-4 中间的记录事实上是非常有帮助的,因为它指出了一些我们还未讲到的操作:

note: maybe move this module `network` to its own directory via `network/mod.rs`

我们可以按照记录所建议的去操作,而不是继续使用之前的与模块同名的文件的模式:

  1. 新建一个叫做 network目录,这是父模块的名字
  2. src/network.rs 移动到新建的 network 目录中并重命名,现在它是 src/network/mod.rs
  3. 将子模块文件 src/server.rs 移动到 network 目录中

如下是执行这些步骤的命令:

$ mkdir src/network
$ mv src/network.rs src/network/mod.rs
$ mv src/server.rs src/network

现在如果运行cargo build的话将顺利编译(虽然仍有警告)。现在模块的布局看起来仍然与列表 7-3 中所有代码都在 src/lib.rs 中时完全一样:

communicator
 ├── client
 └── network
     └── server

对应的文件布局现在看起来像这样:

├── src
│   ├── client.rs
│   ├── lib.rs
│   └── network
│       ├── mod.rs
│       └── server.rs

那么,当我们想要提取network::server模块时,为什么也必须将 src/network.rs 文件改名成 src/network/mod.rs 文件呢,还有为什么要将network::server的代码放入 network 目录的 src/network/server.rs 文件中,而不能将network::server模块提取到 src/server.rs 中呢?原因是如果 server.rs 文件在 src 目录中那么 Rust 就不能知道server应当是network的子模块。为了更清除的说明为什么 Rust 不知道,让我们考虑一下有着如下层级的另一个例子,它的所有定义都位于 src/lib.rs 中:

communicator
 ├── client
 └── network
     └── client

在这个例子中,仍然有这三个模块,clientnetworknetwork::client。如果按照与上面最开始将模块提取到文件中相同的步骤来操作,对于client模块会创建 src/client.rs。对于network模块,会创建 src/network.rs。但是接下来不能将network::client模块提取到 src/client.rs 文件中,因为它已经存在了,对应顶层的client模块!如果将clientnetwork::client的代码都放入 src/client.rs 文件,Rust 将无从可知这些代码是属于client还是network::client的。

因此,一旦想要将network模块的子模块network::client提取到一个文件中,需要为network模块新建一个目录替代 src/network.rs 文件。接着network模块的代码将进入 src/network/mod.rs 文件,而子模块network::client将拥有其自己的文件 src/network/client.rs。现在顶层的 src/client.rs 中的代码毫无疑问的都属于client模块。

模块文件系统的规则

与文件系统相关的模块规则总结如下:

  • 如果一个叫做foo的模块没有子模块,应该将foo的声明放入叫做 foo.rs 的文件中。
  • 如果一个叫做foo的模块有子模块,应该将foo的声明放入叫做 foo/mod.rs 的文件中。

这些规则适用于递归(嵌套),所以如果foo模块有一个子模块barbar没有子模块,则 src 目录中应该有如下文件:

├── foo
│   ├── bar.rs (contains the declarations in `foo::bar`)
│   └── mod.rs (contains the declarations in `foo`, including `mod bar`)

模块自身则应该使用mod关键字定义于父模块的文件中。

接下来,我们讨论一下pub关键字,并除掉那些警告!