Rust学习笔记之包、Crate和模块

2023-03-23 19:52:23 浏览数 (1)

❝所有系统都有一种自毁趋势,往“熄灭”或者“圆寂”方向发展。这个趋势就叫“熵增”。为了维持系统,需要持续的输入能量,这种持续输入的能量我们就叫“负熵流”。 《向上生长》❞

大家好,我是「柒八九」

今天,我们继续「Rust学习笔记」的探索。我们来谈谈关于「包、Crate和模块」的相关知识点。

如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。

文章list

  1. Rust学习笔记之Rust环境配置和入门指南
  2. Rust学习笔记之基础概念
  3. Rust学习笔记之所有权
  4. Rust学习笔记之结构体
  5. Rust学习笔记之枚举和匹配模式

你能所学到的知识点

  1. Rust中包和 crate 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  2. 模块控制作用域与私有性 「推荐阅读指数」 ⭐️⭐️⭐️⭐️
  3. 路径用于引用模块树中的项 「推荐阅读指数」 ⭐️⭐️⭐️⭐️
  4. use名称引入作用域 「推荐阅读指数」 ⭐️⭐️⭐️⭐️
  5. 将模块分割进不同文件 「推荐阅读指数」 ⭐️⭐️⭐️❞

好了,天不早了,干点正事哇。


伴随着项目的增长,你可以通过将代码分解为多个模块和多个文件来组织代码。「一个包可以包含多个二进制 crate 项和一个可选的 crate 库」。伴随着包的增长,你可以将包中的部分代码提取出来,做成独立的 crate,这些 crate 则作为「外部依赖项」

Rust的模块系统the module system,包括:

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

包和 crate

  • 包(package) 是「提供一系列功能的一个或者多个 crate。」一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate
  • crate 是一个「二进制项或者库」crate root 是一个「源文件」Rust 编译器以它为起始点,并构成你的 crate 的根模块。

❝包中所包含的内容由几条规则来确立。

  1. 一个包中「至多只能」包含一个库 cratelibrary crate;
  2. 包中可以包含「任意多」个二进制 cratebinary crate;
  3. 包中「至少包含」一个 crate,无论是库的还是二进制的。❞

输入命令 cargo new

代码语言:javascript复制
$ 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 文件。查看 Cargo.toml 的内容,会发现并没有提到 src/main.rs,因为 Cargo 遵循的一个约定:

  • src/main.rs 就是一个「与包同名」的二进制 cratebinary crate 的 crate 根。
  • 同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的库 cratelibrary crate,且 src/lib.rscrate 根。❞

crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目。

如果一个包「同时含有」 src/main.rssrc/lib.rs,则它有两个 crate「一个库和一个二进制项,且名字都与包相同」

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

一个 crate 会将一个作用域内的「相关功能分组到一起」,使得该功能可以很方便地在多个项目之间共享


定义模块来控制作用域与私有性

「模块」 让我们可以将一个 crate 中的「代码进行分组,以提高可读性与重用性」。模块还可以控制项的 「私有性」

  • 是可以被外部代码使用的(public
  • 还是作为一个内部实现的内容,不能被外部代码使用(private)。

通过执行 cargo new --lib restaurant,来创建一个新的名为 restaurant的库。在 restaurant/src/lib.rs 中,来定义一些模块和函数。

代码语言:javascript复制
fn main() {
  mod front_of_house {
      mod hosting {
          fn add_to_waitlist() {}

          fn seat_at_table() {}
      }

      mod serving {
          fn take_order() {}

          fn server_order() {}

          fn take_payment() {}
      }
  }
}

「关键字 mod 定义一个模块」,指定模块的名字,并用大括号包围模块的主体。我们可以在模块中包含其他模块,就像本示例中的 hostingserving 模块。模块中也可以包含其他项,比如结构体、枚举、常量、trait

通过使用模块,我们可以把「相关的定义组织起来」,并通过模块命名来解释为什么它们之间有相关性。使用这部分代码的开发者可以更方便的循着这种分组找到自己需要的定义,而不需要通览所有。编写这部分代码的开发者通过分组知道该把新功能放在哪里以便继续让程序保持组织性。

之前我们提到,src/main.rssrc/lib.rs 被称为 crate 根。如此称呼的原因是,这两个文件中「任意一个的内容会构成名为 crate 的模块」,且该模块位于 crate 的被称为 模块树 的模块结构的根部at the root of the crate’s module structure。

上面的代码所对应的模块树如下所示。

代码语言:javascript复制
crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

这个树展示了模块间是如何相互嵌套的。这个树还展示了一些模块互为「兄弟」 ,即它们被定义在同一模块内。


路径用于引用模块树中的项

Rust 使用路径的方式在模块树中找到一个的位置,就像在文件系统使用路径一样。如果我们想要调用一个函数,我们需要知道它的路径。

❝路径有两种形式:

  1. 绝对路径absolute path从 crate 根部开始,以 crate 名或者字面量 crate 开头。
  2. 相对路径relative path从「当前模块开始」,以 selfsuper 或当前模块的标识符开头。❞

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

crate 根部定义了一个新函数 eat_at_restaurant,并在其中展示调用 add_to_waitlist 函数的两种方法。eat_at_restaurant 函数是我们 crate 库的一个「公共 API,所以我们使用 pub 关键字来标记它」

代码语言:javascript复制
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();
}

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

crate 后面,我们持续地嵌入模块,直到我们找到 add_to_waitlist。你可以想象出一个相同结构的文件系统,我们通过指定路径 /front_of_house/hosting/add_to_waitlist 来执行 add_to_waitlist 程序。我们使用 cratecrate 根部开始就类似于在 shell 中使用 / 从文件系统根开始。

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

模块不仅对于你组织代码很有用。他们还定义了 Rust 的 私有性边界privacy boundary:这条界线不允许外部代码了解、调用和依赖被封装的实现细节。所以,如果「你希望创建一个私有函数或结构体,你可以将其放入模块」

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

  • 「父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项」。 这是因为子模块封装并隐藏了他们的实现详情,但是「子模块可以看到他们定义的上下文」。❞

使用 pub 关键字暴露路径

想让父模块中的 eat_at_restaurant 函数可以访问子模块中的 add_to_waitlist 函数,因此我们使用 pub 关键字来标记 hosting 模块

代码语言:javascript复制
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();
}

cargo build的时候,代码编译仍然有错误。

mod hosting 前添加了 pub 关键字,使其变成公有的。伴随着这种变化,如果我们可以访问 front_of_house,那我们也可以访问 hosting。但是 hosting 的 内容contents仍然是私有的;

❝这表明「使模块公有并不使其内容也是公有的。模块上的 pub 关键字只允许其父模块引用它」。❞

继续将 pub 关键字放置在 add_to_waitlist 函数的定义之前,使其变成公有。

代码语言:javascript复制
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 起始的相对路径

还可以使用 super 开头来「构建从父模块开始的相对路径」。这么做类似于文件系统中以 .. 开头的语法。

如下模拟了厨师更正了一个错误订单,并亲自将其提供给客户的情况。fix_incorrect_order 函数通过指定的 super 起始的 serve_order 路径,来调用 serve_order 函数:

代码语言:javascript复制
fn serve_order() {}

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

    fn cook_order() {}
}

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


创建公有的结构体和枚举

还可以使用 pub 来设计公有的结构体枚举,不过有一些额外的细节需要注意。如果我们在一个结构体定义的前面使用了 pub ,这个结构体会变成公有的,但是这个「结构体的字段仍然是私有的」。我们可以「根据情况决定每个字段是否公有」

定义了一个公有结构体 back_of_house:Breakfast,其中有一个公有字段 toast 和私有字段 seasonal_fruit

代码语言:javascript复制
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("桃子"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    
    let mut meal = back_of_house::Breakfast::summer("Rye");
    
    meal.toast = String::from("Wheat");
    println!("我喜欢吃{} ", meal.toast);
}

因为 back_of_house::Breakfast 结构体的 toast 字段是公有的,所以我们可以在 eat_at_restaurant 中使用点号来随意的读写 toast 字段。

因为 back_of_house::Breakfast 「具有私有字段」,所以这个「结构体需要提供一个公共的关联函数来构造 Breakfast 的实例」。如果 Breakfast 没有这样的函数,我们将无法在 eat_at_restaurant 中创建 Breakfast 实例,因为我们不能在 eat_at_restaurant 中设置私有字段 seasonal_fruit 的值。

❝如果我们将枚举设为公有,则它的「所有成员都将变为公有」。我们只需要在 enum 关键字前面加上 pub。 ❞

代码语言:javascript复制
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 关键字将名称引入作用域

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

crate::front_of_house::hosting 模块引入了 eat_at_restaurant 函数的作用域,而我们只需要指定 hosting::add_to_waitlist 即可在 eat_at_restaurant 中调用 add_to_waitlist 函数。

代码语言:javascript复制
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();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
fn main() {}

在作用域中增加 use 和路径类似于「在文件系统中创建软连接」(符号连接symbolic link)。通过在 crate 根增加 use crate::front_of_house::hosting,现在 hosting 在作用域中就是有效的名称了,如同 hosting 模块被定义于 crate 根一样。通过 use 引入作用域的路径也会检查私有性,同其它路径一样。

还可以使用 use相对路径来将一个引入作用域。

代码语言:javascript复制
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use front_of_house::hosting;

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


创建惯用的 use 路径

要想使用 use 将函数的「父模块引入作用域」,我们必须在调用函数时指定父模块,这样可以清晰地表明函数不是在本地定义的,同时使完整路径的重复度最小化。

另一方面,使用 use 引入结构体、枚举和其他项时,习惯是「指定它们的完整路径」

代码语言:javascript复制
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

这个习惯用法有一个例外,那就是我们想使用 use 语句将两个「具有相同名称的项」带入作用域,因为 Rust 不允许这样做。

代码语言:javascript复制
fn main() {
  use std::fmt;
  use std::io;

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

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

使用「父模块」可以区分这两个 Result 类型。如果我们是指定 use std::fmt::Resultuse std::io::Result,我们将在同一作用域拥有了两个 Result 类型,当我们使用 Result 时,Rust 则不知道我们要用的是哪个。


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

使用 use 将两个「同名类型引入同一作用域」这个问题还有另一个解决办法:在这个类型的路径后面,我们使用 as 指定一个「新的本地名称或者别名」

代码语言:javascript复制
fn main() {
  use std::fmt::Result;
  use std::io::Result as IoResult;

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

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

在第二个 use 语句中,我们选择 IoResult 作为 std::io::Result 的新名称,它与从 std::fmt 引入作用域的 Result 并不冲突。


使用 pub use 重导出名称

当使用 use 关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果为了让调用你编写的代码的代码能够像在自己的作用域内引用这些类型,可以结合 pubuse。这个技术被称为 重导出re-exporting,因为这样做将引入作用域并同时使其可供其他代码引入自己的作用域。

代码语言:javascript复制
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();
}
fn main() {}

通过 pub use,现在可以通过新路径 hosting::add_to_waitlist 来调用 add_to_waitlist 函数。如果没有指定 pub useeat_at_restaurant 函数可以在其作用域中调用 hosting::add_to_waitlist,但「外部代码则不允许使用这个新路径」


使用外部包

假设项目使用了一个外部包rand,来生成随机数。为了在项目中使用 rand,在 Cargo.toml 中加入了如下行:

文件名: Cargo.toml

代码语言:javascript复制
[dependencies]
rand = "0.8.3"

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

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

代码语言:javascript复制
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng()
                        .gen_range(1..101);
}

crates.io 上有很多 Rust 社区成员发布的包,将其引入你自己的项目都需要一道相同的步骤:在 Cargo.toml 列出它们并通过 use 将其中定义的引入项目包的作用域中。 ❞

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

代码语言:javascript复制
fn main() {
    use std::collections::HashMap;
}

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

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

代码语言:javascript复制
fn main() {
  use std::cmp::Ordering;
  use std::io;
}

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

代码语言:javascript复制
fn main() {
  use std::{cmp::Ordering, io};
// ---snip---
}

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

如果希望将一个路径下 所有「公有项」引入作用域,可以指定路径后跟 *

代码语言:javascript复制
fn main() {
  use std::collections::*;
}

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


将模块分割进不同文件

当模块变得更大时,你可能想要将它们的定义移动到单独的文件中,从而使代码更容易阅读。

front_of_house 模块移动到属于它自己的文件 src/front_of_house.rs 中,通过改变 crate 根文件。在这个例子中,crate 根文件是 src/lib.rs,这也同样适用于以 src/main.rscrate 根文件的二进制 crate 项。

文件名: src/lib.rs

代码语言:javascript复制
mod front_of_house;

pub use crate::front_of_house::hosting;

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

文件名: src/front_of_house.rs

代码语言:javascript复制
pub mod hosting {
    pub fn add_to_waitlist() {}
}

mod front_of_house「使用分号」,而不是代码块,这将告诉 Rust 在另一个与模块同名的文件中加载模块的内容。


0 人点赞