Skip to content

包和模块

Rust 提供了模块系统,可以将代码按层次分成多个逻辑单元(模块),并管理这些模块之间的可见性(公有或私有)。

模块是项(item)的集合,项可以是:函数,结构体,trait,impl 块,甚至其它模块。

Crate 和 package

crate 是 Rust 最小的编译单元,它的形式包括二进制项和库。二进制项可以被编译为可执行程序,比如一个命令行程序或者一个服务器。它们必须有一个 main 函数来定义当程序被执行的时候所需要做的事情。库并没有 main 函数,它们也不会编译为可执行程序,它们提供一些诸如函数之类的东西,使其他项目也能使用这些东西。

packageCargo 就是一个包含构建Rust代码的二进制项的包。Cargo 也包含这些二进制项所依赖的库。其他项目也能用 Cargo 库来实现与 Cargo 命令行程序一样的逻辑。包中可以包含至多一个库 crate。包中可以包含任意多个二进制 crate,但是必须至少包含一个 crate(无论是库的还是二进制的)。

Package vs Crate:

1.crate 是 Rust 最小的编译单元;而Package 包含一系列功能的一个或者多个 crate。 2.crate 有两种形式:二进制项(binary)和库(library);Package 会包含一个 Cargo.toml 文件,阐述如何去构建这些 crate。 3.crate 面向开发者组织代码;Package 面向 Cargo 组织依赖。

伴随着项目的增长,应该通过将代码分解为多个模块和多个文件来组织代码,伴随着package(包)的增长,你可以将package(包)中的部分代码提取出来,做成独立的 crate,这些 crate 则作为外部依赖项。

通过Rust包管理工具Cargo可以创建二进制包:

$ cargo new hello_bin
    Created binary (application) `hello_bin` package

也可以创建库包:

$ cargo new hello_lib
    Created binary (application) `hello_lib` package

上面创建的 Package 中仅包含 src/main.rs 或者 src/lib.rs,意味着它仅包含一个二进制同名包 hello_bin 或者库同名包 hello_lib 。如果一个 Package 同时拥有 src/main.rssrc/lib.rs,那就意味着它包含两个包:库包和二进制包,这两个包名也都是 Package 同名。

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

rust
.
├── Cargo.toml      // 包的配置文件
├── src
│   ├── main.rs     // 包的主函数
│   ├── lib.rs      // 库包
│   └── bin         // 可以放置多个二进制包
│       └── main1.rs  
│       └── main2.rs
├── tests           // 集成测试
│   └── some_integration_tests.rs
├── benches         // 基准测试
│   └── simple_bench.rs
└── examples        // 代码示例
    └── simple_example.rs

模块及其作用域和可见性

模块

模块让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。因为一个模块中的代码默认是私有的,所以还可以利用模块控制项的私有性。私有项是不可为外部使用的内在详细实现。我们也可以将模块和它其中的项标记为公开的,这样,外部代码就可以使用并依赖与它们。

首先讲一下在 Rust 中创建模块的语法:

rust
mod foo;   // 定义名为foo的模块

mod foo {
    // foo模块的代码
    fn func_1() {}

    mod bar_1 {
        // 在模块foo中嵌套了一个模块bar_1
        fn func_2() {}
    }

    mod bar_2 {
        // 在模块foo中嵌套了一个模块bar_2
        fn func_3() {}
    }
}

注意:

  • 使用 mod 关键字来创建新模块,后面紧跟着模块名称;
  • 模块中可以定义各种 Rust 类型,例如函数、结构体、枚举、trait等;
  • foo是bar_1和bar_2的父模块,bar_1和bar_2是foo的子模块;

模块的作用域和可见性

想要调用一个函数,就需要知道它的路径,在 Rust 中,这种路径有两种形式:

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

绝对路径形式,在上面的模块中调用 func_2() 可以采用下面的方式:

rust
use crate::foo::bar_1::func_2();

相对路径的方式,在上面的模块bar_2中调用 func_2() 可以采用下面的方式:

rust
use bar_2::func_2();

如果采用路径的方式调用另外一个模块的函数,如下代码所示例子会报错:

rust
// 错误示例
mod foo {
        mod bar {
            fn func_1() {}
        }

        pub fn func_2() {}
    }


fn main() {
    
    fn func_3() {
        // 绝对路径
        crate::foo::bar::func_1();
        // 相对路径
        foo::bar::func_1();

        // 绝对路径
        crate::foo::func_2();
        // 相对路径
        foo::func_2();
    }
}

因为模块 bar 默认是私有的,模块外部无法进行访问调用。但是 foo 模块内部的函数可以调用,这是因为 foo 和 func_3() 同属于一个包根作用域内,同一个模块内的代码自然不存在私有化问题。

但是 bar 模块是私有的,外部模块和函数都不能进行调用,func_2 函数能进行调用是因为它声明了 pub,所以外部可见,可以调用。模块的可见性并不代表模块内部项的可见性,模块的可见性仅仅是允许其它模块去引用它,但是想要引用它内部的项,还得继续将对应的项标记为 pub。

Rust 在默认情况下,所有的类型都是私有化的,包括函数、方法、结构体、枚举、常量、模块。在 Rust 中,父模块完全无法访问子模块中的私有项,但是子模块却可以访问父模块及其之上的模块的私有项。

在相对路径的调用中,除了以上的方式,还可以采用 self 和 super 的方式进行。例如:

rust
mod foo {
    mod bar {
        pub fn func_1() {}
    }

    fn func_2() {
        self::bar::func_1();  // self 表示在自身的模块中寻找模块 bar 进行调用
    }

    mod xyz {
        fn func_3() {
            super::bar::func_1();  // super表示从上一级的父模块开始进行调用
        }
    }
}

参考链接:

https://doc.rust-lang.org/rust-by-example/mod/visibility.html

https://doc.rust-lang.org/stable/book/ch07-02-defining-modules-to-control-scope-and-privacy.html

引用模块项目的路径

如果代码中,每次调用都采用绝对路径或者相对路径的方式进行的话,那么看起来会很复杂和臃肿。因此我们需要一个办法来简化这种使用方式,在 Rust中,可以使用 use 关键字把路径提前引入到当前作用域中,随后的调用就可以省略该路径,极大地简化了代码。

使用use引入模块

在下面的例子中,将 bar 模块通过 use 的方式引入,这样后面进行调用的时候就不用把全部路径写出来:

rust
mod foo {
    pub mod bar {
        pub fn func_1() {}
    }
}

fn main() {
    use crate::foo::bar;    // 通过use引入模块
    
    fn func_3() {
        bar::func_1();      // 再通过 :: 的方式进行调用,当调用次数很多的时候相比于上面的方式减少了复杂性
    }
}

或者也可以直接引入模块中的某一个特定函数:

rust
mod foo {
    pub mod bar {
        pub fn func_1() {}
    }
}

fn main() {
    use crate::foo::bar::func_1;    // 引入模块中的特定函数
    
    fn func_3() {
        func_1();
    }
}

如果在不同的模块中存在同名的项(比如同名函数)可以采用不同的父模块引入来进行调用,例如:

rust
mod foo {
    pub fn example() {}
}

mod bar {
    pub fn example() {}
}

fn main() {
    use crate::foo;
    use crate::bar;
    
    foo::example();
    bar::example();
}

或者可以采用 as关键字对同名的函数进行别名处理,例如:

rust
mod foo {
    pub fn example() {}
}

mod bar {
    pub fn example() {}
}

fn main() {
    use crate::foo::example as ex_1;
    use crate::bar::example as ex_2;
    
    ex_1();
    ex_2();
}

使用标准库和第三方库

Rust 标准库提供大量的模块和方法可以使用,利用use将模块或者函数引入,例如:

rust
#![allow(unused)]
use std::io;                // 引入标准库中的io模块
use std::io::prelude::*;    // *的意思是将prelude下所有的项全部引入
use std::fs::File;          // 引入标准库中的fs::File模块

fn main() -> io::Result<()> {
    let mut f = File::open("foo.txt")?;
    let mut buffer = [0; 10];

    // read up to 10 bytes
    let n = f.read(&mut buffer)?;

    println!("The bytes: {:?}", &buffer[..n]);
    Ok(())
}

Rust 生态中提供了很多高质量的第三方包,你可以通过在crates.io上搜索,获得自己想要的第三方库。 三方库的使用需要在 Cargo.toml 中[dependencies]添加依赖项,例如添加:

rand = "0.8.3"

在使用时,使用use引入即可:

rust
use rand::Rng;  // 引入rand包的Rng模块

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

如果要引入的模块的项较多,可以使用 {} 将多个引入项包括在里边,这样可以简化引入代码。例如:

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

use std::cmp::Ordering;
use std::time;

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

可以转换为:

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

use std::{cmp::Ordering, time};

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

参考链接:https://doc.rust-lang.org/stable/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html