• 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以看到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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们会发现并没有提到 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() {}
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    我们在 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

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

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

    这是因为:

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

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

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

    mod front_of_house {
        pub mod hosting {
            fn add_to_waitlist() {}
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    看报错是因为 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() {}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    答案是:我们可以使用 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();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    另外,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(())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    5、引入外部依赖

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

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

    rand = "0.8.3"
    
    • 1

    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}");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

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

    ①、比如:

    use std::cmp::Ordering;
    use std::io;
    
    • 1
    • 2

    可以改写为:

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

    ②、层级共享

    use std::io;
    use std::io::Write;
    
    • 1
    • 2

    可以改写为:

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

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

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

    use std::collections::*;
    
    • 1

    这个 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();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

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

    pub mod hosting {
        pub fn add_to_waitlist() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4

    二、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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

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

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

  • 相关阅读:
    前端常用的CDN
    数据处理的那些事「GitHub 热点速览」
    GB28181学习(十六)——基于jrtplib实现tcp被动和主动收流
    C专家编程 第2章 这不是Bug,而是语言特性 2.2 多做之过
    【atoi函数详解】
    AWS DAS认证考点整理(Redshift篇)
    Zabbix监控
    [附源码]SSM计算机毕业设计学生量化考核管理系统JAVA
    git中submodule子模块的添加、使用和删除
    【PyCharm】设置(风格 | 字体 | 模板)
  • 原文地址:https://blog.csdn.net/ysvae/article/details/126642231