Rust 进阶学习笔记(五):包,模块与Cargo指南
目录
项目及其目录结构,包,模块,模块的引入与模块可见性,包的构建,依赖的添加,cargo配置和清单,工作空间,条件编译,发布配置
本文属于我的 Rust 学习笔记 系列。
Rust 入门学习笔记以实际例子为主,讲解部分不是从零开始的,所以不建议纯萌新观看,读者最好拥有任意一种面向对象语言的基础,然后自己多多少少看过 Rust 的基本语法,刷过一点 rustlings。
Rust 进阶学习笔记以及实战的来源则五花八门,将会标注在下一行⬇️。
本节出处:圣经-2.12包和模块 圣经-4.10Cargo
从这节开始,每节的内容开始多了起来,我也做不到日更了。水温逐渐升高了!
本节的内容非常多但信息密度不大,慢慢写吧。
当工程规模变大时,把代码写到一个甚至几个文件中,都是不太聪明的做法。反之,将大的代码文件拆分成包和模块,还允许我们实现代码抽象和复用。因此,跟其它语言一样,Rust 也提供了代码的组织管理的方式。
Cargo 是包管理工具,可以用于依赖包的下载、编译、更新、分发等。Cargo 依赖 crates.io,它是社区提供的包注册中心:用户可以将自己的包发布到该注册中心,然后其它用户通过注册中心引入该包。可以理解成 maven 中央仓库。另有一个网站 lib.rs 非常适合用来查找包(不过这网站的访问速度比较感人)。
本节中,我们会仔细学习这些概念,目的是搞清楚一个项目结构的方方面面。这一节乍一看十分基础,其他语言也都有类似的东西,实际上非常重要,能让我们从代码之外进一步理解 Rust。
项目,包和模块
- 项目(Packages):一个 Cargo 提供的 feature,可以用来构建、测试和分享包
- 包(Crate):一个由多个模块组成的树形结构,可以作为三方库进行分发,也可以生成可执行文件进行运行
- 模块(Module):可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元
包和项目
包
包(Crate)是 Rust 的一个独立的可编译单元,它编译后会生成一个可执行文件或者一个库。
一个包会将相关联的功能打包在一起,使得该功能可以很方便的在多个项目中分享。我们只需要将该包通过use xxx;
引入到当前项目的作用域中,就可以在项目中使用 xxx 的功能。
同一个包中不能有同名的类型,但是在不同包中就可以。在代码中通过不同包名头引用,编译器是不会产生歧义的。
多个包联合在一起,组织成工作空间(WorkSpace)。
项目
这里的项目(Package)可以理解为工程、软件包。它包含有独立的Cargo.toml
文件,以及因为功能性被组织在一起的一个或多个包。一个项目只能包含一个库(library)包,但可以包含多个可执行的二进制包。
下面的命令将会创建一个项目。
此时会出现一个名称是 my-project 的 package,其中包含一个Cargo.toml
文件,以及一个src/main.rs
。src/main.rs
会被 Rust 默认作为二进制包的根文件,该二进制包的包名跟所属 Package 相同,所有的代码执行都从该文件中的fn main()
函数开始。
使用cargo run
可以运行该项目,输出:Hello, world!。
再来创建一个库包。
库类型的包只能作为三方库被其它项目引用,无法独立运行。同样道理,src/lib.rs
会被 Rust 默认当作库包的根文件。
如此一来,一个典型项目会拥有如下的结构。其中可能会包含多个二进制包,这些包文件被放在src/bin
目录下,每一个文件都是独立的二进制包;同时也会包含一个库包,该包只能存在一个src/lib.rs
:
模块
模块(module)是 Rust 的代码构成单元。模块可以将包中的代码按照功能进行重组,最终实现更好的可读性及易用性。同时,还能让开发人员非常灵活地去控制代码的可见性。
模块树
模块之间的关系可以用一棵树表示,类似文件系统的树。从一个包根(crate root)形成的模块出发,可以嵌套若干个模块。模块有如下特点:
- 使用
mod
关键字来创建新模块,后面紧跟着模块名称 - 模块可以嵌套
- 模块中可以定义各种 Rust 类型,例如函数、结构体、枚举、特质等
- 所有模块均定义在同一个文件中
因此,就像树的节点一样,模块之间也存在父子关系。
引用模块
路径引用
Rust 中的路径有两种形式:
- 绝对路径:从包根开始,路径名以包名(或
crate
)开头。 - 相对路径:从当前模块开始,以当前模块的标识符(
self
、super
)开头。
当然,相对路径什么都不加也会视为self
。可以看如下例子:
// 假设函数和模块都定义在包根下
实际开发中,调用的地方和定义的地方往往是分离的,而定义的地方较少会变动。所以可以优先考虑使用绝对路径。
Rust 出于安全的考虑,默认情况下,所有的类型都是私有化的,包括函数、方法、结构体、枚举、常量甚至模块本身。父模块完全无法访问子模块中的私有项,但是子模块却可以访问父模块、父父..模块的私有项。
Rust 提供了pub
关键字,通过它你可以控制模块和模块中指定项的可见性。模块可见性不代表模块内部项的可见性,模块的可见性仅仅是允许其它模块去引用它,但是想要引用它内部的项,还得继续将对应的项标记为pub
。上例中,子模块和里面的函数都需要标注。
父引用
super
代表的是父模块为开始的引用方式,非常类似于文件系统中的..
语法
// 厨房模块
这样的好处是,只要相对关系不变,未来就算它们都不在包根了,依然无需修改引用路径。
自引用
self
其实就是引用自身模块中的项。
表面看起来,加不加self
没有区别,似乎多此一举,本例中确实如此。实际上,self 通常起到在嵌套场景下指明路径起点的作用,防止在某些上下文中无法推断路径。
模块分离
当模块变多或者变大时,需要将模块放入一个单独的文件中,让代码更好维护。
// 从另一个和模块 front_of_house 同名的文件中加载该模块的内容
// 使用绝对路径的方式来引用模块
pub use crate hosting;
这种情况下,模块的声明和实现是分离的,声明语句可以把模块的内容从声明对应的文件中加载进来。use
关键字能够将外部模块中的项引入到当前作用域中来,避免冗长的调用前缀。
当一个模块有许多子模块时,也可以通过文件夹的方式来组织这些子模块。需要显示指定暴露哪些子模块。假设此时创建一个目录 front_of_house,然后在文件夹里创建一个 hosting.rs 文件,内容是
指定方法是创建一个新的文件来定义子模块,子模块名与文件名需要相同。
这个新的文件可以是如下两种:
- 在 front_of_house 目录里创建一个
mod.rs
,如果 rustc 版本在 1.30 之前,这是唯一的方法。 - 在 front_of_house 同级目录里创建一个与模块(目录)同名的 rs 文件
front_of_house.rs
,在新版本里,更建议使用这样的命名方式来避免项目中存在大量同名的 mod.rs 文件。
可见性
我们已经知道,模块默认是私有的。可以添加pub
关键字,使其变成公有的。模块上的pub
关键字只允许其父模块引用它,而不允许访问内部代码。因为模块是一个容器,只是将模块变为公有能做的其实并不太多;同时需要更深入地选择将一个或多个项变为公有。
结构体和枚举的可见性
结构体和枚举的字段的可见性完全不同:
- 将结构体设置为
pub
,但它的所有字段依然是私有的 - 将枚举设置为
pub
,它的所有字段也将对外可见
原因在于,枚举和结构体的使用方式不一样。如果枚举的成员对外不可见,那该枚举将一点用都没有,因此枚举成员的可见性自动跟枚举可见性保持一致,这样可以简化用户的使用。
而结构体的应用场景比较复杂,其中的字段可能一部分在这里被使用,以部分在那里被使用,因此无法确定成员的可见性,那索性就设置为全部不可见,将选择权交给程序员。
use 与 pub
在 Rust 中,可以使用use
关键字把路径提前引入到当前作用域中,随后的调用就可以省略该路径,极大地简化了代码。
基本方式
引入模块中的项有两种方式:绝对路径和相对路径。也可以选择引入模块或者函数。
// 绝对路径 + 引入模块
use crate hosting;
// 相对路径 + 引入函数
use from_waitlist;
从使用简洁性来说,引入函数自然是更甚一筹,但是在某些时候,引入模块会更好:
- 需要引入同一个模块的多个函数
- 作用域中存在同名函数
一般建议优先使用最细粒度(引入函数、结构体等)的引用方式,如果引起了某种麻烦(例如前面两种情况),再使用引入模块的方式。
避免同名引用
我们只要保证同一个模块中不存在同名项就行,模块之间、包之间可以存在同名。引入时避免同名的方式有:
- 模块::函数,即通过父模块来调用
use fmt;
use io;
- 别名:使用
as
关键字赋予引入项一个全新的名称
use Result;
use Result as IoResult;
引入项导出
当外部的模块项 A 被引入到当前模块中时,它的可见性自动被设置为私有的。使用pub use
可实现允许其它外部代码引用我们的模块项。
pub use crate hosting;
这常用于统一使用一个模块来提供对外的 API的场景。此时可以引入其它模块中的 API,然后进行再导出,最终对于用户来说,所有的 API 都是由一个模块统一提供的。
第三方包
修改 Cargo.toml 文件,在[dependencies]
区域添加一行:{包名} = "{版本号}"。等下载完成后,使用use
即可添加第三方库。
第三方包都可在 crates.io 和 lib.rs 中下载和查找。
简化引入
可以使用{}
来一起引入具有相同前缀的模块,大量减少use
的使用。
use HashMap;
use BTreeMap;
use HashSet;
use io;
use Write;
上面五行可以压缩到两行。注意这个 self,可用来替代模块自身
use ;
use ;
全部引入
还可以使用
use *;
引入一个模块中的所有公共项。此时,由于我们根本无法判断引入了哪些东西,容易引发冲突,一般不建议这么使用,但是可以用于快速编写测试代码。
受限可见性
Rust 还提供了受限可见性,即可以控制模块中的公开内容能被哪些人看见。
pub(crate)
或 pub(in crate::a)
就是限制可见性语法,前者是限制在整个包内可见,后者是通过绝对路径,限制在包内的某个模块内可见。
pub 总结
pub
意味着可见性无任何限制pub(crate)
表示在当前包可见pub(self)
在当前模块可见pub(super)
在父模块可见pub(in <path>)
表示在某个路径代表的模块中可见,其中 path 必须是父模块或者祖先模块
Cargo
在 Rust 的使用过程中免不了要使用 Cargo。下面的内容都很简单,不再详细解释了。
# 上两行可以归一:
为何会有 Cargo
如果直接使用 rustc 对二进制包进行编译,生成二进制可执行文件这个方式,虽然简单,但是必须要指定文件名编译,当项目复杂后,这种编译方式也随之更加复杂。此外,如果要指定编译参数,情况将更加复杂。更不好处理的是,一旦要引入第三方的依赖包,这种方式会变得难以管理。因此就有了 Cargo。
Cargo 为了实现目标,做了四件事:
- 引入两个元数据文件,包含项目的方方面面信息: Cargo.toml 和 Cargo.lock
- 获取和构建项目的依赖,例如 Cargo.toml 中的依赖包版本描述,以及从 crates.io 下载包
- 调用 rustc(或其它编译器)并使用的正确的参数来构建项目,例如 cargo build
- 引入一些惯例,让项目的使用更加简单
使用 Cargo 构建项目
对于任何开源的 Rust 项目,cargo build
能够搞定一切构建,而无需关心背后复杂的配置。
指定依赖项
Cargo 默认会从 crates.io 下载依赖。这是 Rust 社区维护的中心化注册服务,用户可以在其中寻找和下载所需的包。在Cargo.toml 文件的[dependencies]
部分添加目标包名和版本号即可。
[]
= "0.1.12"
实际上,还有从本地路径引用的方式。
这节我真写不下去了,感觉没什么写的必要,就这样了吧
📝 系列导航
- 上一篇: Rust 进阶学习笔记(四):宏
- 下一篇: Rust 进阶学习笔记(六):Unsafe
- 合集列表