【Rust学习之旅】模块管理:包、crate、模块、path(七)

784 阅读9分钟

前面我们学习了rust的基础类型以及结构体,枚举类型,以及函数,现在我们已经基本了解了rust的基础部分,有一定的代码组织能力,对于程序来说,随着项目越来越大,都需要模块化管理,提高代码可读性。

在javascript中,我们早期有多种模块化规范,当然现在都推崇ES Module。rust也有自己的模块管理方式。

重要概念

  • Packages):Cargo 的一个功能,它允许你构建、测试和分享 crate。
  • Crates :一个模块的树形结构,它形成了库或二进制项目。
  • 模块Modules)和 use:允许你控制作用域和路径的私有性。
  • 路径path):一个命名例如结构体、函数或模块等项的方式

包和 Crate

crate 是 Rust 在编译时最小的代码单位。crate 有两种形式:二进制项和库。

  • 二进制项 可以被编译为可执行程序,比如一个命令行程序或者一个服务器。

  • 它们也不会编译为可执行程序,它们提供一些诸如函数之类的东西,使其他项目也能使用这些东西。

多数时间 我们说的 crate 指的都是库。

这里有一个rust约定,二进制项的入口文件main.rs,的入口文件是lib.rs,这是不可以更改的。

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

  • 包只能包含一个库 crate(library crate)。
  • 包可以包含任意多个二进制 crate(binary crate),
  • 但是必须至少包含一个 crate(无论是库的还是二进制的)。
  • 一个包可以同时含有 src/main.rs 和 src/lib.rs,也就是同时有库create二进制create,且名字都与包相同

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

我们输入命令 cargo new:就是创建了一个package,前面我们提到了传递参数可以默认创建不同的create。

这里我们可以传递--bin或者--lib,第一个是创建二进制程序(默认),这是默认行为: 传递(--lib)创建一个库

作用域与私有性

我们来看一个文件目录结构:

backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs

这个例子中的 crate 根文件是src/main.rs,该文件包括了:

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("I'm growing {:?}!", plant);
}

在这里我们pub mod garden; 相当于向外暴露了模块 gargen,就像是typescript中export xxx from xxx

在模块中对相关代码进行分组

rust一个模块中的代码默认是私有的,我们定义一个模块,是以 mod 关键字为起始,可以嵌套。通过使用模块,我们可以将相关的定义分组到一起。自由组织结构,就像我们typescript中导出一个对象一样,我们导出不同的对象。

// src/lib.rs
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() {}
    }
}

引用模块项目的路径

Rust 如何在模块树中找到一个项的位置,在javascript中我们直接使用文件路径即可,rust也差不多但存在一点点区别。

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

路径有两种形式:

  • 绝对路径absolute path)是以 crate 根(root)开头的全路径;对于外部 crate 的代码,是以 crate 名开头的绝对路径,对于对于当前 crate 的代码,则以字面值 crate 开头。
  • 相对路径relative path)从当前模块开始,以 selfsuper 或当前模块的标识符开头。

rust使用 pub 关键字来标记,就像typescript 使用export 向外暴露一样。

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

我们这里使用了pub关键字,向外暴露的 eat_at_restaurant,编译器会抛出错误。

这里hosting 模块是私有的。换句话说,我们拥有 hosting 模块和 add_to_waitlist 函数的正确路径,但是 Rust 不让我们使用,因为它不能访问私有片段。

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

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

在rust中同级作用域可以直接使用,这里front_of_houseeat_at_restaurant我们就可以直接使用。

使用 pub 关键字暴露路径

刚才我们说了pub 关键字 向外暴露,模块之间也是一样。

pubpublic的缩写

mod front_of_house {
    pub 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();
}

不过到这里,rust编译器还是会抛出错误,因为add_to_waitlist仍然是私有的

模块公有并不使其内容也是公有的,所以我们需要在函数前也加上pub

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

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

使用 super 起始的相对路径

在javascript中我们使用相对路径时可以使用..来回到上级目录。rust这里使用super来处理。super 允许我们引用已知的父模块中的项。

fn deliver_order() {}

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

    fn cook_order() {}
}

创建公有的结构体和枚举

pub 来设计公有的结构体和枚举,不过关于在结构体和枚举上使用 pub 还有一些额外的细节需要注意。如果我们在一个结构体定义的前面使用了 pub ,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。

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

这里对外部来说toast是可见的,seasonal_fruit是不可见的。

与之相反,如果我们将枚举设为公有,则它的所有成员都将变为公有。我们只需要在 enum 关键字前面加上 pub,如果枚举成员不是公有的,那么枚举会显得用处不大。

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

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

上面我们使用相对路径与绝对路径来调用模块,这对于长路径模块来说,无疑是一种折磨,

所以rust提供了 use 关键字创建一个短路径,然后就可以在作用域中的任何地方使用这个更短的名字。

这里其实就相当于在这个作用域给模块取了一个别名, 通过 use 引入作用域的路径也会检查私有性。

在作用域中增加 use 和路径类似于在文件系统中创建软连接(符号连接,symbolic link)

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 只能创建 use 所在的特定作用域内的短路径。 eat_at_restaurant 函数移动到了一个叫 customer 的子模块,这又是一个不同于 use 语句的作用域,所以函数体不能编译。

这里我们可以使用super::hosting 来处理这个问题

习惯用法

  • rust 使用use关键字引入函数,一般值引入到父模块,方便看出函数的出处
  • 引用结构体或者枚举类型,就需要引入到本身

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

不能使用 use 语句将两个具有相同名称的项带入作用域,这样和javascript也是一样的,同名冲突。两种解决办法:

  • 我们指定到父级,做区分.如:use std::fmt::Result 和 use std::io::Result,我们可以引用到use std::fmt 和 use std::io就可以了

  • 我们使用 as 指定一个新的本地名称或者别名。这里和typescript也是一样的。只是写法上有点区别

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

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

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

使用 pub use 重导出名称

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

use 我们也可以加上pub关键字,向外导出,就像typescript中的export xxx from xxx 一样

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

使用外部包

就像我们前端的package.json 一样我们可以定义外部依赖项一样,rust在 Cargo.toml中定义

我们可以在[dependencies]下声明依赖。

[dependencies]
rand = "0.8.5"

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

将其引入项目中:

  • 在 Cargo.toml 列出它们并通过 
  • use 将其中定义的项引入项目包的作用域中。
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}");
}

就像我们前端可以将自己写好的包上传到npm 一样。 crates.io 上也有很多 Rust 社区成员发布的包,

注意 std 标准库对于你的包来说也是外部 crate。因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml 来引入 std,不过需要通过 use 将标准库中定义的项引入项目包的作用域中来引用它们,比如我们使用的 HashMap

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

当需要引入很多定义于相同包或相同模块的项时,为每一项单独列出一行会占用源码很大的空间。例如猜猜看章节示例 2-4 中有两行 use 语句都从 std 引入项到作用域:

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

我们可以使用嵌套路径将相同的项在一行中引入作用域。这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分

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

我们可以在路径的任何层级使用嵌套路径

可以在嵌套路径中使用 self

use std::io;
use std::io::Write;
// 等价于
use std::io::{self, Write};

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

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

use std::collections::*;

这个 use 语句将 std::collections 中定义的所有公有项引入当前作用域。使用 glob 运算符时请多加小心!Glob 会使得我们难以推导作用域中有什么名称和它们是在何处定义的。

结语

这一期我们学习rust的模块化,怎么暴露模块,我们引入别人的包,这些概念都是我们前端日常开发中用到的,不过我感觉ES Muudle设计的更方便😄,rust的use 语法和javascript 的 import 语法相作用相似。

本文首发于 @阿乐去买菜,转载请署明出处