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::greetings
和japanese::farewells
.因为子模块是在父模块的命名空间里,所以这些名字并不冲突:english::greeting
和japanese::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.rs
或english/mod.rs
,以及japanese.rs
或japanese/mod.rs
.此处,由于我们的子模块还有自己的自模块,所以选择后者目录的形式.在src/english/mod.rs
和src/japanese/mod.rs
中,代码如下:
// in src/english/mod.rs and src/japanese/mod.rs
mod greetings;
mod farewells;
同理,Rust也是会寻找src/english/greetings.rs
或src/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::hello
和phrases::japanese::goodbye
,尽管这两个函数真正定义在phrases::japanese::greetings::hello
和phrases::japanese::farewells::goodbye
.代码的内部组织结构不影响外部接口的形式.
另外,注意,我们在声明模块之前就pub use
了.因为Rust要求use
语句只能在文件的最开始.(译者: 如果有extern crate
的话,要放在这个之后)
这样就能编译运行了
$ cargo run