% Rust的Crates和Modules

当一个项目越来越庞大、代码越来越多的时候,一个好的工程实施方式是将这些代码划分为一个部分一个部分,分而治之,并且采用private和public限定方式,定义好各部分之间的接口。为了方便这种工程实施方式,Rust中采用了模块的代码组织方式。

基本概念:Crates和Modules

Rust有两个不同的模块组织方式:CrateModule。一个Crate相当于其它语言的librarypackage。Rust中采用Cargo这个包管理工具,来管理一个一个的Crate。一个Crate能够被生成为一个可执行文件或共享库。

每个Crate都有一个隐含的根模块文件,其它的模块就引用在这个根模块文件下,这其中关键的手段就是采用Module

下面讲一个例子,我们将生成一个名为”phrases”的Crate,这个Crate包含了不同语言的短语,为了使例子简单,仅区分了英语和中文的”greetings”和”farewells”。该例子的模块设计如下:

  1.   +-----------+
  2. +---| greetings |
  3. | +-----------+
  4. +---------+ |
  5. | english |---+
  6. +---------+ | +-----------+
  7. | +---| farewells |
  8. +---------+ | +-----------+
  9. | phrases |---+
  10. +---------+ | +-----------+
  11. | +---| greetings |
  12. +----------+ | +-----------+
  13. | chinese |---+
  14. +----------+ |
  15. | +-----------+
  16. +---| farewells |
  17. +-----------+

在这个例子中,”phrases”是这个Crate的名子,其它的就作为Module,从图中可以看出,这个Crate的组织方式就像一个树型结构,”phrases”是这个Crate的根,”english”和”chinese”两个Module是这个Crate的分枝。

下面用代码来实现这个Crate

  1. $ cargo new phrases //这是在创建一个library
  2. $ cd phrases
  3. $ tree
  4. .
  5. ├── Cargo.toml
  6. └── src
  7. └── lib.rs
  8. 1 directory, 2 files

src/lib.rs就是”phrases”这个Crate的根。

定义Modules

我们将使用mod这个关键定来定义各个模块,src/lib.rs里的代码如下:

  1. mod english {
  2. mod greetings {
  3. }
  4. mod farewells {
  5. }
  6. }
  7. mod chinese {
  8. mod greetings {
  9. }
  10. mod farewells {
  11. }
  12. }

上面的代码中可见,模块的定义是mod后紧跟模块名,模块内部代码包含在{}之间。在一个已知的mod中,可以定义下级mod,并通过::符号引用下级mod。在本例中的四个嵌套模块引用方式如下:english::greetingsenglish::farewellschinese::greetingschinese::farewells,这里的greetingsfarewells分别分布在两个不同的模块中,所以并没有命名冲突。

由于这个Crate没有定义main()函数,所以用Cargo工具只能生成一个库文件:

  1. $ cargo build
  2. Compiling phrases v0.0.1 (file:///%E6%A1%8C%E9%9D%A2/phrases)
  3. $ ls target/
  4. deps examples libphrases-066b8293cb7b2145.rlib native

libphrases-066b8293cb7b2145.rlib就是生成后的库文件。

分割为多个文件组成的Crate

如果每个Crate仅包含一个文件,一旦工程代码增加,这个Crate将变得很大,而难以维护。通常都是将这个Crate分割成多个文件,Rust支持如下文件组织方式:

结合前面的”phrases”的描述,如果要声明一个english的下级模块,我们可以在Cargo生成的项目代码中创建src/english.rssrc/english/mod.rs代码文件,并在src/lib.rs文件中写入的如下代码:

  1. mod english;

这样就可以引用english模块。

其项目目标文件构成如下(注:在src/english/的下级文件中并没有写入代码):

  1. $ tree
  2. .
  3. ├── Cargo.lock
  4. ├── Cargo.toml
  5. ├── src
  6. ├── english
  7. ├── farewells.rs
  8. ├── greetings.rs
  9. └── mod.rs
  10. ├── chinese
  11. ├── farewells.rs
  12. ├── greetings.rs
  13. └── mod.rs
  14. └── lib.rs
  15. └── target
  16. ├── deps
  17. ├── examples
  18. ├── libphrases-066b8293cb7b2145.rlib
  19. └── native
  20. 7 directories, 10 files

其中src/lib.rs就是”phrases”这个Crate的根,其引用下级模块的代码如下:

  1. mod english;
  2. mod chinese;

这样的声明,告诉Rust自动寻找src/english.rssrc/chinese.rs或者src/english/mod.rssrc/chinese/mod.rs。同样,在englishchinese这一级模块中要引用下级模块就可以分别在src/english/mod.rssrc/chinese/mod.rs进行声明:

  1. mod greetings;
  2. mod farewells;

同样,这个声明将告诉Rust自动寻找src/english/greetings.rssrc/chinese/greetings.rs或者src/english/farewells/mod.rssrc/chinese/farewells/mod.rs

截止目前,src/englishsrc/chinese下所有文件都是空的。下面,我们将在相应的文件中定义一些函数。

src/english/greetings.rs输入如下:

  1. fn hello() -> String {
  2. "hello!".to_string()
  3. }

src/english/farewells.rs输入如下:

  1. fn goodbye() -> String {
  2. "goodbye!".to_string()
  3. }

src/chinese/greetings.rs输入如下:

  1. fn hello() -> String {
  2. "你好".to_string()
  3. }

src/chinese/farewells.rs输入如下:

  1. fn goodbye() -> String {
  2. "再见".to_string()
  3. }

下面,让我们来进行模块之间的相互调用(仅定了函数,而没有引用,在运行cargo build的时候,Rust会自动报错误)。

引入Crates定义的库

前面已经定义了一个名为”phrases”的Crate,如何使用这个库呢?

下面将创建一个src/main.rs文件,并输入如下代码:

  1. extern crate phrases;
  2. fn main() {
  3. println!("Hello in English: {}", phrases::english::greetings::hello());
  4. println!("Goodbye in English: {}", phrases::english::farewells::goodbye());
  5. println!("Hello in Chinese: {}", phrases::chinese::greetings::hello());
  6. println!("Goodbye in Chinese: {}", phrases::chinese::farewells::goodbye());
  7. }

extern crate声明编译时将链接”phrases”这个Crate,这样才能通过::逐级引用”phrases”里面定义的函数。

同时,Cargo也认为src/main.rs也是一个Crate的根,一旦编译src/main.rs成功将获得一个可执行文件。目前,我们的项目包含了src/lib.rssrc/main.rs这两个Crate,这种组织方式对于生成可执行的Crate非常普遍,很多时候将函数定义在库Crate中,生成可执行的Crate调用这个库。

下面我们来编译这个项目,但会报错如下:

  1. $ cargo build
  2. Compiling phrases v0.0.1 (file:///%E6%A1%8C%E9%9D%A2/phrases)
  3. /phrases/src/english/greetings.rs:1:1: 3:2 warning: function is never used: `hello`, #[warn(dead_code)] on by default
  4. /phrases/src/english/greetings.rs:1 pub fn hello() -> String {
  5. /phrases/src/english/greetings.rs:2 "hello!".to_string()
  6. /phrases/src/english/greetings.rs:3 }
  7. /phrases/src/english/farewells.rs:1:1: 3:2 warning: function is never used: `goodbye`, #[warn(dead_code)] on by default
  8. /phrases/src/english/farewells.rs:1 pub fn goodbye() -> String {
  9. /phrases/src/english/farewells.rs:2 "goodbye".to_string()
  10. /phrases/src/english/farewells.rs:3 }
  11. /phrases/src/chinese/greetings.rs:1:1: 3:2 warning: function is never used: `hello`, #[warn(dead_code)] on by default
  12. /phrases/src/chinese/greetings.rs:1 fn hello() -> String {
  13. /phrases/src/chinese/greetings.rs:2 "你好".to_string()
  14. /phrases/src/chinese/greetings.rs:3 }
  15. /phrases/src/chinese/farewells.rs:1:1: 3:2 warning: function is never used: `goodbye`, #[warn(dead_code)] on by default
  16. /phrases/src/chinese/farewells.rs:1 fn goodbye() -> String {
  17. /phrases/src/chinese/farewells.rs:2 "再见".to_string()
  18. /phrases/src/chinese/farewells.rs:3 }
  19. /phrases/src/main.rs:4:38: 4:72 error: function `hello` is private
  20. /phrases/src/main.rs:4 println!("Hello in English: {}", phrases::english::greetings::hello());
  21. ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  22. note: in expansion of format_args!
  23. <std macros>:2:23: 2:77 note: expansion site
  24. <std macros>:1:1: 3:2 note: in expansion of println!
  25. /phrases/src/main.rs:4:5: 4:76 note: expansion site
  26. /phrases/src/main.rs:5:40: 5:76 error: function `goodbye` is private
  27. /phrases/src/main.rs:5 println!("Goodbye in English: {}", phrases::english::farewells::goodbye());
  28. ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  29. note: in expansion of format_args!
  30. <std macros>:2:23: 2:77 note: expansion site
  31. <std macros>:1:1: 3:2 note: in expansion of println!
  32. /phrases/src/main.rs:5:5: 5:80 note: expansion site
  33. /phrases/src/main.rs:6:39: 6:74 error: function `hello` is private
  34. /phrases/src/main.rs:6 println!("Hello in Chinese: {}", phrases::chinese::greetings::hello());
  35. ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  36. note: in expansion of format_args!
  37. <std macros>:2:23: 2:77 note: expansion site
  38. <std macros>:1:1: 3:2 note: in expansion of println!
  39. /phrases/src/main.rs:6:5: 6:78 note: expansion site
  40. /phrases/src/main.rs:7:41: 7:78 error: function `goodbye` is private
  41. /phrases/src/main.rs:7 println!("Goodbye in Chinese: {}", phrases::chinese::farewells::goodbye());
  42. ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  43. note: in expansion of format_args!
  44. <std macros>:2:23: 2:77 note: expansion site
  45. <std macros>:1:1: 3:2 note: in expansion of println!
  46. /phrases/src/main.rs:7:5: 7:82 note: expansion site
  47. error: aborting due to 4 previous errors
  48. Could not compile `phrases`.
  49. To learn more, run the command again with --verbose.

通过报错的信息可见,在Rust所有的模块和函数默认情况下都是私有的。

定义公共接口

虽然默认情况下,Rust定义的函数、模块和变量都是私有的,但Rust提供了非常精细地定义公共接口的方法。一般使用pub这个关键字来定义公共接口。为了上面的例子能够正常运行,我们将在相应的模块和函数处添加pub关键字:

src/lib.rs代码修改如下:

  1. pub mod english;
  2. pub mod chinese;

src/english/mod.rssrc/chinese/mod.rs代码修改如下:

  1. pub mod greetings;
  2. pub mod farewells;

src/english/greetings.rs代码修改如下:

  1. pub fn hello() -> String {
  2. "hello!".to_string()
  3. }

src/english/farewells.rs代码修改如下:

  1. pub fn goodbye() -> String {
  2. "goodbye!".to_string()
  3. }

src/chinese/greetings.rs代码修改如下:

  1. pub fn hello() -> String {
  2. "你好".to_string()
  3. }

src/chinese/farewells.rs代码修改如下:

  1. pub fn goodbye() -> String {
  2. "再见".to_string()
  3. }

运行cargo build将会执行成功。

前面,在src/main.rs引用下级模块的函数时,输入了phrases::english::greetings::hello()这个太长,并重复输入了4次,不简洁。Rust提供了另一个关键字use来定义当前可见泛围。

使用use引入Modules

Rust提供了use关键字能够将下级模块的可见泛围引入到当前,下面将src/main.rs里的代码修改如下:

  1. extern crate phrases;
  2. use phrases::english::greetings;
  3. use phrases::english::farewells;
  4. fn main() {
  5. println!("Hello in English: {}", greetings::hello());
  6. println!("Goodbye in English: {}", farewells::goodbye());
  7. }

带有use关键字的两行代码,将greetingsfarewells这两个模块的可见泛围提到了当前,所以,我们可以直接使用这个两个模块下面定义的函数。最佳实践就是这样,直接引入下级模块,而不是直接引入函数。

像下面这样直接引入函数:

  1. extern crate phrases;
  2. use phrases::english::greetings::hello;
  3. use phrases::english::farewells::goodbye;
  4. fn main() {
  5. println!("Hello in English: {}", hello());
  6. println!("Goodbye in English: {}", goodbye());
  7. }

这样做在上面这个很简单的项目中,Rust不会报错,一旦项目变大,将很容易存在函数命名冲突。比如像下面这样:

  1. extern crate phrases;
  2. use phrases::english::greetings::hello;
  3. use phrases::chinese::greetings::hello;
  4. fn main() {
  5. println!("Hello in English: {}", hello());
  6. println!("Goodbye in Chinese: {}", hello());
  7. }

则会报错如下:

  1. $ cargo build
  2. Compiling phrases v0.0.1 (file:///%E6%A1%8C%E9%9D%A2/phrases)
  3. /phrases/src/main.rs:5:5: 5:40 error: a value named `hello` has already been imported in this module
  4. /phrases/src/main.rs:5 use phrases::chinese::greetings::hello;
  5. ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  6. error: aborting due to previous error
  7. Could not compile `phrases`.
  8. To learn more, run the command again with --verbose.

如果我们将引入同一个模块的多个不同的下级模块,我们可以采用更加简便的代码输入方式:

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

使用pub use在内部Module引入其它模块

我们可以在已定义Crate内部的其它模块中,引入函数,这样可以在既定的内部代码组织结构中添加附加接口。

下面,接合前面的例子,修改src/main.rs如下:

  1. extern crate phrases;
  2. use phrases::english::{greetings, farewells};
  3. use phrases::chinese;
  4. fn main() {
  5. println!("Hello in English: {}", greetings::hello());
  6. println!("Goodbye in English: {}", farewells::goodbye());
  7. println!("Hello in Chinese: {}", chinese::hello());
  8. println!("Goodbye in Chinese: {}", chinese::goodbye());
  9. }

再修改src/chinese/mod.rs如下:

  1. pub use self::greetings::hello;
  2. pub use self::farewells::goodbye;
  3. mod greetings;
  4. mod farewells;

pub use声明,将chinese模块的下级模块的函数可见泛围引入到当前模块,并成为当前chinese模块的一个函数。

同时,要注意pub use或者use是在mod声明之前定义的。

编译、运行,其结果如下:

  1. $ cargo build
  2. Compiling phrases v0.0.1 (file:///%E6%A1%8C%E9%9D%A2/phrases)
  3. $ ./target/phrases
  4. Hello in English: hello!
  5. Goodbye in English: goodbye
  6. Hello in Chinese: 你好
  7. Goodbye in Chinese: 再见