首页 > 其他分享 >Rust 使用包、Crate 和模块管理不断增长的项目

Rust 使用包、Crate 和模块管理不断增长的项目

时间:2024-01-11 15:12:18浏览次数:35  
标签:use Crate crate pub 模块 house Rust mod

目录
本文在原文有删减,原文参考使用包、Crate 和模块管理不断增长的项目

Rust 有许多功能可以管理代码的组织,包括:

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

包和 Crate

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

二进制项 可以被编译为可执行程序,比如一个命令行程序或者一个服务器,必须有一个 main 函数来定义当程序被执行的时候所需要做的事情。

没有 main 函数,也不会编译为可执行程序,提供一些诸如函数之类的东西给其他项目使用。

crate root 是一个源文件,Rust 编译器以它为起始点,并构成 crate 的根模块。

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

包中可以包含至多一个库 crate(library crate),包中可以包含任意多个二进制 crate(binary crate),但是必须至少包含一个 crate(无论是库的还是二进制的)。

输入命令 cargo new 创建包:

$ cargo new my-project
     Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

注:ls 命令为 Linux 平台的指令,Windows 下可用 dir。

Cargo 遵循的一个约定:

  • src/main.rs 就是一个与包同名的二进制 crate 的 crate 根。
  • 如果包目录中包含 src/lib.rs ,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。
  • crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目。

一个只包含 src/main.rs 的包,意味着它只含有一个名为 my-project 的二进制 crate。如果一个包同时含有 src/main.rs 和 src/lib.rs,则它有两个 crate:一个二进制的和一个库的,且名字都与包相同。

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

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

下面介绍模块、路径、use关键词和pub关键词如何在编译器中工作,以及大部分开发者如何组织他们的代码:

  • 从 crate 根节点开始: 当编译一个 crate, 编译器首先在 crate 根文件(通常,对于一个库 crate 而言是src/lib.rs,对于一个二进制 crate 而言是src/main.rs)中寻找需要被编译的代码。

  • 声明模块: 在 crate 根文件中可以声明一个新模块,如用mod garden声明了一个叫做garden的模块,编译器会在下列路径中寻找模块代码:

    • 内联,在大括号中,当mod garden后方不是一个分号而是一个大括号
    • 在文件 src/garden.rs
    • 在文件 src/garden/mod.rs
  • 声明子模块: 在除了 crate 根节点以外的其他文件中可以定义子模块,如在src/garden.rs中定义了mod vegetables,编译器会在以父模块命名的目录中寻找子模块代码:

    • 内联,在大括号中,当mod vegetables后方不是一个分号而是一个大括号
    • 在文件 src/garden/vegetables.rs
    • 在文件 src/garden/vegetables/mod.rs
  • 模块中的代码路径: 在同一个 crate 内,只要隐私规则允许,可以从任意位置引用该模块的代码。如可以通过 crate::garden::vegetables::Asparagus 来引用 garden vegetables 模块下的 Asparagus 类型。

  • 私有 vs 公用: 一个模块里的代码默认对其父模块私有,为了使一个模块公用应当在声明时使用 pub mod 替代 mod,为了使一个公用模块内部的成员公用应当在声明前使用 pub

  • use 关键字: 在一个作用域内,可以用 use关键字创建了一个成员的快捷方式来减少长路径的重复,如在crate::garden::vegetables::Asparagus的作用域可以通过 use crate::garden::vegetables::Asparagus;创建一个快捷方式,然后就可以在作用域中只写Asparagus来使用该类型。

创建一个名为backyard的二进制 crate 来说明这些规则,该 crate 的路径同样命名为backyard,文件目录如下:

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

这个例子中的 crate 根文件是src/main.rs,内容如下:

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

//告诉编译器应该包含在src/garden.rs文件中发现的代码
pub mod garden;

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

文件 src/garden.rs 代码如下:

//在src/garden/vegetables.rs中的代码也应该被包括
pub mod vegetables;

文件 src/garden/vegetables.rs 代码如下:

#[derive(Debug)]
pub struct Asparagus {}

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

模块可以将一个 crate 中的代码进行分组,提高可读性和重用性。模块内的代码默认是私有的,可以利用模块的私有性来控制访问权限,私有项是外部无法使用的内部实现细节。同时,我们可以标记模块及其内部的项为公开,使外部代码可以使用和依赖它们。

执行 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() {}
    }
}

上面示例中的模块树的结构如下:

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

注:src/main.rssrc/lib.rs 之所以被叫做 crate 根,是因为这两个文件的内容都分别在 crate 模块结构的根组成了一个名为 crate 的模块,该结构被称为 模块树(module tree)

整个模块树都植根于名为 crate 的隐式模块下,模块树的结构类似于电脑上文件系统的目录树。

引用模块项目的路径

来看一下 Rust 如何在模块树中找到一个项的位置,调用一个函数需要知道它的路径,路径有两种形式:

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

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

使用绝对路径和相对路径来调用 add_to_waitlist 函数:

//无法通过编译:hosting 模块是私有的
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

// front_of_house 模块在模块树中与 eat_at_restaurant 定义在同一层级
pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

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

一般更倾向于使用绝对路径,因为把代码定义和项调用各自独立地移动是更常见的。

在 Rust 中,默认所有项(函数、方法、结构体、枚举、模块和常量)对父模块都是私有的,父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用它们父模块中的项

使用 pub 关键字暴露路径

为 mod hosting 和 fn add_to_waitlist 添加 pub 关键字使它们可以在 eat_at_restaurant 函数中被调用:

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

如果你计划共享你的库 crate 以便其它项目可以使用你的代码,公有 API 将是决定 crate 用户如何与你代码交互的契约。关于管理公有 API 的修改以便被人更容易依赖你的库的考量,可以参考 The Rust API Guidelines

二进制和库 crate 包的最佳实践

我们提到过包可以同时包含一个 src/main.rs 二进制 crate 根和一个 src/lib.rs 库 crate 根,并且这两个 crate 默认以包名来命名。通常,这种包含二进制 crate 和库 crate 的模式的包,在二进制 crate 中只有足够的代码来启动一个可执行文件,可执行文件调用库 crate 的代码。又因为库 crate 可以共享,这使得其它项目从包提供的大部分功能中受益

模块树应该定义在 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() {}
}

当认为 back_of_house 模块和 deliver_order 函数之间可能具有某种关联关系,并且重新组织 crate 的模块树时需要一起移动,我们就可以使用 super。

创建公有的结构体和枚举

可以使用 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");
}

back_of_house::Breakfast 具有私有字段,必须提供一个公共的关联函数来构造 Breakfast 的实例 ,否则将无法在 eat_at_restaurant 中创建 Breakfast 实例。

与之相反,如果将枚举设为公有,则它的所有成员都将变为公有。在 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 关键字将路径引入作用域

不得不编写路径来调用函数显得不便且重复,可以使用 use 关键字创建一个短路径,然后就可以在作用域中的任何地方使用这个更短的名字。通过 use 引入作用域的路径也会检查私有性,同其它路径一样。

通过在 crate 根增加 use crate::front_of_house::hosting,现在 hosting 在作用域中就是有效的名称了,如同 hosting 模块被定义于 crate 根一样:

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 所在的特定作用域内的短路径:

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() {
        //编译器错误:短路径不在适用于 customer 模块中
        hosting::add_to_waitlist(); 
    }
}

如果想修复这个编译错误,可以将 use 移动到 customer 模块内,或者在子模块 customer 内通过 super::hosting 引用父模块中的这个短路径。

创建惯用的 use 路径.

使用 use 引入函数

使用 use 将 add_to_waitlist 函数引入作用域并不符合习惯:

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

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    //不清楚 add_to_waitlist 是在哪里被定义的
    add_to_waitlist();
}

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

使用 use 引入结构体、枚举和其他项

将 HashMap 结构体引入作用域的习惯用法:

use std::collections::HashMap;

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

使用 use 引入结构体、枚举和其他项时,习惯是指定它们的完整路径,这是一种惯例。

使用 use 的例外用法

如果想使用 use 语句将两个具有相同名称的项带入作用域则需要指定父模块,将两个具有相同名称但不同父模块的 Result 类型引入作用域:

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

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

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

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

使用 use 将两个同名类型引入同一作用域这个问题还有另一个解决办法:在这个类型的路径后面使用 as 指定一个新的本地名称或者别名。
通过 as 重命名其中一个 Result 类型:

use std::fmt::Result;
//选择 IoResult 作为 std::io::Result 的新名称
use std::io::Result as IoResult;

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

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

使用 pub use 重导出名称

使用 use 关键字将某个名称导入当前作用域后,这个名称在此作用域中就可以使用了,但它对此作用域之外还是私有的。如果想让其他人调用我们的代码时也能够正常使用这个名称,那可以将 pub 和 use 合起来使用,这种技术被称为 “重导出(re-exporting)”。

通过 pub use 使名称可从新作用域中被导入至任何代码:

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

使用外部包

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

rand = "0.8.5"

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

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

use rand::Rng;

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

std 标准库也是外部 crate,只是无需修改 Cargo.toml 来引入 std,但需要通过 use 将标准库中定义的项引入项目包的作用域中来引用它们:

use std::collections::HashMap;

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

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

// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

可以使用嵌套路径将相同的项在一行中引入作用域,指定嵌套的路径在一行中将多个带有相同前缀的项引入作用域:

// --snip--
use std::{cmp::Ordering, io};
// --snip--

可以在路径的任何层级使用嵌套路径,这在组合两个共享子路径的 use 语句时非常有用。通过两行 use 语句引入两个路径,其中一个是另一个的子路径:

use std::io;
use std::io::Write;

为了在一行 use 语句中引入这两个路径,可以在嵌套路径中使用 self:

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

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

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

//将 std::collections 中定义的所有公有项引入当前作用
use std::collections::*;

使用 glob 运算符时需要小心,Glob 会使得我们难以推导作用域中有什么名称和它们是在何处定义的。
glob 运算符经常用于测试模块 tests 中,这时会将所有内容引入作用域。

将模块拆分成多个文件

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

声明 front_of_house 模块,其内容将位于 src/front_of_house.rs:

mod front_of_house;

pub use crate::front_of_house::hosting;

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

在 src/front_of_house.rs 中定义 front_of_house 模块:

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

mod 不同于其他编程语言中看到的 "include" 操作,它更依赖于代码文件目录。

为了移动 hosting,修改 src/front_of_house.rs 使之仅包含 hosting 模块的声明:

pub mod hosting;

接着创建一个 src/front_of_house 目录和一个包含 hosting 模块定义的 hosting.rs 文件:

pub fn add_to_waitlist() {}

编译器所遵循的哪些文件对应哪些模块的代码的规则,意味着目录和文件更接近于模块树

另一种文件路径

前面介绍了 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 文件,当在编辑器中同时打开它们时对程序员很不友好。

标签:use,Crate,crate,pub,模块,house,Rust,mod
From: https://www.cnblogs.com/timefiles/p/17958617

相关文章

  • 【Qt之Quick模块】8. Quick基础、布局管理、布局管理器
    1.前言QtQuick编程,提供了多种布局方式。如,静态布局,可以使用组件的x、y属性进行设置,或者进行绑定。还可以使用锚anchors进行布局。此外,还可以使用定位器以及定位管理器为多组件进行布局。但使用布局管理器和锚会占用内存和实例化时间,若使用x、y、width、height等属性能完成需......
  • 2023年山东省职业院校技能大赛高职组信息安全管理与评估 模块一
    2023年山东省职业院校技能大赛高职组信息安全管理与评估模块一模块一竞赛项目试题根据信息安全管理与评估技术文件要求,模块一为网络平台搭建与网络安全防护。本文件为信息安全管理与评估项目竞赛-模块一试题。所需的设备、机械、装置和材料:所有测试项目都可以由参赛选手根据......
  • Spark 框架模块和Spark的运行模式 -
    整个Spark框架模块包含:SparkCore、SparkSQL、SparkStreaming、SparkGraphX、SparkMLlib,而后四项的能力都是建立在核心引擎之上SparkCore:Spark的核心,Spark核心功能均由SparkCore模块提供,是Spark运行的基础。SparkCore以RDD为数据抽象,提供Python、Java、Scala、R语......
  • BOSHIDA 了解DC电源模块的基本参数及选择方法
    BOSHIDA了解DC电源模块的基本参数及选择方法DC电源模块是一种用来提供稳定直流电源的设备,常被应用在电子产品测试、实验室设备等领域。了解DC电源模块的基本参数和选择方法有助于正确选择和使用合适的模块。 1.输出电压范围:DC电源模块通常有固定的输出电压范围,例如0-30V。......
  • Finance_金蝶KIS专业版全模块精讲
    金蝶KIS专业版全模块精讲https://www.bilibili.com/video/BV19Y4y1X7DD?p=5&vd_source=8b9de621639420a0ceb703aceed712f7  第2节、公共基础设置新增部门 新增用户      第5节、固定资产  ......
  • LoRa SIP模块动能世纪XD6500S集成RF前端+LoRa无线电收发器SX1262
    相信大部分了解LoRa的朋友们都知道,LoRa是低功耗广域网通信技术中的一种,是Se***ch公司专有的一种基于扩频技术的超远距离无线传输技术。LoRaWAN是为LoRa远距离通信网络设计的一套通讯协议和系统架构。它是一种媒体访问控制(MAC)层协议。而我们今天的主角LoRaSIP模块动能世纪XD6500S......
  • 互联网项目架构演变过程(单体架构-模块化架构-微服务架构)
    1.单体架构1.传统的架构分为三层架构:web控制层,业务控制层,数据库访问层2.业务没有拆分,所有的代码写在一个项目工程中3. 一旦有一个模块导致服务不可用,可能会影响整个项目 2.模块化架构模块化项目就是把传统架构的项目进行业务拆分成多个app,最终打包成一个项目进行部署......
  • openpyxl模块---读取数据
    测试数据: 读取该表格内容:代码如下:defopen():fromopenpyxlimportload_workbookwb=load_workbook('C:/Users/admin/Desktop/baihuo.xlsx')###获取工作簿sh1=wb.activesh2=wb['Sheet1']sh3=wb.get_sheet_by_name('Sheet1')print(sh1......
  • 17. 从零用Rust编写正反向代理, Rust中一些功能的实现
    wmproxywmproxy是由Rust编写,已实现http/https代理,socks5代理,反向代理,静态文件服务器,内网穿透,配置热更新等,后续将实现websocket代理等,同时会将实现过程分享出来,感兴趣的可以一起造个轮子法项目地址gite:https://gitee.com/tickbh/wmproxygithub:https://github.com/tickbh/wmpr......
  • 监控易:独立而又耦合的产品模块,实现个性化运维需求
        监控易是一款高效、可靠的IT监控管理平台,旨在帮助企业实现对IT设备的全面监控和管理。    监控易产品模块包括视图、告警中心、设备管理、业务管理、网络管理、日志管理、机房动环、资产管理、运维管理、统计报告、智能工具、云平台和智能预测管理。    ......