Home

Rust 3.2 包和模块

17 Jan 2015 by LelouchHe

原文链接

项目逐渐复杂之后,按照软件工程实践理论,应该要将其分解为一系列组合起来的小模块.另一个重要的实践是定义良好的接口,区分公有接口(public)和私有实现(private).Rust通过模块系统(module system)来帮助实施这些实践.

基本术语: 包(crate)和模块(module)

和Rust模块系统相关的有2个概念: 包(crate)和模块(module).包和其他语言中的库(library)或包(package)是类似的.因此,Cargo就相当于我们的包管理工具:使用Cargo将我们的包传给别人.根据项目的不同,包可以用来构建可执行程序(executable)或者共享库(shared library).

每个包都有一个隐含的根模块(root module),包含着包的全部代码.可以在根模块下定义树形的子模块.模块让你可以在包内部区分不同的代码.

举个例子,开发一个”phrase”包,用不同的语言表达语句.简单起见,我们只表达”欢迎(greeting)”和”再见(farewell)”.下面是模块的结构:

                                +-----------+
                            +---| greetings |
                            |   +-----------+
              +---------+   |
              | english |---+
              +---------+   |   +-----------+
              |             +---| farewells |
+---------+   |                 +-----------+
| phrases |---+
+---------+   |                  +-----------+
              |              +---| greetings |
              +----------+   |   +-----------+
              | japanese |---+
              +----------+   |
                             |   +-----------+
                             +---| farewells |
                                 +-----------+

这里例子中,phrases是包的名称.剩下的都是模块.可以看到,它们组成了一个从phrases为根的树状结构.

既然我们有了这个计划,就可以在代码里定义这些模块了.作为开始,先生成要给新包:

$ cargo new phrases
$ cd phrases

如果你还记得,这样会生成一个简单的项目:

$ tree .
.
|-- Cargo.toml
|-- src
    |-- lib.rs

src/lib.rs就是包的根节点,对应的是结构图中的phrases

定义模块

我们使用mod关键字来定义模块.下面是src/lib.rs:

// in src/lib.rs

mod english {
    mod greetings {
    
    }

    mod farewells {
    
    }
}

mod japanese {
    mod greetings {
    
    }

    mod farewells {
    
    }
}

mod之后,紧跟模块的名称.模块名称遵循Rust命名的惯例: lower_snake_case(小写+下划线).模块的内容包裹在大括号里(“{}”).

在模块内部,还可以声明子模块(sub-mod).可以使用双冒号(“::”)来引用子模块:四个嵌套的模块分别是english::greetings,english::farewells,japanese::greetingsjapanese::farewells.因为子模块是在父模块的命名空间里,所以这些名字并不冲突:english::greetingjapanese::greetings尽管都叫greeting,但彼此并不冲突.

因为这个包没有main函数,而且文件名是lib.rs,所以Cargo会把包构建成库:

$ cargo build
$ ls target
deps libphrases-aa9bd2f1afbef635.rlib native

(译者: 我这里还有build, .fingerprint, examples)

libphrases-hash.rlib就是编译后的包.在学习如何使用这个包之前,我们先将这个包分拆成多个文件.

多文件包

如果每个包只有一个文件,这个文件会变得非常大.通常拆分文件更加容易一些,Rust提供了2种方式来完成分拆.

以前是这样定义模块的:

mod english {
    // code
}

现在我们这样定义:

mod english;

这样定义之后,则需要新建一个文件来表示模块内容,可以是english.rs,或者是english/mod.rs,内容是:

// code

在这些文件里,是不需要重复声明模块的:模块在最初的文件里已经用mod声明过了(译者: 即src/lib.rs中)

修改之后,我们就把包拆分为2个子目录和7个文件:

$ tree .
.
|-- Cargo.toml
|-- src
    |-- english
    |   |-- farewells.rs
    |   |-- greetings.rs
    |   |-- mod.rs
    |
    |-- japanese
    |   |-- farewells.rs
    |   |-- greetings.rs
    |   |-- mod.rs
    |
    |-- lib.rs

src/lib.rs还是包的根文件,代码如下:

// in src/lib.rs

mod english;

mod japanese;

这两句声明,告知Rust去寻找english.rsenglish/mod.rs,以及japanese.rsjapanese/mod.rs.此处,由于我们的子模块还有自己的自模块,所以选择后者目录的形式.在src/english/mod.rssrc/japanese/mod.rs中,代码如下:

// in src/english/mod.rs and src/japanese/mod.rs

mod greetings;

mod farewells;

同理,Rust也是会寻找src/english/greetings.rssrc/english/greetings/mod.rs,其他模块类似.因为这层子模块下面没有其他模块了,所以我们选择文件的形式,即src/english/greetins.rs.

现在,src/english/greetsings.rs等文件都是空的.我们添加一些函数.

先在src/english/greetings.rs:

// in src/english/greetins.rs

fn hello() -> String {
    "Hello!".to_string()
}

然后是src/english/farewells.rs:

// in src/english/farewells.rs

fn goodbye() -> String {
    "Goodbye.".to_string()
}

类似的,在src/japanese/greetings.rs中:

// in src/japanese/greetings.rs

fn hello() -> String {
    "こんにちは".to_string()
}

当然,你可以从这里复制粘贴到你的文件里,或者直接写点其他的字符串.把”konnichiwa”写到程序里对理解模块系统其实帮助不大.

最后是src/japanese/farewells.rs:

// in src/japanese/farewells.rs

fn goodbye() -> String {
    "さようなら".to_string()
}

(好奇的话,这句叫做”Sayōnara”)

这样,包就已经开发完了,我们从其他包里使用下这个包.

导入外部包

有了库包,现在构建一个可执行的包,导入并使用这个库包.

新建src/main.rs,并且输入(现在还无法编译成功):

// in src/main.rs

extern crate phrases;

fn main() {
    println!("Hello in English: {}", phrases::english::greetings::hello());
    println!("Goodbye in English: {}", phrases::english::farewells::goodbye());
    println!("Hello in Japanese: {}", phrases::japanese::greetings::hello());
    println!("Goodbye in Japanese: {}", phrases::japanese::farewells::goodbye());
}

extern crate告诉Rust需要连编phrases包.这样就能在这里使用phrases模块的东西了.上面提到过,要使用双冒号(“::”)来引用模块内的子模块和其中的函数.

而且,Cargo假定src/main.rs是可执行包的根文件,而不是库包的.一旦编译了src/main.rs,就会生成可以运行的可执行文件.这种模式对可执行包很通用:大部分函数在库包里,可执行包只使用这些库.这样,其他的程序也就能使用这些库包,而且这也有助于关注点的分离(separation of concerns).

但这样子无法通过编译.会得到如下的类似错误:

$ cargo build
error: function `hello` is private

所有的模块或函数,默认情况下都是私有的.接下来要更深入的讨论这一点.

导出公有接口(public interface)

Rust允许你精确的控制接口中哪些是公有的(public),所以默认情况下全都是私有的.要想让某些公有,需要使用pub关键字.我们先看english模块,所以首先简化下src/main.rs:

// in src/main.rs

extern crate phrases;

fn main() {
    println!("Hello in English: {}", phrases::english::greetings::hello());
    println!("Goodbye in English: {}", phrases::english::farewells::goodbye());
}

src/lib.rs中,给english的模块声明增加pub:

// in src/lib.rs

pub mod english;

mod japanese;

然后在src/english/mod.rs中,给子模块添加pub:

// in src/english/mod.rs

pub mod greetins;
pub mod farewells;

src/english/greetins.rs中,给函数声明也增加pub:

// in src/english/greetins.rs

pub fn hello() -> String {
    "Hello!".to_string()
}

同样在src/english/farewells.rc做修改:

// in src/english/farewells.rs

pub fn goodbye() -> String {
    "Goodbye.".to_string()
}

现在就能通过编译了,尽管会警告说japanese模块定义了未使用的函数.

$ cargo run

函数是公有的了,就能直接使用了.赞!但是,输入phrases::english::greetings::hello()实在太长了.Rust有另一个关键字可以把名字导入到当前的作用域里,这样你就能使用比较短的名字来引用相关量的.下面介绍use关键字.

使用use导入模块

Rust中可以使用use关键字,将名字导入到当前作用域里.修改下src/main.rs:

// in src/main.rs

extern crate phrases;

use phrases::english::greetings;
use phrases::english::farewells;

fn main() {
    println!("Hello in English: {}", greetings::hello());
    println!("Goodbye in English: {}", farewells::goodbye());
}

这2个use把模块导入到当前作用域,所以就可以通过一个短的名字来引用函数.按照惯例,最好要导入模块,而不是直接导入函数.也就是说,你可以这样:

// in src/main.rs

extern crate phrases;

use phrases::english::greetings::hello;
use phrases::english::farewells::goodbye;

fn main() {
    println!("Hello in English: {}", hello());
    println!("Goodbye in English: {}", goodbye());
}

但这样不推荐.这样子的话更容易带来名字的冲突.简单的程序里没太大影响,但项目变大之后,就是个大问题了.要是名字冲突了,就会编译失败.比如,如果japanese同样改成公有接口,然后这样:

// in src/main.rs

extern crate phrases;

use phrases::english::greetings::hello;
use phrases::japanese::greetings::hello;

fn main() {
    println!("Hello in English: {}", hello());
    println!("Hello in Japanese: {}", hello());
}

就会产生编译错误:

error: a value named `hello` has already been imported in this module
use phrases::japanese::greetings::hello;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

如果要从一个模块中导出多个名字,没必要输入多次.如下的代码:

use phrases::english::greetings;
use phrases::english::farewells;

可以简化为:

use phrases::english::{greetings, farewells};

二者是等价的,而后者更简单些.

使用pub use重新导出

use不仅可以用来简化名称,还可以在你的包里重新导出其他模块里的函数.这就允许你提供一个不和代码组织结构相关的外部公共接口.

举个例子.修改src/main.rs:

// in src/main.rs

extern crate phrases;

use phrases::english::{greetings, farewells};
use phreses::japanese;

fn main() {
    println!("Hello in English: {}", greetings::hello());
    println!("Goodbye in English: {}", farewells::goodbye());
    println!("Hello in Japanese: {}", japanese::hello());
    println!("Goodbye in Japanese: {}", japanese::goodbye());
}

然后修改src/lib.rs,将japanese模块公有:

// in src/lib.rs

pub mod english;

pub mod japanese;

接下来,将两个函数公有化,第一个在src/japanese/greetings.rs:

// in src/japanese/greetings.rs

pub fn hello() -> String {
    "こんにちは".to_string()
}

第二个在src/japanese/farewells.rs:

// in src/japanese/farewells.rs

pub fn goodbye() -> String {
    "さようなら".to_string()
}

最后,修改src/japanese/mod.rs:

// in src/japanese/mod.rs

pub use self::greetings::hello;
pub use self::farewells::goodbye;

mod greetings;
mod farewells;

pub use可以将函数引入到当前的模块中.因为在japanese模块中使用pub use,所以我们现在就可以使用phrases::japanese::hellophrases::japanese::goodbye,尽管这两个函数真正定义在phrases::japanese::greetings::hellophrases::japanese::farewells::goodbye.代码的内部组织结构不影响外部接口的形式.

另外,注意,我们在声明模块之前就pub use了.因为Rust要求use语句只能在文件的最开始.(译者: 如果有extern crate的话,要放在这个之后)

这样就能编译运行了

$ cargo run