Skip to content

包和模块

此笔记记录于Rust Course,大多数为其中的摘要,少数为笔者自己的理解

总览

  • 项目(Package):可以用来构建、测试和分享包
  • 工作空间(WorkSpace):对于大型项目,可以进一步将多个包联合在一起,组织成工作空间
  • 包(Crate):一个由多个模块组成的树形结构,可以作为三方库进行分发,也可以生成可执行文件进行运行
  • 模块(Module):可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元

包Crate

对于 Rust 而言,包是一个独立的可编译单元,它编译后会生成一个可执行文件或者一个库。

一个包会将相关联的功能打包在一起,使得该功能可以很方便的在多个项目中分享。例如标准库中没有提供但是在三方库中提供的 rand 包,它提供了随机数生成的功能,我们只需要将该包通过 use rand; 引入到当前项目的作用域中,就可以在项目中使用 rand 的功能:rand::XXX

同一个包中不能有同名的类型,但是在不同包中就可以。例如,虽然 rand 包中,有一个 Rng 特征,可是我们依然可以在自己的项目中定义一个 Rng,前者通过 rand::Rng 访问,后者通过 Rng 访问,对于编译器而言,这两者的边界非常清晰,不会存在引用歧义。

项目Package

由于 Package 就是一个项目,因此它包含有独立的 Cargo.toml 文件,以及因为功能性被组织在一起的一个或多个包。一个 Package 只能包含一个库(library)类型的包,但是可以包含多个二进制可执行类型的包。

二进制Package

创建一个二进制 Package

$ cargo new my-project
     Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

Cargo 有一个惯例:src/main.rs 是二进制包的根文件,该二进制包的包名跟所属 Package 相同,在这里都是 my-project,所有的代码执行都从该文件中的 fn main() 函数开始。

库Package

再来创建一个库类型的 Package

$ cargo new my-lib --lib
     Created library `my-lib` package
$ ls my-lib
Cargo.toml
src
$ ls my-lib/src
lib.rs

库类型的 Package 只能作为三方库被其它项目引用,而不能独立运行,只有之前的二进制 Package 才可以运行。

与 src/main.rs 一样,Cargo 知道,如果一个 Package 包含有 src/lib.rs,意味它包含有一个库类型的同名包 my-lib,该包的根文件是 src/lib.rs

易混淆的Package和包

看完上面,相信大家看出来为何 Package 和包容易被混淆了吧?因为你用 cargo new 创建的 Package 和它其中包含的包是同名的!

不过,只要你牢记 Package 是一个项目工程,而包只是一个编译单元,基本上也就不会混淆这个两个概念了:src/main.rs 和 src/lib.rs 都是编译单元,因此它们都是包。

典型的Package结构

上面创建的 Package 中仅包含 src/main.rs 文件,意味着它仅包含一个二进制同名包 my-project。如果一个 Package 同时拥有 src/main.rs 和 src/lib.rs,那就意味着它包含两个包:库包和二进制包,这两个包名也都是 my-project —— 都与 Package 同名。

一个真实项目中典型的 Package,会包含多个二进制包,这些包文件被放在 src/bin 目录下,每一个文件都是独立的二进制包,同时也会包含一个库包,该包只能存在一个 src/lib.rs

.
├── Cargo.toml
├── Cargo.lock
├── src
│   ├── main.rs
│   ├── lib.rs
│   └── bin
│       └── main1.rs
│       └── main2.rs
├── tests
│   └── some_integration_tests.rs
├── benches
│   └── simple_bench.rs
└── examples
    └── simple_example.rs
  • 唯一库包:src/lib.rs
  • 默认二进制包:src/main.rs,编译后生成的可执行文件与 Package 同名
  • 其余二进制包:src/bin/main1.rs 和 src/bin/main2.rs,它们会分别生成一个文件同名的二进制可执行文件
  • 集成测试文件:tests 目录下
  • 基准性能测试 benchmark 文件:benches 目录下
  • 项目示例:examples 目录下

模块Module

创建嵌套模块

使用 cargo new --lib restaurant 创建一个小餐馆,注意,这里创建的是一个库类型的 Package,然后将以下代码放入 src/lib.rs 中:

rust
// 餐厅前厅,用于吃饭
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}
  • 使用 mod 关键字来创建新模块,后面紧跟着模块名称
  • 模块可以嵌套,这里嵌套的原因是招待客人和服务都发生在前厅,因此我们的代码模拟了真实场景
  • 模块中可以定义各种 Rust 类型,例如函数、结构体、枚举、特征等
  • 所有模块均定义在同一个文件中

模块树

上一节中,我们提到过 src/main.rs 和 src/lib.rs 被称为crate root,这个奇葩名称的来源(我不想承认是自己翻译水平太烂-,-)是由于这两个文件的内容形成了一个模块 crate,该模块位于包的树形结构(由模块组成的树形结构)的根部:

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

这颗树展示了模块之间彼此的嵌套关系,因此被称为模块树。其中 crate 包根是 src/lib.rs 文件,包根文件中的三个模块分别形成了模块树的剩余部分。

父子模块

如果模块 A 包含模块 B,那么 A 是 B 的父模块,B 是 A 的子模块。在上例中,front_of_house 是 hosting 和 serving 的父模块,反之,后两者是前者的子模块。

用路径引用模块

在 Rust 中,这种路径有两种形式:

  • 绝对路径,从包根开始,路径名以包名或者 crate 作为开头
  • 相对路径,从当前模块开始,以 selfsuper 或当前模块的标识符作为开头

让我们继续经营那个惨淡的小餐馆,这次为它实现一个小功能: 文件名:src/lib.rs

rust
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

绝对路径引用

因为 eat_at_restaurant 和 add_to_waitlist 都定义在一个包中,因此在绝对路径引用时,可以直接以 crate 开头,然后逐层引用,每一层之间使用 :: 分隔:

rust
crate::front_of_house::hosting::add_to_waitlist();

模块树结构如下:

crate
 └── eat_at_restaurant
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

可以看出,绝对路径的调用,完全符合了模块树的层级递进,非常符合直觉,如果类比文件系统,就跟使用绝对路径调用可执行程序差不多:/front_of_house/hosting/add_to_waitlist,使用 crate 作为开始就和使用 / 作为开始一样。

相对路径引用

再回到模块树中,因为 eat_at_restaurant 和 front_of_house 都处于包根 crate 中,因此相对路径可以使用 front_of_house 作为开头:

rust
front_of_house::hosting::add_to_waitlist();

如果类比文件系统,那么它类似于调用同一个目录下的程序,你可以这么做:front_of_house/hosting/add_to_waitlist

绝对还是相对

在实际使用时,需要遵循一个原则:当代码被挪动位置时,尽量减少引用路径的修改,相信大家都遇到过,修改了某处代码,导致所有路径都要挨个替换,这显然不是好的路径选择。

不过,如果不确定哪个好,你可以考虑优先使用绝对路径,因为调用的地方和定义的地方往往是分离的,而定义的地方较少会变动。

代码可见性

rust
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

上述代码会报错。

错误信息很清晰:hosting 模块是私有的,无法在包根进行访问,那么为何 front_of_house 模块就可以访问?因为它和 eat_at_restaurant 同属于一个包根作用域内,同一个模块内的代码自然不存在私有化问题。

模块不仅仅对于组织代码很有用,它还能定义代码的私有化边界:在这个边界内,什么内容能让外界看到,什么内容不能,都有很明确的定义。因此,如果希望让函数或者结构体等类型变成私有化的,可以使用模块。

Rust 出于安全的考虑,默认情况下,所有的类型都是私有化的,包括函数、方法、结构体、枚举、常量,是的,就连模块本身也是私有化的。在中国,父亲往往不希望孩子拥有小秘密,但是在 Rust 中,父模块完全无法访问子模块中的私有项,但是子模块却可以访问父模块、父父..模块的私有项

pub关键字

只需要将 hosting 模块以及add_to_wailist()标记为对外可见即可:

rust
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

使用super引用模块

super 代表的是父模块为开始的引用方式,非常类似于文件系统中的 .. 语法:../a/b 文件名:src/lib.rs

rust
fn serve_order() {}

// 厨房模块
mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }

    fn cook_order() {}
}

使用self引用模块

rust
fn serve_order() {
    self::back_of_house::cook_order()
}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        crate::serve_order();
    }

    pub fn cook_order() {}
}

结构体和枚举的可见性

为何要把结构体和枚举的可见性单独拎出来讲呢?因为这两个家伙的成员字段拥有完全不同的可见性:

  • 将结构体设置为 pub,但它的所有字段依然是私有的
  • 将枚举设置为 pub,它的所有字段也将对外可见

原因在于,枚举和结构体的使用方式不一样。如果枚举的成员对外不可见,那该枚举将一点用都没有,因此枚举成员的可见性自动跟枚举可见性保持一致,这样可以简化用户的使用。

而结构体的应用场景比较复杂,其中的字段也往往部分在 A 处被使用,部分在 B 处被使用,因此无法确定成员的可见性,那索性就设置为全部不可见,将选择权交给程序员。

模块与文件分离

当模块变多或者变大时,需要将模块放入一个单独的文件中,让代码更好维护。

rust
// 放入一个单独的文件中 `src/front_of_house.rs`
pub mod hosting {
    pub fn add_to_waitlist() {}
}
rust
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
  • mod front_of_house; 告诉 Rust 从另一个和模块 front_of_house 同名的文件中加载该模块的内容
  • 使用绝对路径的方式来引用 hosting 模块:crate::front_of_house::hosting;
  • 在这里出现了一个新的关键字 use,联想到其它章节我们见过的标准库引入 use std::fmt;,可以大致猜测,该关键字用来将外部模块中的项引入到当前作用域中来,这样无需冗长的父模块前缀即可调用:hosting::add_to_waitlist();

使用use引入模块及受限可见性

在 Rust 中,可以使用 use 关键字把路径提前引入到当前作用域中,随后的调用就可以省略该路径,极大地简化了代码。

基本引入方式

1)绝对路径引入模块

rust
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

我们使用 use 和绝对路径的方式,将 hosting 模块引入到当前作用域中,然后只需通过 hosting::add_to_waitlist 的方式,即可调用目标模块中的函数,相比 crate::front_of_house::hosting::add_to_waitlist() 的方式要简单的多

2)相对路径引入模块中的函数

rust
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
    add_to_waitlist();
    add_to_waitlist();
}

引入模块还是函数?

从使用简洁性来说,引入函数自然是更甚一筹,但是在某些时候,引入模块会更好:

  • 需要引入同一个模块的多个函数
  • 作用域中存在同名函数

避免同名引用

1)模块::函数

rust
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
}

fn function2() -> io::Result<()> {
    // --snip--
}

2)as别名引用

rust
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
}

fn function2() -> io::Result<()> {
    // --snip--
}

引入项再导出

当外部的模块项 A 被引入到当前模块中时,它的可见性自动被设置为私有的,如果你希望允许其它外部代码引用我们的模块项 A,那么可以对它进行再导出:

rust
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

使用 pub use 即可实现。这里 use 代表引入 hosting 模块到当前作用域,pub 表示将该引入的内容再度设置为可见。

当你希望将内部的实现细节隐藏起来或者按照某个目的组织代码时,可以使用 pub use 再导出

使用第三方包

关于如何引入外部依赖,我们在 Cargo 入门中就有讲,这里直接给出操作步骤:

  1. 修改 Cargo.toml 文件,在 [dependencies] 区域添加一行:rand = "0.8.3"
  2. 此时,如果你用的是 VSCode 和 rust-analyzer 插件,该插件会自动拉取该库,你可能需要等它完成后,再进行下一步(VSCode 左下角有提示)

好了,此时,rand 包已经被我们添加到依赖中,下一步就是在代码中使用:

rust
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1..101);
}

crates.io,lib.rs:

Rust 社区已经为我们贡献了大量高质量的第三方包,你可以在 crates.io 或者 lib.rs 中检索和使用,从目前来说查找包更推荐 lib.rs,搜索功能更强大,内容展示也更加合理,但是下载依赖包还是得用crates.io

你可以在网站上搜索 rand 包,看看它的文档使用方式是否和我们之前引入方式相一致:在网上找到想要的包,然后将你想要的包和版本信息写入到 Cargo.toml 中。

使用{}简化引入方式

rust
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::collections::HashSet;

use std::cmp::Ordering;
use std::io;

简化为:

rust
use std::collections::{HashMap,BTreeMap,HashSet};
use std::{cmp::Ordering, io};

对于下面的同时引入模块和模块中的项:

rust
use std::io;
use std::io::Write;

可以使用 {} 的方式进行简化:

rust
use std::io::{self, Write};

使用*引入模块下的所有项

对于之前一行一行引入 std::collections 的方式,我们还可以使用:

rust
use std::collections::*;

受限的可见性

这也是模块体系中最为核心的概念,控制了模块中哪些内容可以被外部看见,但是在实际使用时,光被外面看到还不行,我们还想控制哪些人能看,这就是 Rust 提供的受限可见性。

pub(crate) 或 pub(in crate::a) 就是限制可见性语法,前者是限制在整个包内可见,后者是通过绝对路径,限制在包内的某个模块内可见,总结一下:

  • pub 意味着可见性无任何限制
  • pub(crate) 表示在当前包可见
  • pub(self) 在当前模块可见
  • pub(super) 在父模块可见
  • pub(in <path>) 表示在某个路径代表的模块中可见,其中 path 必须是父模块或者祖先模块
rust
// 一个名为 `my_mod` 的模块
mod my_mod {
    // 模块中的项默认具有私有的可见性
    fn private_function() {
        println!("called `my_mod::private_function()`");
    }

    // 使用 `pub` 修饰语来改变默认可见性。
    pub fn function() {
        println!("called `my_mod::function()`");
    }

    // 在同一模块中,项可以访问其它项,即使它是私有的。
    pub fn indirect_access() {
        print!("called `my_mod::indirect_access()`, that\n> ");
        private_function();
    }

    // 模块也可以嵌套
    pub mod nested {
        pub fn function() {
            println!("called `my_mod::nested::function()`");
        }

        #[allow(dead_code)]
        fn private_function() {
            println!("called `my_mod::nested::private_function()`");
        }

        // 使用 `pub(in path)` 语法定义的函数只在给定的路径中可见。
        // `path` 必须是父模块(parent module)或祖先模块(ancestor module)
        pub(in crate::my_mod) fn public_function_in_my_mod() {
            print!("called `my_mod::nested::public_function_in_my_mod()`, that\n > ");
            public_function_in_nested()
        }

        // 使用 `pub(self)` 语法定义的函数则只在当前模块中可见。
        pub(self) fn public_function_in_nested() {
            println!("called `my_mod::nested::public_function_in_nested");
        }

        // 使用 `pub(super)` 语法定义的函数只在父模块中可见。
        pub(super) fn public_function_in_super_mod() {
            println!("called my_mod::nested::public_function_in_super_mod");
        }
    }

    pub fn call_public_function_in_my_mod() {
        print!("called `my_mod::call_public_funcion_in_my_mod()`, that\n> ");
        nested::public_function_in_my_mod();
        print!("> ");
        nested::public_function_in_super_mod();
    }

    // `pub(crate)` 使得函数只在当前包中可见
    pub(crate) fn public_function_in_crate() {
        println!("called `my_mod::public_function_in_crate()");
    }

    // 嵌套模块的可见性遵循相同的规则
    mod private_nested {
        #[allow(dead_code)]
        pub fn function() {
            println!("called `my_mod::private_nested::function()`");
        }
    }
}

fn function() {
    println!("called `function()`");
}

fn main() {
    // 模块机制消除了相同名字的项之间的歧义。
    function();
    my_mod::function();

    // 公有项,包括嵌套模块内的,都可以在父模块外部访问。
    my_mod::indirect_access();
    my_mod::nested::function();
    my_mod::call_public_function_in_my_mod();

    // pub(crate) 项可以在同一个 crate 中的任何地方访问
    my_mod::public_function_in_crate();

    // pub(in path) 项只能在指定的模块中访问
    // 报错!函数 `public_function_in_my_mod` 是私有的
    //my_mod::nested::public_function_in_my_mod();
    // 试一试 ^ 取消该行的注释

    // 模块的私有项不能直接访问,即便它是嵌套在公有模块内部的

    // 报错!`private_function` 是私有的
    //my_mod::private_function();
    // 试一试 ^ 取消此行注释

    // 报错!`private_function` 是私有的
    //my_mod::nested::private_function();
    // 试一试 ^ 取消此行的注释

    // 报错! `private_nested` 是私有的
    //my_mod::private_nested::function();
    // 试一试 ^ 取消此行的注释
}