菜鸡前端的Rust学习笔记(七)—模块化

2,981 阅读11分钟

7. 在大型项目中使用包、箱和模块来管理项目

写在前面

目前我们所有编写的代码都是在一个文件中的一个模块,随着项目增加,我们需要将代码切分成多个模块和多个文件。一个包(package) 可以包含多个二进制的箱(Crates) ,随着包的增大也可以拆成多个独立的箱(crates)以及可选的一个箱(Crates)。本章主要就是讲述了关于包管理相关的内容,对于非常大的项目来说,一些相关的包可以需要共同升级,cargo提供了workspace的概念会在后续讲解。

除了函数集之外,封装一些函数的实践,让你的代码能够更容易复用,一旦你实现了一个操作其他代码可以通过公开的接口直接使用,不需要关心内部实现(需要让外部调用的使用puiblic,其他部分私有化private)。

相关的观点还有作用域:无论是编译器还是coder,在创建作用域的时候,我们需要知道我们的变量、函数、结构体、枚举等在作用域内外哪里生效,是否会有命名冲突的情况。

rust管理模块系统主要包括:

  1. Packages(包): cargo的模块特性,让你能构建、测试和分享箱(crates)
  2. Crates(箱):一个库或者可执行文件的模块树
  3. Modules(模块):让你控制组织结构、作用域和私有路径(其实我理解这里就是代码了)
  4. Paths(路径):一种命名项目的方式,例如结构体、函数或者模块

本章主要讲解的就是模块的交互,怎么样在作用域中使用,最后将讲解一下模块系统如何使用。

7.1 包和箱

一个箱(crate)是一个二进制文件或者一个库。crate root是rust编译器开始运行和打包你的模块的源文件。

一个包(package)是提供一个或多个箱,提供了一系列的功能。(一个package包含了一个cargo.toml文件,用于描述如何构建这些箱)。

创建一个新的package工程

cargo new lesson_8_package

04-package.png

我们创建的文件已经包括了cargo.toml文件,其用来表示当前的这个包,接下来我们看下cargo.toml这个文件的内容,其对应的包名和工程名是一样,然后main.rs被认为是一个二进制箱的root文件,然后,src/lib.rs文件被认为是箱的库文件。之前我们写的代码都是只有一个main.rs如果后续我们的代码又有marin.rs又有lib.rs那么这个时候我们就有两个箱了,一个用于构建二进制文件,一个用于处理库文件。

05-toml.png

如何一个package中有多个二进制文件:创建一个src/bin文件夹,其下的每一个文件都会被分割成一个二进制箱文件

7.2 定义模块控制作用域和私有化

几个关键字

paths:允许你去命名每一个想要分享的功能

use:引入对应路径的功能到作用域中

pub:使功能为公用功能

as:给方法取别名(我猜和import as一样)

为什么要模块化?

模块化通过箱来组织代码具有更好的可读性和复用性。模块也用来控制项目私有化,可以定义什么是可以暴露给外部的(public)和只有内部能使用的方法(private)

7.2.1 模块的定义

注意点

  1. 使用mod关键字来代表这个文件是一个modules
  2. 可以使用cargo new --lib xxxx来创建lib
  3. lib文件其实就是一个lib.rs会被编译器认为是一个模块
  4. 模块是可以嵌套的,其是一个树结构,如果一个A模块内部包含了一个B模块,那么我们说B是A的子模块
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() {}
    }
}

7.2.2 箱的树结构

对应的该箱的树结构如下所示,hostingservingfront_of_house的子模块

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

7.3 在模块树对每一项的引用

7.3.1 绝对路径和相对路径的使用

调用路径上对应的方法有两种:

  1. 绝对路径方法:在箱的根文件中,使用crate关键字和箱的名称来调用对应的方法
  2. 相对路径方法:在当前模块中使用selfsuper关键字来使用相关的方法

注意

  1. 在调用绝对或者相对路径的时候,一般使用多了个::来对相应的识别符做区分

  2. 如何区别如何使用绝对路径还是相对路径?

    取决于你封装代码的形式,比如下面的这个eat_at_restaurant这个方法,如果你是和上面的front_of_house放在一个模块中的那么绝对路径需要修改,相对路径不用修改,但是如果你要拆成两个模块,那么绝对路径不用修改,而相对路径需要修改。

  3. 官方建议多使用绝对路径,因为代码定义和每一项都更倾向于独立定义。

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

这个时候如果我们编译上述代码,会报错,这是因为在Rust中默认认为模块是私有的,我们不能接入私有的模块,在rust中所有在私有边界中运行的项目(函数,方法,结构体,枚举,模块和常量)都是默认为私有化的

error[E0603]: module `hosting` is private
  --> src\lib.rs:27:28
   |
27 |     crate::front_of_house::hosting::add_to_waitlist();
   |                            ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src\lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^
   
For more information about this error, try `rustc --explain E0603`.
error: could not compile `lesson_8_package` due to previous error

7.3.2 使用pub 关键字来暴露相关方法

我们可以通过pub关键字,将模块中的部分子模块以及方法向外进行暴露,所以修改后的代码如下图所示

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {
            println!("add to wait list");

            self::seat_at_table();
        }
        fn seat_at_table() {
            println!("seat at table");
        }
    }

    mod serving {
        fn take_order() {
            println!("take order");
        }
        fn serve_order() {
            println!("serve order");
        }
        fn take_payment() {
            println!("take payment");
        }
    }
}

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

注意点:因为是默认是私有的,所以这里我们需要将hostingadd_to_waitlist都增加pub关键字,不然编译还是会报错

目前上述代码已经能够正常编译了~

7.3.3 使用super关键字调用相对路径方法

如果一个子模块想调用兄弟模块或者父模块的一些项目怎么办呢?可以使用super关键字,其作用类似shell中的..到上一个作用域的上下文中,这样就可以调用相关的项目。

以上述7.3.2中,如果子模块serving想要调用hosting中的方法,现在就需要使用super关键字。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {
            println!("add to wait list");
            self::seat_at_table();
        }
        fn seat_at_table() {
            println!("seat at table");
        }
    }
​
    mod serving {
        fn take_order() {
            println!("take order");
            // 用super到父级
            // 然后调用父级hosting中的add_to_waitlist
            super::hosting::add_to_waitlist();
        }
        fn serve_order() {
            println!("serve order");
        }
        fn take_payment() {
            println!("take payment");
        }
    }
}

7.3.4 将struct和enum设置为公开的

struct模块的注意点

  1. struct在设置为pub的时候,相应的需要对外暴露的属性也需要用pub关键字声明
  2. 在实现对应的方法的时候,如果该方法需要向外暴露也需要增加pub
mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        // 这里使用的时候seasonal_fruit是不可以使用的,因为是私有变量
        seasonal_fruit: String,
    }
    
    impl Breakfast {
            pub fn summer(toast: &str) -> Breakfast {
                Breakfast {
                    toast: String::from(toast),
                    // 这个seasonal_fruit是私有变量
                    // 这里需要注意的是,如果不是因为我们有这个summer方法
                    // 我们可能一辈子都没办法创建Breakfast
                    // 因为seasonal_fruit是不可访问的
                    seasonal_fruit: String::from("Peach"),
                }
            }

            pub fn tell_fruit_food(&self) {
                // 实例方法通过self可以调到内部的私有变量
                println!("fruit is -> {}", self.seasonal_fruit);
            }
       }
    }
}

pub fn eat_at_restaurant() {
    crate::front_of_house::hosting::add_to_waitlist();
    front_of_house::hosting::add_to_waitlist();

    let mut meal = back_of_house::Breakfast::summer("Rye");

    // 这里meal.seasonal_fruit是无法被访问的,因为是私有变量
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast, please", meal.toast);
}

现在我们已经会定义对应的模块了,接下来我们将使用use关键字,使用关键字来使用对应的模块

7.4 使用use关键字将包引入到作用域中

7.4.1 使用use关键字将暴露模块引入作用域

我们可以使用use关键字,创建路径和作用域之间的连接。之前的代码中的Breakfast结构体和hosting子模块,都需要通过绝对路径一致引用父模块,这样写起来十分浪费时间,代码也不太清楚(比较冗余),可以通过use关键字来简化:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {
            println!("add to wait list");
            self::seat_at_table();
        }
        fn seat_at_table() {
            println!("seat at table");
        }
    }

    mod serving {
        fn take_order() {
            println!("take order");
            super::hosting::add_to_waitlist();
        }
        fn serve_order() {
            println!("serve order");
        }
        fn take_payment() {
            println!("take payment");
        }
    }
}

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    pub enum Appetizer {
        Soup,
        Salad
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                // 这个seasonal_fruit是私有变量
                seasonal_fruit: String::from("Peach"),
            }
        }

        pub fn tell_fruit_food(&self) {
            println!("fruit is -> {}", self.seasonal_fruit);
        }
    }
}

// 将host注入到这个作用域中
// 将Breakfast注入到这个作用域中
// 将Appetizer注入到本作用域中
use crate::front_of_house::hosting;
use crate::back_of_house::Breakfast;
use crate::back_of_house::Appetizer;

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

    let mut meal = Breakfast::summer("Rye");

    meal.toast = String::from("Wheat");

    let mut appetizer = Appetizer::Soup;

    println!("I'd like {} toast, please", meal.toast);
}

7.4.2 编写符合习惯的use路径

为什么要规范的写引入路径?

  1. 当我们使用use写后面的路径的时候,尽量写到最近模块(离当前方法来说最近的那个作用域)的地址,不建议将整个路径写完整,不利于后期加使用的放,并且主子域的划分也不太 清晰。
  2. 如果两个元素有相同的需要引出的东西,这个时候如果我们引入路径仅仅写到那个struct或者其他项,可能会有重名冲突。

7.4.3 用as关键字为模块提供一个新的名字

7.4.2中说如果存在导出的两个模块重名怎么办,可以使用as关键字,将导出的模块内容进行“改名”

// 后续使用Result的地方改成FmtResult即可
use std::fmt::Result as FmtResult;

fn main() {
    println!("Hello, world!");
}

7.4.4 使用pub use导出命名

当使用use引入模块的时候,可能这个name在一个新的作用域中是私有的,因此,我们可以使用pub use让我们引用的模块在代码作用域中式公开可用的。当使用了pub use之后外部代码可以使用相应的方法,如果没有使用pub use只有在他自己的作用域中能够使用相关use的方法

7.4.5 使用外部包

使用外部包的步骤:

  1. cargo.toml文件中增加需要引入依赖的包名和版本

    [package]
    name = "lesson_8_package"
    version = "0.1.0"
    edition = "2021"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [dependencies]
    rand="0.8.3"
    
  2. 在对应的作用域中,通过use关键词将对应的模块进行导入

    use rand::Rng;
    
    fn main() {
        let rand_number = rand::thread_rng().gen_range(1..10);
    }
    

7.4.6 使用集成路径来清除大的use列表

如果我们需要引入一个包中的多个项目,这个时候我们通过单一的use去写的话,如果引入100个项目,可能就要写垂直的100行,但是我们其实可以使用聚集的方式来写,具体的我们可以参考以下的例子:

// 聚合之前的写法
mod lib;

use lib;
use lib::front_of_house;
use lib::eat_at_restaurant;

// 聚合之后的写法

// 这里的self代表lib本身,后面类似import时候的解构的写法
use lib::{ self as my_lib, eat_at_restaurant, front_of_house };

7.4.7 全局操作符

如果我们想引入所有公开的项目在当前作用域,我们可以使用*使用这个全局操作符的例子如下:

mod lib;
use lib::*;
​
fn main() {
    println!("Hello, world!");
    eat_at_restaurant();
    let mut appetizer = back_of_house::Breakfast::summer("Brand");
    front_of_house::hosting::add_to_waitlist();
}
​

这里我们需要注入,如果多个模块都是全部导入会不会有重名的冲突情况(慎用)

7.5 分离模块在多个其他文件中

目前所有的模块都是写在一个文件里面的,但是将一些通用的函数方法拆出来其最终目的是为了在别的文件中复用,如何在别的文件中复用呢?

  1. 使用mod关键字申明对应的文件(文件名是什么声明的就是什么)
  2. 使用use关键字,将模块中的部分项目引入到相应作用域中
// main.rs
// 之前写函数的文件名为lib.rs 所以这里是mod lib
mod lib;
​
use lib::front_of_house;
use lib::eat_at_restaurant;
​
fn main() {
    println!("Hello, world!");
    eat_at_restaurant();
    front_of_house::hosting::add_to_waitlist();
}

总结

rust让我们去切分自己的文件通过多个箱,然后一个箱中可以切分多个模块,你可以在一个项目中引用多个模块中的项目。 你可以通过绝对路径或者相对路径的方式引用相应的包,通过use作用域来讲对应的模块引入到作用域中。模块的代码默认是私有化的,你可以通过pub关键字来使其变成公有化。