Rust中组织代码的方式即称为代码组织,它定义了对于外部调用者而言,哪些部分是私有的(private),哪些部分是公有的(public),作用域内哪些部分是有效的。在Rust中,使用模块系统将这些东西组合起来。
1 Package、Crate 和 Module
模块系统主要可以分为以下三个部分:
- Package(包):一个Cargo特性,使开发者可以构建、测试和共享Crate;
- Crate(单元包):一个模块(Module)树,可以产生库文件(Library Crate)或者可执行文件(Binary Crate);
- Module(模块):让你控制代码的组织、作用域和私有路径,相当于基本的代码块单元;
1.1 Package 包
可以将一个工程理解为一个Package,它包含一个Cargo.toml
文件,并根据这个文件构建多个Crate。一个Package只能有0-1个Library Crate,可以拥有多个Binary Crate,但必须至少有一个Crate。
1.2 Crate 单元包
Crate只有两种类型,即库(Library Crate)或者可执行文件(Binary Crate)。我们把一个源代码文件称为Crate Root,编译器从这里开始,组成Crate的根Module。按照一般惯例(Cargo默认,不在配置文件中体现),src/main.rs
是Binary Crate的Crate Root,这个Crate的名字和Package的名字相同;src/lib.rs
是Library Crate的Crate Root,这个Crate的名字和Package的名字也是相同的。Cargo会把Crate Root文件交给rustc编译器来构建Crate。使用Crate可以将相关功能组合到一个作用域内,便于在项目间进行共享,并且防止冲突。
当一个Package下有多个Binary Crate时,就需要将这些文件放在
src/bin
目录下,每个文件都是单独的Crate。
1.3 Module 模块
定义Module可以控制代码(或者条目,item)的作用域和私有性。在一个Crate内使用Module对代码进行分组,增强可读性并使其易于维护。Module是可以嵌套的。Module内可以包含struct、function、enum、trait、常量等定义。下面是一个定义Module的例子:在src/lib.rs
文件中定义了一个Module的树形结构。
mod front_of_house {
mod hosting {
fn add_to_waitlists() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
2 路径和私有边界
2.1 Path 路径
在Module代码块中,我们使用Path(路径)作为struct、function或者module的一种命名方式。这里的路径不是文件路径,而是表示代码的组织关系。路径分为绝对路径和相对路径。路径分隔符为::
。
- 绝对路径:从Crate的名字或者
crate
关键字开始; - 相对路径:从自身开始,使用
super
或者self
或者模块标识符表示。
默认情况下,Rust中的条目(struct、function等)都是私有的,使用pub
关键字将某条目标记为公共的。父级模块无法访问子模块的私有条目,但是子模块可以访问所有祖先模块的所有条目。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlists() {}
fn seat_at_table() {}
}
}
fn eat_at_restaurant() {
front_of_house::hosting::add_to_waitlists(); // 相对路径
crate::front_of_house::hosting::add_to_waitlists(); // 绝对路径
}
在上面的例子中,front_of_house
模块和eat_at_restaurant
函数都是根节点crate
下面的条目,所以他们可以互相访问。同级别条目可以互相访问,不受私有边界(Private Boundary)的限制。
使用super
关键字可以访问上级目录的条目:
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
super::serve_order();
cook_order();
}
fn cook_order() {}
}
2.2 pub struct 和 pub enum
可以使用pub关键字将struct或者enum声明为公共的(默认私有),但是二者的不同点在于:struct成员默认私有,除非将其声明为公共;而enum成员默认共有(默认私有会使enum失去功能)。
下面是一个使用pub struct的例子:
mod back_of_house {
pub struct Breakfast {
pub toast: String,
season_of_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
season_of_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);
}
2.3 use 关键字
使用use关键字可以将制定条目导入到作用域内,引用时仍然需要遵守私有性规则。引用时可以使用绝对路径,也可以使用相对路径。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting; // 绝对路径,相当于将hosting模块置于根crate下
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
如果要使用某个函数,一般引用到函数的父级模块,来提高代码的可读性;但是对于struct或者enum等其他条目,可以直接引用到它本身。如果要同时使用同一个父级模块的不同模块,可以使用as
关键字进行简写:
use std::fmt::Result;
use std::io::Result as IoResult;
use
关键字引入的条目仅对当前代码作用域有效,它默认也是私有的,如果外部代码也想访问本文件引入的条目,那么就需要使用pub use
关键字对条目进行重导出。
可以使用嵌套路径对同一个父级模块的多个子条目进行引入:
use std::io::{self, Write, Result}; // 使用self引入本身
use std::collections::*; // 使用*通配符引入所有子条目(通常在测试和预导入时使用)
3 将模块内容移动到其他文件
当定义模块时,如果模块后的名字是;
而不是代码块,那么Rust就会从与模块同名的文件中加载内容,但是模块树的结构并不会发生变化。在使用这种方式导入其他文件中的内容时,需要注意模块的层级结构需要和文件系统的结构保持一致。
例如重构下面的代码:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
lib.rs
mod front_of_house;
use front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
front_of_house.rs
pub mod hosting;
front_of_house/hosting.rs
pub fn add_to_waitlist() {}