序
前面我们学习了rust的基础类型以及结构体,枚举类型,以及函数,现在我们已经基本了解了rust的基础部分,有一定的代码组织能力,对于程序来说,随着项目越来越大,都需要模块化管理,提高代码可读性。
在javascript中,我们早期有多种模块化规范,当然现在都推崇ES Module
。rust也有自己的模块管理方式。
重要概念
- 包(Packages):Cargo 的一个功能,它允许你构建、测试和分享 crate。
- Crates :一个模块的树形结构,它形成了库或二进制项目。
- 模块(Modules)和 use:允许你控制作用域和路径的私有性。
- 路径(path):一个命名例如结构体、函数或模块等项的方式
包和 Crate
crate 是 Rust 在编译时最小的代码单位。crate 有两种形式:二进制项和库。
-
二进制项 可以被编译为可执行程序,比如一个命令行程序或者一个服务器。
-
库 它们也不会编译为可执行程序,它们提供一些诸如函数之类的东西,使其他项目也能使用这些东西。
多数时间 我们说的 crate 指的都是库。
这里有一个rust约定,二进制项的入口文件main.rs
,库
的入口文件是lib.rs
,这是不可以更改的。
包(package)是提供一系列功能的一个或者多个 crate。一个包会包含一个 Cargo.toml 文件,阐述如何去构建这些 crate。
- 包只能包含一个库 crate(library crate)。
- 包可以包含任意多个二进制 crate(binary crate),
- 但是必须至少包含一个 crate(无论是库的还是二进制的)。
- 一个包可以同时含有 src/main.rs 和 src/lib.rs,也就是同时有
库create
和二进制create
,且名字都与包相同
通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。
我们输入命令 cargo new
:就是创建了一个package,前面我们提到了传递参数可以默认创建不同的create。
这里我们可以传递
--bin
或者--lib
,第一个是创建二进制程序(默认),这是默认行为: 传递(--lib)创建一个库
作用域与私有性
我们来看一个文件目录结构:
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs
这个例子中的 crate 根文件是src/main.rs,该文件包括了:
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {:?}!", plant);
}
在这里我们pub mod garden;
相当于向外暴露了模块 gargen
,就像是typescript中export xxx from xxx
。
在模块中对相关代码进行分组
rust一个模块中的代码默认是私有的,我们定义一个模块,是以 mod
关键字为起始,可以嵌套。通过使用模块,我们可以将相关的定义分组到一起。自由组织结构,就像我们typescript中导出一个对象一样,我们导出不同的对象。
// 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() {}
}
}
引用模块项目的路径
Rust 如何在模块树中找到一个项的位置,在javascript中我们直接使用文件路径即可,rust也差不多但存在一点点区别。
绝对路径和相对路径都后跟一个或多个由双冒号(::
)分割的标识符。而不是/
路径有两种形式:
- 绝对路径(absolute path)是以 crate 根(root)开头的全路径;对于外部 crate 的代码,是以 crate 名开头的绝对路径,对于对于当前 crate 的代码,则以字面值
crate
开头。 - 相对路径(relative path)从当前模块开始,以
self
、super
或当前模块的标识符开头。
rust使用 pub
关键字来标记,就像typescript 使用export
向外暴露一样。
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();
}
我们这里使用了pub
关键字,向外暴露的 eat_at_restaurant
,编译器会抛出错误。
这里hosting
模块是私有的。换句话说,我们拥有 hosting
模块和 add_to_waitlist
函数的正确路径,但是 Rust 不让我们使用,因为它不能访问私有片段。
在 Rust 中,默认所有项(函数、方法、结构体、枚举、模块和常量)对父模块都是私有的。
父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。
在rust中同级作用域可以直接使用,这里front_of_house
与eat_at_restaurant
我们就可以直接使用。
使用 pub 关键字暴露路径
刚才我们说了pub
关键字 向外暴露,模块之间也是一样。
pub
是public
的缩写
mod front_of_house {
pub 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();
}
不过到这里,rust编译器还是会抛出错误,因为add_to_waitlist
仍然是私有的
模块公有并不使其内容也是公有的,所以我们需要在函数前也加上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();
}
使用 super 起始的相对路径
在javascript中我们使用相对路径时可以使用..
来回到上级目录。rust这里使用super
来处理。super
允许我们引用已知的父模块中的项。
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
创建公有的结构体和枚举
pub
来设计公有的结构体和枚举,不过关于在结构体和枚举上使用 pub
还有一些额外的细节需要注意。如果我们在一个结构体定义的前面使用了 pub
,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
}
这里对外部来说toast
是可见的,seasonal_fruit
是不可见的。
与之相反,如果我们将枚举设为公有,则它的所有成员都将变为公有。我们只需要在 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 关键字将路径引入作用域
上面我们使用相对路径与绝对路径来调用模块,这对于长路径模块来说,无疑是一种折磨,
所以rust提供了 use
关键字创建一个短路径,然后就可以在作用域中的任何地方使用这个更短的名字。
这里其实就相当于在这个作用域给模块取了一个别名, 通过 use
引入作用域的路径也会检查私有性。
在作用域中增加
use
和路径类似于在文件系统中创建软连接(符号连接,symbolic link)
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
所在的特定作用域内的短路径。
eat_at_restaurant
函数移动到了一个叫 customer
的子模块,这又是一个不同于 use
语句的作用域,所以函数体不能编译。
这里我们可以使用super::hosting
来处理这个问题
习惯用法
- rust 使用use关键字引入函数,一般值引入到父模块,方便看出函数的出处
- 引用结构体或者枚举类型,就需要引入到本身
使用 as 关键字提供新的名称
不能使用 use
语句将两个具有相同名称的项带入作用域,这样和javascript也是一样的,同名冲突。两种解决办法:
-
我们指定到父级,做区分.如:
use std::fmt::Result
和use std::io::Result
,我们可以引用到use std::fmt
和use std::io
就可以了 -
我们使用
as
指定一个新的本地名称或者别名。这里和typescript也是一样的。只是写法上有点区别
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
使用 pub use 重导出名称
使用 use
关键字,将某个名称导入当前作用域后,这个名称在此作用域中就可以使用了,但它对此作用域之外还是私有的。
use 我们也可以加上pub
关键字,向外导出,就像typescript中的export xxx from xxx
一样
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();
}
使用外部包
就像我们前端的package.json 一样我们可以定义外部依赖项一样,rust在 Cargo.toml中定义
我们可以在[dependencies]
下声明依赖。
[dependencies]
rand = "0.8.5"
在 Cargo.toml 中加入 rand
依赖告诉了 Cargo 要从 crates.io 下载 rand
和其依赖,并使其可在项目代码中使用。
将其引入项目中:
- 在 Cargo.toml 列出它们并通过
use
将其中定义的项引入项目包的作用域中。
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
就像我们前端可以将自己写好的包上传到npm 一样。 crates.io 上也有很多 Rust 社区成员发布的包,
注意
std
标准库对于你的包来说也是外部 crate。因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml 来引入std
,不过需要通过use
将标准库中定义的项引入项目包的作用域中来引用它们,比如我们使用的HashMap
:
嵌套路径来消除大量的 use 行
当需要引入很多定义于相同包或相同模块的项时,为每一项单独列出一行会占用源码很大的空间。例如猜猜看章节示例 2-4 中有两行 use
语句都从 std
引入项到作用域:
use std::cmp::Ordering;
use std::io;
我们可以使用嵌套路径将相同的项在一行中引入作用域。这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分
use std::{cmp::Ordering, io};
我们可以在路径的任何层级使用嵌套路径
可以在嵌套路径中使用 self
use std::io;
use std::io::Write;
// 等价于
use std::io::{self, Write};
通过 glob 运算符将所有的公有定义引入作用域
如果希望将一个路径下 所有 公有项引入作用域,可以指定路径后跟 *
,glob 运算符:
use std::collections::*;
这个 use
语句将 std::collections
中定义的所有公有项引入当前作用域。使用 glob 运算符时请多加小心!Glob 会使得我们难以推导作用域中有什么名称和它们是在何处定义的。
结语
这一期我们学习rust的模块化,怎么暴露模块,我们引入别人的包,这些概念都是我们前端日常开发中用到的,不过我感觉ES Muudle
设计的更方便😄,rust的use
语法和javascript 的 import
语法相作用相似。
本文首发于 @阿乐去买菜,转载请署明出处