当前位置: 首页 > 工具软件 > Crate > 使用案例 >

rust的package,crate,module

章鸿光
2023-12-01

package,crate,module

rust提供了非常优秀的包管理器cargo,我们可以使用crate,module,package来组织代码。

crate

crate是rust在编译时最小的单元。crate可以包含模块,模块可以定义在其他文件,然后和crate一起编译。

crate 有两种形式:二进制项和库。二进制项 可以被编译为可执行程序,比如一个命令行程序或者一个服务器。它们必须有一个 main 函数来定义当程序被执行的时候所需要做的事情。目前我们所创建的 crate 都是二进制项。

库 并没有 main 函数,它们也不会编译为可执行程序,它们提供一些诸如函数之类的东西,使其他项目也能使用这些东西。crate库和其它编程语言中的library概念一致。

crate root 是一个源文件,Rust 编译器在crate根文件中寻找需要被编译的代码。

package

package是提供一系列功能的一个或者多个 crate。一个package会包含一个 Cargo.toml 文件,阐述如何去构建这些 crate。package中可以包含至多一个库 crate(library crate)。package中可以包含任意多个二进制 crate(binary crate),但是必须至少包含一个 crate(无论是库的还是二进制的)。

当我们使用cargo new的时候,就会创建一个package。Cargo 会给我们的package创建一个 Cargo.toml 文件。查看 Cargo.toml 的内容,会发现并没有提到 src/main.rs,因为 Cargo 遵循的一个约定:src/main.rs 就是一个与package同名的二进制 crate 的 crate 根。同样的,Cargo 知道如果包目录中包含 src/lib.rs,则package带有与其同名的库 crate,且 src/lib.rs 是 crate 根。crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目。

模块 Module

使用模块可以将 crate 中的代码按照功能性进行重组,最终实现更好的可读性及易用性。同时,我们还能非常灵活地去控制代码的可见性,进一步强化 Rust 的安全性。在crate根文件中,你可以声明module,例如使用mod garden声明一个叫做garden的模块。下面是一个例子,用来说明package,crate,module分别都是什么。
首先,我们使用cargo new --lib restaurant,来创建一个新的名为 restaurant 的库,并在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() {}
    }
}

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

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

上面这段代码在front_of_house中定义了两个子模块hosting和serving,在子模块内部又分别定义了多个函数。整个结构看起来就像一棵树。和我们的电脑上文件系统的目录树非常类似。你可以使用模块来组织你的代码。

一个项目的模块树可能如下所示。在一个package下可以有多个crate,crate下可以有多个module。

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

引用module路径

上面的代码组织方法构成了一个模块树, 而 rust 在模块树中找到一个项的位置是使用路径的方式,就像在文件系统使用路径一样。为了调用一个函数,我们需要知道它的路径。

路径有两种形式:

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

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

假设我们希望调用 add_to_waitlist 函数,该如何做?我们在 crate 根定义了一个新函数 eat_at_restaurant,并在其中展示调用 add_to_waitlist 函数的两种方法。

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

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

此时,你会发现编辑器报错了,你无法编译通过(cargo build)。编译器告诉你

module `hosting` is private

错误信息说 hosting 模块是私有的。换句话说,我们拥有 hosting 模块和 add_to_waitlist 函数的的正确路径,但是 Rust 不让我们使用,因为它不能访问私有片段。在 Rust 中,默认所有项(函数、方法、结构体、枚举、模块和常量)对父模块都是私有的。如果希望创建一个私有函数或结构体,你可以将其放入一个模块。

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

Rust 选择以这种方式来实现模块系统功能,因此默认隐藏内部实现细节。这样一来,你就知道可以更改内部代码的哪些部分而不会破坏外部代码。不过 Rust 也提供了通过使用 pub 关键字来创建公共项,使子模块的内部部分暴露给上级模块。

修改上面的代码,给hosting模块前面加上pub关键字来试试。

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

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}


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

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

这时,你发现编辑器的报错从hosting变成了add_to_waitlist了。还是不能通过cargo build。这表明使模块公有并不使其内容也是公有。模块上的 pub 关键字只允许其父模块引用它,而不允许访问内部代码。因为模块是一个容器,只是将模块变为公有能做的其实并不太多;同时需要更深入地选择将一个或多个项变为公有。

那么OK,我们再给add_to_waitlist函数加上pub关键字。

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

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}


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

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

这时候,错误消失,可以通过cargo build。不过带给我们的疑问是,为什么front_of_house不需要声明为pub,这是因为,虽然 front_of_house 模块不是公有的,不过因为 eat_at_restaurant 函数与 front_of_house 定义于同一模块中。

如果你计划让eat_at_restaurant 函数成为 crate 库的一个公共 API,那么还需要在eat_at_restaurant函数前加上pub。

我们刚才解决了编译问题,现在来解释一下绝对路径和相对路径。

第一种方式,我们在 eat_at_restaurant 中调用 add_to_waitlist 函数,使用的是绝对路径。add_to_waitlist 函数与 eat_at_restaurant 被定义在同一 crate 中,这意味着我们可以使用 crate 关键字为起始的绝对路径。

第二种方式,我们在 eat_at_restaurant 中调用 add_to_waitlist,使用的是相对路径。这个路径以 front_of_house 为起始,这个模块在模块树中,与 eat_at_restaurant 定义在同一层级。与之等价的文件系统路径就是 front_of_house/hosting/add_to_waitlist。以模块名开头意味着该路径是相对路径。

选择使用相对路径还是绝对路径,要取决于你的项目,也取决于你是更倾向于将项的定义代码与使用该项的代码分开来移动,还是一起移动。举一个例子,如果我们要将 front_of_house 模块和 eat_at_restaurant 函数一起移动到一个名为 customer_experience 的模块中,我们需要更新 add_to_waitlist 的绝对路径,但是相对路径还是可用的。然而,如果我们要将 eat_at_restaurant 函数单独移到一个名为 dining 的模块中,还是可以使用原本的绝对路径来调用 add_to_waitlist,但是相对路径必须要更新。我们更倾向于使用绝对路径,因为把代码定义和项调用各自独立地移动是更常见的。

模块树应该定义在 src/lib.rs 中。这样通过以包名开头的路径,公有项就可以在二进制 crate 中使用。二进制 crate 就完全变成了同其它 外部 crate 一样的库 crate 的用户:它只能使用公有 API。这有助于你设计一个好的 API;你不仅仅是作者,也是用户!

super

我们还可以使用 super 而不是当前模块或者 crate 根来开头来构建从父模块开始的相对路径。这么做类似于文件系统中以 … 开头的语法。使用 super 允许我们引用已知的父模块中的项,当模块与父模块关联的很紧密的时候,如果某天可能需要父模块要移动到模块树的其它位置,这使得重新组织模块树变得更容易。例如:

fn deliver_order() {}

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

    fn cook_order() {}
}

fix_incorrect_order 函数在 back_of_house 模块中,所以我们可以使用 super 进入 back_of_house 父模块,也就是本例中的 crate 根。在这里,我们可以找到 deliver_order。

创建公有的结构体和枚举

我们还可以使用 pub 来设计公有的结构体和枚举,不过关于在结构体和枚举上使用 pub 还有一些额外的细节需要注意。如果我们在一个结构体定义的前面使用了 pub ,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。我们可以根据情况决定每个字段是否公有。与之相反,如果我们将枚举设为公有,则它的所有成员都将变为公有。我们只需要在 enum 关键字前面加上 pub。如果枚举成员不是公有的,那么枚举会显得用处不大;给枚举的所有成员挨个添加 pub 是很令人恼火的,因此枚举成员默认就是公有的。结构体通常使用时,不必将它们的字段公有化,因此结构体遵循常规,内容全部是私有的,除非使用 pub 关键字。例如:

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

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // 在夏天订购一个黑麦土司作为早餐
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // 改变主意更换想要面包的类型
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // 如果取消下一行的注释代码不能编译;
    // 不允许查看或修改早餐附带的季节水果
    // meal.seasonal_fruit = String::from("blueberries");
}
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

无论我们选择 add_to_waitlist 函数的绝对路径还是相对路径,每次我们想要调用 add_to_waitlist 时,都必须指定front_of_house 和 hosting。幸运的是,有一种方法可以简化这个过程。我们可以使用 use 关键字创建一个短路径,然后就可以在作用域中的任何地方使用这个更短的名字。例如:

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

// 使用use缩短名称
use crate::front_of_house::hosting;

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

通过在 crate 根增加 use crate::front_of_house::hosting,现在 hosting 在作用域中就是有效的名称了,如同 hosting 模块被定义于 crate 根一样。通过 use 引入作用域的路径也会检查私有性。

注意 use 只能创建 use 所在的特定作用域内的短路径。

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

use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();
    }
}

将 eat_at_restaurant 函数移动到了一个叫 customer 的子模块后,hosting对于子模块是不可见的,无法编译。可以通过前面所讲述的super来告诉程序,hosting的位置。例如:


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

use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        super::hosting::add_to_waitlist();
    }
}

创建惯用的 use 路径

如果需要使用某个模块中的函数,那么惯例是使用 use 将函数的父模块引入作用域,我们必须在调用函数时指定父模块,这样可以清晰地表明函数是不是在本地定义的,同时使完整路径的重复度最小化。另一方面,使用 use 引入结构体、枚举和其他项时,习惯是指定它们的完整路径。例如,我们使用标准库的hashmap时,use std::collections::HashMap; ,例如下面的例子,如果不使用模块路径,那么将会引入两个同名的函数,这是rust不允许的。

use std::fmt::Result;
use std::io::Result;

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

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

而引入父模块则可以避免这种问题。

use std::fmt;
use std::io;

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

fn function2() -> io::Result<()> {
    // --snip--
    Ok(())
}

除此之外,还可以使用as关键字指定一个本地名称。

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

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

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

使用 pub use 重导出名称

使用 use 关键字,将某个名称导入当前作用域后,这个名称在此作用域中就可以使用了,但它对此作用域之外还是私有的。如果想让其他人调用我们的代码时,也能够正常使用这个名称,就好像它本来就在当前作用域一样,那我们可以将 pub 和 use 合起来使用。这种技术被称为 “重导出(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模块了。外部代码现在可以使用路径 restaurant::hosting::add_to_waitlist来调用add_to_waitlist函数。相当于hosting成为了restaurant模块的一部分。

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

当需要引入很多定义于相同包或相同模块的项时,为每一项单独列出一行会占用源码很大的空间。例如:

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

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

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

在较大的程序中,使用嵌套路径从相同包或模块中引入很多项,可以显著减少所需的独立 use 语句的数量!

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

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

use std::collections::*;

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

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

将模块拆分成多个文件

当模块变得更大时,你可能想要将它们的定义移动到单独的文件中,从而使代码更容易阅读。例如,我们会将模块提取到各自的文件中,而不是将所有模块都定义到 crate 根文件中。以前面的restaurant为例,我们将其拆分为多个文件。

// src/lib.rs
mod front_of_house;     // 声明front_of_house,
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
// src/fron_of_house.rs
pub mod hosting { 
    pub fn add_to_waitlist() {}
}

现在的目录结构如下所示:

src
├── front_of_house.rs
└── lib.rs

首先将 front_of_house 模块提取到其自己的文件中。删除 front_of_house 模块的大括号中的代码,只留下 mod front_of_house;声明;接下来将之前大括号内的代码放入一个名叫 src/front_of_house.rs 的新文件中。可以通过编译。

注意你只需在模块树中的某处使用一次 mod 声明就可以加载这个文件。一旦编译器知道了这个文件是项目的一部分(并且通过 mod 语句的位置知道了代码在模块树中的位置),项目中的其他文件应该使用其所声明的位置的路径来引用那个文件的代码,这在“引用模块项目的路径”部分有讲到。换句话说,mod 不是你可能会在其他编程语言中看到的 “include” 操作。

接下来我们同样将 hosting 模块提取到自己的文件中。这个过程会有所不同,因为 hosting 是 front_of_house 的子模块而不是根模块。我们将 hosting 的文件放在与模块树中它的父级模块同名的目录中,在这里是 src/front_of_house/。为了移动 hosting,修改 src/front_of_house.rs 使之仅包含 hosting 模块的声明。接着我们创建一个 src/front_of_house 目录和一个包含 hosting 模块定义的 hosting.rs 文件。例如:

// src/lib.rs
mod front_of_house;     // 声明front_of_house,
pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
// src/front_of_house.rs
pub mod hosting;        // 声明hosting模块
// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}     // 实现add_to_waitlist

那么目录如下所示:

src
├── front_of_house
│   └── hosting.rs
├── front_of_house.rs
└── lib.rs

我们将各个模块的代码移动到独立文件了,同时模块树依旧相同。eat_at_restaurant 中的函数调用也无需修改继续保持有效,即便其定义存在于不同的文件中。这个技巧让你可以在模块代码增长时,将它们移动到新文件中。

注意,src/lib.rs 中的 pub use crate::front_of_house::hosting 语句是没有改变的,在文件作为 crate 的一部分而编译时,use 不会有任何影响。mod 关键字声明了模块,Rust 会在与模块同名的文件中查找模块的代码。

另一种文件路径

目前为止我们介绍了 Rust 编译器所最常用的文件路径;不过一种更老的文件路径也仍然是支持的。

对于声明于 crate 根的 front_of_house 模块,编译器会在如下位置查找模块代码:

src/front_of_house.rs(我们所介绍的)

src/front_of_house/mod.rs(老风格,不过仍然支持) 对于 front_of_house 的子模块 hosting,编译器会在如下位置查找模块代码:

src/front_of_house/hosting.rs(我们所介绍的)

src/front_of_house/hosting/mod.rs(老风格,不过仍然支持)

如果你对同一模块同时使用这两种路径风格,会得到一个编译错误。在同一项目中的不同模块混用不同的路径风格是允许的,不过这会使他人感到疑惑。

使用 mod.rs 这一文件名的风格的主要缺点是会导致项目中出现很多 mod.rs 文件,当你在编辑器中同时打开他们时会感到疑惑。

参考资料

rust程序设计语言

 类似资料: