Rust如何进行模块化开发?

简介: Rust如何进行模块化开发?
类似es6的模块化,Rust通过package、create、module来实现代码的模块化管理

Rust如何进行模块化开发?

Rust的代码组织包括:哪些细节可以暴露,哪些细节是私有的,作用域内哪些名称有效等等。

而这些功能被统称为模块系统,模块系统被分为(由上到下层层包含):

  • Package(包):Cargo的特性,让你构建、测试、共享create
  • Create(单元包):一个模块树,它可以产生一个library或可执行文件
  • Module(模块)、use:让你控制代码的组织、作用域、私有路径
  • Path(路径):为struct、function或module等项命名的方式

Package和Create

create的类型:

  • binary(二进制create)
  • library(库create)

其中,关于Create,还有个概念——Create Root:

  • 是源代码文件
  • Rust编译器从这里开始,组成你的Create的根Module

一个Package:

  • 包含一个Cargo.toml,它描述了如何构建这些Crates
  • 只能包含0-1个library create(库create)
  • 可以包含任意数量的binary create(二进制create)
  • 但必须至少包含一个create(library或binary)

我们使用cargo新建一个项目

image-20221206214124276

然后会提示: Created binary (application) my-project package,这代表我们创建了一个二进制的应用程序,名叫my-project的package

我们进入这个文件夹:

image-20221206214514512

我们可以看到src/min.rs文件,这是我们程序的入口文件,但是我们在Cargo.toml中并没有看到相关的配置:

[package]
name = "my-project"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

这是因为cargo有一些惯例

Cargo的惯例

  • src/main.rs是binary create的create root
  • create的名与package名相同

如果我们还有一个这个文件:src/lib.rs,那么:

  • 表明package包含一个library create
  • 它是library create的create root
  • create的名与package名相同

Cargo将会把create root文件交给rustc(rust编译器)来构建library或者binary

一个Package可以同时包含src/main.rs和src/lib.rs

一个Package也可以有多个binary create:

  • 文件放在src/bin,放在这里的每个文件都是单独的binary create

Create的作用

将相关功能组合到一个作用域内,便于在项目间进行共享。

同时,这也能防止命名冲突,例如rand create,访问它的功能需要通过它的名字:rand

定义module来控制作用域和私有性

Module:

  • 在一个create内,将代码进行分组
  • 增加可读性,易于复用
  • 控制项目(item)的私有性。public,private

建立module:

  • mod关键字
  • 可嵌套
  • 可包含其他项(struct、enum、常量、trait、函数等)的定义
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() {}
    }
}

image-20221206221349287

src/main.rs 和 src/lib.rs 叫做create roots:

  • 这两个文件(任意一个)的内容形成了名为create的模块,位于整个模块树的根部
  • 整个模块树在隐式的模块下

路径Path

路径的作用是为了在rust的模块中找到某个条目

路径的两种形式:

  • 绝对路径:从create root开始,使用create名或字面值create
  • 相对路径:从当前模块开始,使用self(本身),super(上一级)或当前模块的标识符

路径至少由一个标识符组成,标识符之间使用::。

举个例子(下面这段程序将报错,我们将在后面讲到如何解决):

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();//相对路径
}

那么为什么会报错呢?

我们查看报错的原因:module hosting is private,编译器告诉我们,hosting这个module是私有的。至此,为了解决这个问题,我们应该去了解一下私有边界。

私有边界(private boundary)

  • 模块不仅可以组织代码,还可以定义私有边界
  • 如果把函数或struct等设为私有,可以将它放到某个模块中。
  • rust中所有的条目(函数,方法,struct,enum,模块,常量)默认情况下是私有的
  • 父级模块无法访问子模块中的私有条目
  • 但是在子模块中可以使用所有祖先模块中的条目

为什么rust默认这些条目是私有的呢?因为rust希望能够隐藏内部的实现细节,这样就会让开发者明确知道:更改哪些内部代码的时候,不会破坏外部的代码。同时,我们可以使用pub关键字将其声明为公共的。

pub关键字

rust默认这些条目为私有的,我们可以使用pub关键字来将某些条目标记为公共的。

我们将hosting声明pub,add_to_waitlist这个function也要声明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();//相对路径
}

为什么front_of_house这个mod不需要添加pub呢?因为它们是同级的。

super关键字

super:用来访问父级模块路径中的内容,类似文件系统中的..

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

pub struct

声明一个公共的struct就是将pub放在struct前:

mod back_of_house {
    pub struct Breakfast {}
}

声明了一个公共的struct后:

  • struct是公共的
  • struct的字段默认是私有的

而我们想让struct中的字段为公有的必须在前面加上pub:

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,      //公有的
        seasonal_fruit: String, //私有的
    }
}

也就是说:struct的字段需要单独设置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");//报错:field `seasonal_fruit` is private
}

pub enum

声明一个公共的enum就是将pub放在enum前:

mod back_of_house {
    pub enum Appetizer {}
}

我们声明了一个公共的enum后:

  • enum是公共的
  • enum的变体也都是公共的
mod back_of_house {
    pub enum Appetizer {
        Soup,  //公共的
        Salad, //公共的
    }
}

为什么呢?因为枚举里面只有变体,只有变体是公共的这个枚举才有用。而struct中某些部分为私有的也不影响struct的使用,所以rust规定公共的struct中的字段默认为私有的。

Use关键字

我们可以使用use关键字将路径导入到作用域内,而我们引入的东西也任然遵循私有性规则(公共的引入的才能用)

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
        fn some_function() {}//私有的,使用use导入后,外部依然不能调用这个函数
    }
}

use crate::front_of_house::hosting;
// 相当于mod hosting {}

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

使用use来指定相对路径(和使用条目时的规则相同):

use front_of_house::hosting;

我们可以注意到我们调用的add_to_waitlist是导入的hostingmod下的,那我们可不可以直接导入function呢?

当然是可以的(不过并不推荐直接导入方法):

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

use crate::front_of_house::hosting::add_to_waitlist;
// 相对于mod hosting {}

pub fn eat_at_restaurant() {
    add_to_waitlist();
}

use的习惯用法

当我们直接导入方法时,我们有可能就搞不清楚是从其他模块导入的还是在这个作用域下声明的。

所以,通常情况下,我们导入的通常为父级模块。

//...
use crate::front_of_house::hosting;

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

不过,struct,enum,其他:指定完整路径(指定到本身)

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

但是同名的条目,我们在引入时需指定父级模块(比如下面的例子,两个类型都叫Result)

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

fn f1() -> fmt::Result {
    //...
}

fn f2() -> io::Result {
    //...
}
//...

as关键字

关于上面同名的问题,还有另一种解决方法:使用as关键字

as关键字可以为引入的路径指定本地的别名

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

fn f1() -> Result {
    //...
}

fn f2() -> IoResult {
    //...
}

使用 pub use 重新导出名称

使用 use 将路径(名称)导入到作用域内后,该名称在此作用域内是私有的,外部的模块是没办法访问use导入的模块的。

由前面pub的作用可知,类似pub fn、pub mod,我们可以使用pub use来导入,相当于它导入了这个内容,然后又将它导出了。

(当我们使用pub use时会发现没有警告:“导入了但没有使用”,因为它同时也导出了,也被视作使用了这个导入的内容)

导入外部包

我们通过在Cargo.toml中的[dependencies]添加依赖:

# ...
[dependencies]
rand = "^0.8.5"

出现:Blocking waiting for file lock on package cache

删除User/.cargo文件夹中的.package-cache文件。重新执行cargo build下载依赖。

很多时候我们的下载速度很慢,我们可以将下载源换到国内,在用户文件夹下的.cargo文件夹中添加 config 文件,写入以下内容:

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"
# 如果所处的环境中不允许使用 git 协议,可以把上面的地址改为
# registry = "https://mirrors.ustc.edu.cn/crates.io-index"
#[http]
#check-revoke = false

这时候cargo build就会很快了。

我们这样导入:

use rand::Rng;

另外:标准库也被当做外部包,需要导入,并且:

  • 我们不需要修改Cargo.toml来添加依赖
  • 需要使用use将std的特定条目导入到当前作用域

use多次导入(嵌套导入)

use std::{ascii, io};
//相当于:use std::ascii;
// use std::io;

这样的导入该如何简写呢?

use std::io;
use std::io::Chain;

可以使用self

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

如何将模块放入其他文件?

假如我们的src/lib.rs中的内容是这样:

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

我们可以在lib.rs同级目录下新建front_of_house.rs,然后将模块内容写在文件中:

front_of_house.rs

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

lib.rs

mod front_of_house;
//...

如果我们想将hosting模块的内容单独存放呢?

我们需要新建一个front_of_house文件夹,并新建hosting.rs文件

image-20221207212857681

hosting.rs

pub fn add_to_waitlist() {}

front_of_house.rs

pub mod hosting;

lib.rs

mod front_of_house;
//...

原来的文件内容:

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

随着模块逐渐变大,这项功能将能够帮助我们更好的管理代码

相关文章
|
4月前
|
人工智能 搜索推荐 测试技术
实战拆解 GEO:外贸 B2B 企业如何构建面向 AI 搜索的增长引擎
本文详解外贸B2B企业应对AI搜索变革的GEO(生成式引擎优化)方法论:从“争搜索排名”转向“进AI答案”,强调构建企业数字人格、基于客户问题生产可信内容、建设知识原子库、双模官网承载、CRM闭环转化,并指出常见误区与分层验证指标,助力企业系统化打造AI时代的增长基础设施。
572 1
|
10月前
|
存储 编解码 API
3步搞定!阿里云视频点播入门保姆级教程
本文是阿里云视频点播系列教程首篇,面向零基础用户,详细介绍从开通服务、上传视频、转码处理到播放器嵌入的完整流程,涵盖控制台操作与SDK开发,助你快速搭建音视频应用。
1974 1
3步搞定!阿里云视频点播入门保姆级教程
|
Oracle 关系型数据库 MySQL
OceanBase实践入门:高可用原理和容灾方案
OceanBase的多副本(奇数)设计,以及使用Paxos协议同步事务日志,是OceanBase高可用能做到自动切换(RTO约20s)和不丢数据(RPO=0)的关键。OceanBase在这个设计上还衍生出很多特性:如负载均衡和异地多活等。
6486 0
|
9月前
|
安全 BI 5G
工厂人员定位软件系统从场景分级与技术选型、系统架构到核心功能详解(一)
工厂人员定位系统以UWB/蓝牙AOA等技术为核心,构建“实时可视、轨迹可溯、风险可防、应急可控”的全场景管理平台,实现从“事后追溯”到“事前预警+事中干预”的升级,适配高危区、车间及仓储等多元场景,融合安全合规与生产提效。如果您想进一步了解工厂人员定位软件系统的技术和案例,欢迎搜索维构lbs智能定位~
|
10月前
|
存储 人工智能 JSON
🤖 Agent系统
大模型Agent是具备自主规划、推理决策、工具调用与记忆能力的智能体,核心由大脑(LLM)、感知、行动与记忆组件构成,支持任务分解、反思优化与多轮交互。常见框架如LangChain、AutoGPT、CrewAI等,广泛应用于复杂任务处理与企业级AI系统。(238字)
|
JavaScript
vue3 实现电子签名
vue3 实现电子签名
1482 1
|
消息中间件 NoSQL Java
基于springboot+Redis的前后端分离项目之消息队列(六)-【黑马点评】
在这里笔者想给大家分享一下课程内没有的思路,看看有没有小伙伴这么想,比如,我们可以不可以使用异步编排来做,或者说我开启N多线程,N多个线程,一个线程执行查询优惠卷,一个执行判断扣减库存,一个去创建订单等等,然后再统一做返回,这种做法和课程中有哪种好呢?答案是课程中的好,因为如果你采用我刚说的方式,如果访问的人很多,那么线程池中的线程可能一下子就被消耗完了,而且你使用上述方案,最大的特点在于,你觉得时效性会非常重要,但是你想想是吗?并不是,比如我只要确定他能做这件事,然后我后边慢慢做就可以了,我并不需要他一口气做完这件事,所以我们应当采用的是课程中,类似消息队列的方式来完成我们的需求。
|
人工智能 NoSQL Cloud Native
【云原生专题】基于Docker+Neo4j图数据库搭建企业级分布式应用拓扑图
【云原生专题】基于Docker+Neo4j图数据库搭建企业级分布式应用拓扑图
【云原生专题】基于Docker+Neo4j图数据库搭建企业级分布式应用拓扑图
|
存储 Ubuntu 关系型数据库
7天突破PolarDB for Postgre 2022版—第一讲 单机本地存储部署实例
分享人:棠羽,阿里云智能数据库产品事业部PolarDB产品部内核工程师
7天突破PolarDB for Postgre 2022版—第一讲  单机本地存储部署实例
|
Python
visdom安装与基本用法
visdom安装与基本用法
1044 0
visdom安装与基本用法