Rust项目的代码组织

news/2025/3/19 6:50:05/文章来源:https://www.cnblogs.com/wang_yb/p/18341616

学习一种编程语言时,常常优先关注在语言的语法和标准库上,希望能够尽快用上新语言来开发,
我自己学习新的开发语言时也是这样。

不过,想用一种新的语言去开发实际的项目,或者自己做点小工具的话,除了语言本身之外,
了解它在项目中如何组织代码也是至关重要的。
毕竟在实际项目中,不可能像学习语言时那样,常常只有一个代码文件。

本文主要介绍Rust语言中常见的代码组织方式,我用Rust的时间也不长,不当之处,敬请指正。

1. 管理代码的单元

Rust在语言层面提供了3个代码管理单元,分别是:

  1. module:模块,类似于其他语言中的命名空间,一般是封装了某个具体功能的实现。

  2. crateRust编译的最小单元,每个crate中有一个Cargo.toml文件来配置crate相关信息,

     	一个`crate`可以是一个库也可以是一个二进制。
    
  3. package:一个package可以视为一个项目,cargo new 命令创建的就是package

这三者的关系是一个package可以包含多个crate,一个crate可以包含多个module。
image.png

对于一般的项目来说,常见的代码组织形式是一个crate+多个module

如果功能复杂,业务比较多的话,可以将独立的功能或者业务封装成crate,变成一个package包含多个crate+多个module
比如官方推荐用来学习Rust的项目 ripgrep(https://github.com/BurntSushi/ripgrep)。
就是多crate的项目,每个crate中有自己的Cargo.toml配置。
image.png

2. 模块(module)

packagecratemodule三种组织代码的方式中,最常用的就是module
下面主要介绍Rustmodule的定义和引用方式。

首先,我们创建一个演示用的项目。

$ cargo new myprojCreating binary (application) `myproj` package

默认项目的目录结构如下:
image.png

2.1. 同文件中的模块

Rust的模块不一定要定义在单独的文件中,同一个文件中可以定义多个不同的模块。
这一点和有些按照文件和目录来区分模块的语言是不一样的。
比如,下面在main.rs中定义了两个模块datacompute

mod data {pub fn fetch_data() -> Vec<u32> {vec![1, 2, 3]}
}mod compute {pub fn square_data(data: &Vec<u32>) -> Vec<u32> {let mut new_data: Vec<u32> = vec![];for i in data {new_data.push(i.pow(2));}new_data}
}use compute::square_data;
use data::fetch_data;fn main() {let data = fetch_data();println!("fetch data: {:?}", data);let new_data = square_data(&data);println!("square data: {:?}", new_data);
}

引用同一文件中的模块很简单,直接use模块和对应的方法即可。

use compute::square_data;
use data::fetch_data;

执行效果如下:
image.png

也许你会有这样的疑问,既然都在同一个文件中,为什么还要定义模块,直接定义两个函数不就可以了,
这样main函数中直接就可以使用,还省得去引用模块。

因为对于一些代码比较短,但是通用性高的功能函数(比如简单的字符串转换,日期格式转换),
每个函数都创建一个模块文件的话,略显繁琐,统一在一个文件中定义反而更加清爽简洁。

以后如果模块中的功能越来越多,越来越复杂,再将此模块重构到一个单独的文件中也不迟。

2.2. 同目录中的模块

同文件中的模块引用非常简单,但是当模块中的代码逐渐增多之后,就需要考虑用一个单独的文件来定义模块。
比如,将上面代码中模块datacompute中的函数分别放入不同的文件。
文件结构如下:
image.png
其中 src 目录下的3个文件中的内容分别为:

// data.rs 文件
pub fn fetch_data() -> Vec<u32> {vec![1, 2, 3]
}
// compute.rs 文件
pub fn square_data(data: &Vec<u32>) -> Vec<u32> {let mut new_data: Vec<u32> = vec![];for i in data {new_data.push(i.pow(2));}new_data
}
// main.rs
mod compute;
mod data;use compute::square_data;
use data::fetch_data;fn main() {let data = fetch_data();println!("fetch data: {:?}", data);let new_data = square_data(&data);println!("square data: {:?}", new_data);
}

注意这里与上一节同文件中模块的几点不同。
首先,在模块的文件data.rscompute.rs中,直接定义函数即可,
不用加上 mod data{}这样的模块名,因为文件名datacompute会自动作为模块的名字。

其次,在main.rs文件中引用不同文件的模块时,
需要先定义模块(相当于导入了data.rscompute.rs),

mod compute;
mod data;

然后在 use 其中的函数。

use compute::square_data;
use data::fetch_data;

2.3. 不同目录中的模块

最后在看看不同目录中的模块如何引用。
当项目功能逐渐增多和复杂之后,一个功能模块就不止一个文件了,所以就需要将模块封装到不同的目录中。
比如一个普通的Web系统,其中日志模块,数据库模块等等都会单独封装到不同的目录中。

重新修改示例中的文件,代码改为如下的结构:
image.png
封装一个日志模块,其中有2个输出日志的代码文件(debug.rsinfo.rs),分别输出不同级别的日志。
然后在代码中引用日志模块并输出日志。
各个文件的代码分别如下:

// debug.rs 文件
pub fn log_msg(msg: &str) {println!("[DEBUG]: {}", msg);
}// info.rs 文件
pub fn log_msg(msg: &str) {println!("[INFO]: {}", msg);
}// main.rs 文件
mod log;use crate::log::debug;
use crate::log::info;fn main() {debug::log_msg("hello");info::log_msg("hello");
}

注意,除了上面这3个文件,在log文件夹中还有个mod.rs文件,
这个文件至关重要,其中的内容就是定义log文件夹中有哪些模块。

// mod.rs 文件
pub mod debug;
pub mod info;

有了这个文件,log文件夹才会被Rust当成是一个模块,才可以在main.rs中引用mod log;

执行效果如下:
image.png

接下来,再增加一个database模块,让database模块引用log模块中的函数,
也就是兄弟模块间的引用。
文件目录结构变为:
image.png
新增的database模块中两个文件中的代码如下:

// db.rs
use crate::log::debug;
use crate::log::info;pub fn create_db() {debug::log_msg("start to create database");info::log_msg("Create Database");debug::log_msg("success to create database");
}// mod.rs
pub mod db;

main.rs中的代码如下:

// main.rs
mod database;
mod log;use crate::database::db;fn main() {db::create_db();
}

执行结果如下:
image.png

这里有一点需要注意database模块的db.rs中直接引用了log模块中的函数,
需要在main.rs中定义mod log;
虽然main.rs中没有直接使用log模块中的内容,仍然需要定义mod log;
否则database模块中的db.rs无法引用log模块中的函数。

2.4. 模块的相对和绝对引用

Rust模块的引用有两种方式,相对路径的引用和绝对路径的引用。
上面的示例中都是绝对引用,比如:use crate::log::debug;use crate::database::db;
绝对引用以crate开头,可以把crate理解为的代码的根模块。

相对引用有两个关键字,selfsuperself表示同级的模块,super表示上一级的模块。
比如,上面示例中main.rsdb.rs文件中的模块也可以改为相对引用:

//main.rs
// use crate::database::db;
use self::database::db;//db.rs
// use crate::log::debug;
// use crate::log::info;
use super::super::log::debug;
use super::super::log::info;

3. 包(crate)

crateRust编译和发布的最小单元,一般项目都是单crate多module的,
如果项目中某些模块通用型强,可能会单独发布给其他项目用,那么把这些模块封装成crate也是不错的想法。

Rust时间不长,还没有实际用到多crate的情况,感兴趣的话可以参考ripgrep项目(https://github.com/BurntSushi/ripgrep)。

4. 总结

本文重点介绍了Rust中模块(module)的定义和引用方式,目的为了让我们在使用Rust时能够合理的组织和重构自己的代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/777643.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

springboot+vue前后端分离项目-项目搭建17-集成AOP系统日志

后端 1. 新增logs表和实体类,新增com/example/demo/mapper/LogsMapper.java,新增com/example/demo/controller/LogsController.javapackage com.example.demo.controller;import cn.hutool.core.util.StrUtil; import cn.hutool.poi.excel.ExcelReader; import cn.hutool.poi…

8.2日CSP-J初赛内容总结

8.2日CSP-J初赛内容总结Adobe:PS,PR,......Reader 微软:Onedrive(存文件),Excel(表格),Word(文字编辑),Onenote(笔记),PowerPoint(PPT)位号从正数部分最低位开始编号,0到更大的数字。 位号从左往右的小数部分从 \(-1\) 开始编号,编号变小基数:进制的进位数字 位权:基数的…

8.3日CSP-J初赛内容总结

8.3日CSP-J初赛内容总结 优先级 \(括号>非>与>或\) \(括号>逻辑运算>位运算\) \(括号>按位取反>按位与>按位或=按位异或\) 按位与或非 \(\to\) 补码按位取反补码所有位取反 按位与将 \(2\) 个补码对其地位 逐位比较1的个数基本上等于 \(n\) 除 \(2\) 的…

初学java4

这周我重新下载了jdk-17并更改了路径,目的是为了添加新的环境变量用于使用eclipse。 eclipse for java作为老牌java编程所需的软件,很值得我学习使用,不过下载以及环境变量的准备有些麻烦。 下载成功后使用起来就很方便了。

问题:ModuleNotFoundError: No module named pydotplus

无法找到pydotplus模块在 Anaconda propmt中安装:pip install pydotplus

使用PasteSpider实现类似Jenkins的功能,让你的2G服务器也可以飞起

或许你接触过Jenkins, 在我理解就是拉取源码,然后构建成镜像,最后启动容器! 但是这个功能对于小内存的服务器来说就是奢望了! 今天介绍一个新版本,把你这个遗憾弥补下! 在PasteSpider中,也是支持拉取源码,然后编译发布的!!! 以下案例使用svn作为源码管理 如果你使用…

D37 2-SAT P3007 [USACO11JAN] The Continental Cowngress G

视频链接:D37 2-SAT P3007 [USACO11JAN] The Continental Cowngress G_哔哩哔哩_bilibili P3007 [USACO11JAN] The Continental Cowngress G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)// O(n*n) #include <iostream> #include <cstring> #include <algo…

Java 文件 I/O流详解

文件文件操作是Java开发中一个重要的组成部分,它允许开发者对文件进行读取,写入,创建,删除和修改等操作,文件操作的主要通过java.io包中的类来实现的,其中的File类更是文件操作的核心类File类的常用方法 创建文件或目录文件创建使用createNewFile();可以创建一个新的空文件,如果…

kubelet节点资源预留

目录一、Node Allocatable1、node资源预留1.1 为什么要做资源预留?1.2 node allocatable1.2.1 查看node节点资源1.2.2 确认node01节点资源2、 配置资源预留2.1 kube预留值2.2 systemReserved预留2.3 evictionHard预留2.4 整体配置2.5 重启服务2.6 Allocatable资源说明 一、N…

mysql 是否该数据列每个数据都唯一就应该设置唯一索引?

前言 比较一下唯一索引和普通索引的区别。 如果有一列数据唯一,这个时候是否是就直接设置唯一索引,这样可以避免插入重复的值,来实现业务需求。 那么唯一索引是如何保持唯一的呢?这个对性能是否有影响。 正文 数据库我们知道是增删改查。 那么首先来看下这个查,唯一索引是…

Markdown 达人必备!轻松几步画出专业流程图

流程图,顾名思义,就是表示一个事件或活动的流程的图示。流程图,顾名思义,就是表示一个事件或活动的流程的图示。 ‍ ‍ 快速入门 最简单的例子:从 A 到 B graph TDA --> B‍ 效果: graph TDA --> B ‍ ‍ 注意起始的关键字“grpah”是必须的,表明这是流程图。 后续…

CVE-2023-1313 复现

CVE-2023-1313 复现cockpit在2.4.1版本之前存在任意文件上传漏洞PS:通过在浏览器中打开/install来运行安装首先看到了一个登录页面.我们按照提示访问/install,成功的添加了用户名和密码都为admin,登录. 来到如下界面,可以进行任意文件上传然后点击三个点可以选择下载文件,将得…