Rust 从入门到精通11-包和模块管理项目

当我们项目较小时,一个 main.rs 文件就能搞定所有。但是如果项目较大,功能较多时,就很难搞定了。我们需要对相关功能进行分组和划分不同功能的代码,这样编写和维护都会比较方便。

如果你是一个Java程序员,相信你一定用过这样几个东西。首先项目较大,依赖较多,我们通常会使用maven/gradle 等工具进行依赖管理,然后将各个功能划分到不同的 package 中,比如(controller/service/dao/domain/utils等等)。

那么rust 程序员该如何管理大型项目呢?

①、Cargo: Rust 的包管理工具,能够管理外部依赖以及进行项目的编译和测试 create;

②、package: Cargo 提供的功能,一个包会含有一个 Cargo.toml 文件,是提供一系列功能的一个或多个 create。

③、create: 表示项目,是 Rust 中的独立编译单元。每个 create 对应生成一个库或可执行文件(.lib/.dll/.so/.exe)。

④、模块Modules)和 use: 允许你控制作用域和路径的私有性。

⑤、路径path):为 struct、function 或 module 等项命名的方式。

PS:其实这么多名词核心问题就是如何管理作用域,我们代码中的变量、方法,开发者如何调用?又或者能够调用哪些?编译器如何去找?

1、Cargo

Cargo 是Rust 的包管理工具,并不是一个编译器。

Rust 的编译器是 rustc

我们使用 Cargo 编译工程实际上还是调用 rustc 来完成的。如果我们想知道 cargo 在后面是如何调用 rustc 完
成编译的,可以使用 cargo build --verbose 选项查看详细的编译命令。

常用的 Cargo 命令:

一、cargo -h : 帮助命令

二、cargo new: 创建项目

三、cargo build: 编译项目

四、cargo run: 运行项目

五、cargo check: 只检查编译错误,而不做代码优化以及生成可执行程序,非常适合在开发过程中快速检查语法、类型错误。

六、cargo clean: 清理以前编译的结果。

七、cargo doc: 生成该项目的文档。

八、cargo test: 执行单元测试。

九、cargo bench: 执行 benchmark 性能测试。

十、cargo update: 升级所有依赖项的版本,重新生成 Cargo.lock 文件。

十一、cargo install: 安装可执行程序。这个命令非常有用,可以扩展 cargo 的子命令,为它增加新的功能。比如 可以使用 cargo install cargo-tree 命令,然后通过 cargo tree 打印依赖项的树形结构。

十二、cargo uninstall: 卸载可执行程序。

2、package 和 create

create 的作用:将相关功能组合到一个作用域内,便于在项目间进行共享,防止冲突。

①、crate 是一个二进制项(binary)或者库(library)。

②、crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的 crate 的根模块(module)。

③、包*(package) 是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。

包中可以包含至多一个库 crate(library crate)。包中可以包含任意多个二进制 crate(binary crate),但是必须至少包含一个 crate(无论是库的还是二进制的)。

下面我们通过 Cargo 创建一个包。

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

可以看到Cargo 会给我们的包创建一个 Cargo.toml 文件,查看内容如下:

[package]
name = "my-project"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

我们会发现并没有提到 src/main.rs,因为 Cargo 遵循的一个约定:src/main.rs 就是一个与包同名的二进制 crate 的 crate 根。

同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目。

在此,我们有了一个只包含 src/main.rs 的包,意味着它只含有一个名为 my-project 的二进制 crate。如果一个包同时含有 src/main.rssrc/lib.rs,则它有两个 crate:一个二进制的和一个库的,且名字都与包相同。

通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。

3、module

模块 的作用:

①、在一个 crate 中,将代码进行分组,以提高可读性与重用性。

②、控制项目的访问权限(private/public),默认是私有(private)。

3.1 创建 mod

// 前台
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() {}
    }
}

我们在 src 目录下创建了一个 lib.rs 文件,然后在里面添加上面的代码。

这里面我们定义一个模块,是以 mod 关键字为起始,然后指定模块的名字(这里叫做 front_of_house),并且用花括号包围模块的主体。在模块内,我们还可以定义其他的模块,就像本例中的 hostingserving 模块。模块还可以保存一些定义的其他项,比如结构体、枚举、常量、特性、或者函数。

通过使用模块,我们可以将相关的定义分组到一起,并指出他们为什么相关。程序员可以通过使用这段代码,更加容易地找到他们想要的定义,因为他们可以基于分组来对代码进行导航,而不需要阅读所有的定义。程序员向这段代码中添加一个新的功能时,他们也会知道代码应该放置在何处,可以保持程序的组织性。

这个模块的树形结构如下:

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

4、path

上一节我们定义了模块以及如何创建模块,那么如何访问模块中某一项呢?

这就需要使用路径(path),就像在文件系统使用路径一样。如果我们想要调用一个函数,我们需要知道它的路径。

4.1 绝对路径和相对路径

  • 绝对路径absolute path)从 crate 根开始,以 crate 名或者字面值 crate 开头。
  • 相对路径relative path)从当前模块开始,以 selfsuper 或当前模块的标识符开头。

绝对路径和相对路径后都跟一个或多个由双冒号(::)分割的标识符。

// 前台模块
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();
}

PS:建议使用绝对路径,这样代码调用位置移动也不用修改。

4.2 使用 pub 关键字控制访问权限

上面的代码,我们编译,会报如下错误:

Rust 从入门到精通11-包和模块管理项目_第1张图片

这是因为:

①、 Rust 中默认所有项(函数、方法、结构体、枚举、模块和常量)都是私有的。

②、父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。

为了让上面的代码编译通过,我们可以使用关键字 pub 将 front_of_house 模块下的 hosting 模块定义为公共的。

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

我们接着编译,发现还是报错:

Rust 从入门到精通11-包和模块管理项目_第2张图片

看报错是因为 add_to_waitlist() 方法是私有的,我们只是给其父模块增加了 pub 关键字,这说明:

通过关键字 pub 使其模块公有,但是其内容默认还是私有的。

我们需要将 add_to_waitlist() 方法也加上 pub 关键字,才会编译通过。

4.3 使用 super 关键字表示父模块路径

super 关键字表示父模块路径,类似文件系统中的 .. 开头的语法。

fn serve_order() {}

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

    fn cook_order() {}
}

4.4 使用 use 关键字将路径引入作用域

前面例子中无论我们选择 add_to_waitlist 函数的绝对路径还是相对路径,每次我们想要调用 add_to_waitlist 时,都必须指定front_of_househosting

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

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

那么有没有办法简化这个路径呢?

答案是:我们可以使用 use 关键字将路径一次性引入作用域,然后调用该路径中的项,就如同它们是本地项一样。

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();
}

另外,use 也可以引用相对路径,并且也会检查路径私有性。

4.5 使用 as 关键字提供新的名称

as 关键字可以为引入的路径指定本地别名。

比如使用 use 将两个同名类型引入同一作用域,调用的时候就必须带上父路径,如果不想带上,可以给这两个同名类型起一个别名。

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
    Ok(())
}

fn function2() -> IoResult<()> {
    // --snip--
    Ok(())
}

4.6 使用 pub use 重新导出名称

使用 use 关键字,将某个名称导入当前作用域后,这个名称在此作用域中就可以使用了,但它对此作用域之外还是私有的。

如果想让其他人调用我们的代码时,也能够正常使用这个名称,就好像它本来就在当前作用域一样,那我们可以将 pubuse 合起来使用。这种技术被称为 “重导出re-exporting)”:我们不仅将一个名称导入了当前作用域,还允许别人把它导入他们自己的作用域。

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();
}

5、引入外部依赖

在Java项目中,我们引入外部依赖通常是在 pom.xml 文件中引入外部依赖。

在 rust 中,我们是在 Cargo.toml 文件中引入,比如引入一个随机数依赖:

Rust 从入门到精通11-包和模块管理项目_第3张图片

rand = "0.8.3"

Cargo.toml 中加入 rand 依赖告诉了 Cargo 要从 crates.io 下载 rand 和其依赖,并使其可在项目代码中使用。

接着,为了将 rand 定义引入项目包的作用域,我们加入一行 use 起始的包名,它以 rand 包名开头并列出了需要引入作用域的项。

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

6、嵌套路径来消除大量的use 行

当需要引入很多定义于相同包或相同模块的项时,可以进行合并。

①、比如:

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

可以改写为:

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

②、层级共享

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

可以改写为:

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

7、通过 * 运算符将所有的公有定义引入作用域

如果希望将一个路径下 所有 公有项引入作用域,可以指定路径后跟 *,glob 运算符:

use std::collections::*;

这个 use 语句将 std::collections 中定义的所有公有项引入当前作用域。

使用 * 运算符时要注意:这会使得我们难以推导作用域中有什么名称和它们是在何处定义的。

glob 运算符经常用于测试模块 tests 中,这时会将所有内容引入作用域。

用于预导入(prelude)模块。

8、将模块拆分成多个文件

当模块变多时(多个方法、结构体等),我们需要将它们的定义移动到单独的文件中,从而使代码更容易阅读。

这里需要注意两个点:

①、模块定义时,如果模块名后面是“;”,而不是代码块,那么rust 会从与模块同名的文件中加载内容。

②、模块树的结构不会变化。

比如,我们在 lib.rs 创建如下内容:

// 前台模块
mod front_of_house {
    pub mod hosting {
       pub fn add_to_waitlist() {
       }
    }
}

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

现在,我们想把 font_of_house 模块移动出去,需要进行两步操作:

一、在 src 目录下创建 font_of_house.rs 文件,内容如下:

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

二、lib.rs 改写

// 前台模块
mod front_of_house;

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

同理,如果我们还想把 hosting 里面的模块也提取出去,那该怎么办呢?

假设,我们直接在 src 目录下创建 hosting.rs 文件,然后看看编译结果:

Rust 从入门到精通11-包和模块管理项目_第4张图片

这对应了我们前面说的模块树的结构是不变的,所以编译器是找 src/font_of_house 目录下的 hosting.rs 文件,我们不能将其放在 src 目录下。

你可能感兴趣的:(Rust,从入门到精通,rust,开发语言,后端)