很少有哪个应用在编写的时候是仅可以依靠一个文件来实现的,也很少有哪个应用可以在不借助其他任何外部库仅凭语言的自身语法和特性实现的。所以如何组织外部依赖库和自身功能组成文件,其实算是使用一门语言的基础。Rust中对于分散的文件是通过模块组织的,但是Rust模块的概念又与其他常见的语言略有不同。
包
Rust的应用是由包组成的,为每一个包都是一个Rust项目。我们所编写的应用也是一个包。包在Rust中的标识词汇是crate
。我们在创建Rust应用的时候所使用的cargo new
命令创建的实际上就是一个包。
在应用中使用cargo add
安装的,在Cargo.toml
中列出的应用的依赖,也是一个个的包。所以对于一个应用来说,除了它自己以外,任何出现的包都是外部包。对于外部包的引用,除了需要在Cargo.toml
中声明依赖以外,还需要在应用的主程序文件中使用extern crate
进行声明。
cargo new --bin
创建的可以编译为可执行二进制文件的应用来说,主程序文件是main.rs
。对于使用cargo new --lib
创建的库来说,主程序文件是lib.rs
。
例如在我们的应用中使用了serde
、once_cell
等依赖,那么在main.rs
的开头部分就如同下面示例这样来书写。
|
|
引用的外部依赖库只需要在main.rs
中声明引用即可,不需要在应用的其他文件和模块中再次声明引用。
extern crate
声明应用所需要使用的外部依赖库,Rust编译器知道应该去哪里寻找所需要的依赖。
Cargo.toml
中声明的内容主要是所使用依赖库的名称、版本和启用的特性,但是在Cargo.toml
中列出的依赖库并不一定要在应用中使用。
std
和core
库的区别
std
代表的就是Rust的标准库,我们在Rust Standard Library Docs上检索的时候,经常还可以发现与标准库中的内容存在同名的内容,但是是放置在core
库中的。那么std
和core
这两个库有什么区别呢?
首先要了解的是Rust语言除了可以用来开发一般PC或者服务器上使用的应用以外,还可以用来开发嵌入式设备应用。但是有很多嵌入式设备因为受到容量的限制,不可能集成功能繁多且比较庞大的标准库进去,只能集成Rust内置的核心库。core
就是Rust的核心库,它实际上是std
标准库的子集,而且在std
标准库中,所有使用到core
库中的内容,实际上都是直接重新导出的,所以std
库中与core
库中同名的内容实际上就是同一个内容。
综上所述,core
库就是std
库的子集,其中同名的内容都是相同的内容,平时可以随意选择使用。
模块
模块(module)可以认为是Rust中相当于其他语言中的命名空间。与包不同的是,包解决的是项目间的代码组织和依赖的问题,模块主要解决的应用项目中代码组织的问题。模块在Rust中的标识词汇是mod
,模块是Rust中函数、类型、常量等内容的容器。
在应用项目中,模块有三种书写方法。
在同一个文件中编写模块
在统一个文件中定义多个模块是Rust中最基础的模块定义方法,也是使用其他模块定义方法的基础。这种模块的定义语法与声明结构体、枚举等数据结构的形式非常相似。
|
|
pub
关键字的使用。
但是在任何一个应用中,都不可能在一个文件中完成所有代码的编写,所以这种模块定义格式只是一个语法基础,其实用意义并不大。
在独立文件中编写模块
上一节中的模块定义还可以被简写为mod 模块名称;
,这样一来,模块的定义就变成了模块声明。模块的实际定义就可以被转移到一个独立的文件中了。Rust对于这种模块的定义方法,是对独立的模块定义文件的文件名是有要求的,必须与mod 模块名称;
声明语句中的“模块名称”一致,例如mod service;
声明就必须在同级目录中存在一个名为service.rs
的文件作为service
模块的实际定义文件。
模块的实际定义文件中不需要再包含mod
语句来声明自己的模块名称。如果再次使用了mod
来声明模块,那么这个声明的模块将作为当前模块的子模块存在。
但是一般不会把所有的模块都放置在同一级目录中,因为这样形成的代码组织结构非常难以管理。
把目录作为模块
上一节中在独立的文件里定义模块的方法适用于模块不存在子模块的情形,当一个模块拥有子模块的时候,使用目录来组织子模块代码是一种更好的选择。如果要将一个目录定义为模块,需要在这个目录下创建mod.rs
文件,并且这个目录的名称需要在上一级主程序文件或者mod.rs
中使用mod
关键字声明,换句话说就是目录的名称就是模块的名称。
Rust对于模块的检索是存在两种策略的,在遇到mod 模块名称;
定义的时候,会首先去检查是否存在模块名称.rs
的模块定义文件,然后再去检查是否存在以模块名称
命名的目录以及在这个目录下是否存在mod.rs
文件。
如果这两种情况都存在或者都不存在,那么Rust将会报编译错误。
所以根据Rust对于模块的检索方法,建议是采用在主程序文件或者每一级模块的mod.rs
文件中,只声明本级模块所拥有的子模块,如果被声明的模块没有子模块,那么就直接采用独立文件的形式进行定义,如果存在子模块,那么就采用目录的形式定义。
模块的引用和导入
在代码中对于应用内定义的模块和依赖库中的模块都可以采用绝对路径来访问,与操作系统中的分隔符不同,Rust中不同级别模块之间的分隔符是::
,例如常用的访问标准库的语法use std::collections::HashMap;
。但是需要注意的是,如果一个模块的声明没有使用pub
关键字,那么这个模块就不能够使用绝对路径被访问到。
在默认情况下,对于模块中的所有内容的访问在必须使用绝对路径来访问,但是这个路径通常较长,所以Rust引入了使用use
关键字导入模块中内容的方法。使用use
以后,被引入的目标内容就会变成当前模块的一部分,所以也就不需要使用绝对路径来访问了。如果需要引入一个模块中的多个内容,不需要使用多条use
语句,只需要使用{}
列举被引用的内容即可,例如use std::collections::{HashMap, HashSet};
。
在父模块中使用子模块的内容
在Rust中不管是哪一级的模块相互之间都是独立的,父模块不能直接访问子模块中的任何内容。如果需要访问子模块的内容,也必须在父模块中式use
语句导入,但是可以不使用绝对路径来导入。
在父模块中导入子模块的内容可以使用use self::child::Content;
的形式通过self
来实现基于相对路径的导入。
self
是当前模块的别名,在使用的时候注意它在不同的位置会有不同的含义。
在子模块中使用父模块的内容
子模块也不会自动继承父模块中的内容,如果需要使用父模块中定义的内容也必须导入。子模块导入父模块中的内容同样也可以使用相对路径来完成。从子模块中使用相对路径访问父模块使用的是super
别名,例如use super::Content;
。
super
和self
两个别名相似功能的别名还有crate
,crate
用来表示当前应用的根模块,或者叫包的根。
在子模块中访问兄弟模块的内容
通过以前两节的内容,那么从一个子模块中访问兄弟模块就可以使用use super::sibling::Content;
的形式。
Prelude
在使用Rust的一些依赖库的时候,尤其是std
标准库,经常会看到一个名词——prelude
。prelude
是一个前置模块,其中定义的是一些常用类型和特征的引用。这种前置模块会在声明引用库的时候自动导入其中定义的内容。
例如std
标准库就被自动链接到了每一个项目,所以std
定义的prelude
内容例如Vec
、Result
之类的内容就会被自动的导入。以下是std
的prelude
自动导入的一些常用内容。
std::marker::{Copy, Send, Sized, Sync, Unpin}
std::ops::{Drop, Fn, FnMut, FnOnce}
std::mem::drop
std::boxed::Box
std::borrow::ToOwned
std::clone::Clone
std::cmp::{PartialEq, PartialOrd, Eq, Ord}
std::convert::{AsRef, AsMut, Into, From}
std::default::Default
std::iter::{Iterator, Extend, IntoInterator, DoubleEndedIterator, ExactSizeIterator}
std::option::Option::{self, Some, None}
,这里使用self
引入了Option
。std::result::Result::{self, Ok, Err}
,这里使用self
引入了Result
。std::string::{String, ToString}
std::vec::Vec
std::convert::{TryFrom, TryInto}
(需要Rust 2021版本)std::iter::FromIterator
(需要Rust 2021版本)