使用Package,crate和Moudles管理日益增长的Rust程序

module system

  • Package: A Cargo feature that lets you build, test, and share crates.
  • Crates: A tree of modules that produces a library or executable
  • Modules and use: Let you control the organization, scope, and privacy of paths
  • Paths: A way of naming an item, such as a struct, function, or module

Package 和 Crates

crate

crate分为两种

  • binary crate :二进制
  • library crate :库

二进制的crate可以编译成可执行文件,必须包含main函数;而库的crate没有main函数也不被编译成可执行文件。它定义打算和多个项目共享的功能,大多数情况下rust开发者说的crate就是库的crate

包 Package

A package is a bundle of one or more crates that provides a set of functionality.

package包含一个描述如何构建这些crates的Cargo.toml文件, Cargo实际就是一个package,包含了构建代码的命令行工具的二进制的crate和其依赖的库 crate

package可以包含任意数量的二进制crate,但最多有一个库 crate。package必须包含至少一个crate,不论是二进制crate还是库 crate

#![allow(unused)]
fn main() {
$ cargo new my-project
     crated binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
}

执行cargo new之后,Cargo遵循惯例将src/main.rs作为二进制crate的根(root),若是src/lib.rs则是library crate的根,Cargo传递crate根到rustc以构建lib和可执行文件

定义模块(Modules)以控制作用域和隐私

模块快速参考

  • 从crate根开始: 当编译crate时,编译器会首先寻找crate根(通常是src/lib.rs或者src/main.rs)开始编译代码
  • 声明模块: crate根文件中可声明新模块,比如使用mod garden;声明"garden"模块。编译器会在以下位置寻找该模块代码
    • 在当前根文件中寻找mod garden{}
    • src/garden.rs中
    • src/garden/mod.rs中
  • 声明子模块: 在crate根文件之外,可以声明子模块。如在src/garden.rs中声明一个mod vegetables;。编译器会在以下位置中寻找
    • 在当前文件src/garden.rs中寻找mod vegetables{}
    • src/garden/vegetables.rs中
    • src/garden/vegetables/mod.rs中
  • 模块中的代码路径: 比如使用vegetable模块的Asparagus类型为crate::garden::vegetables::Asparagus
  • private和public: 模块中的代码默认是私有的。显式使用pub mod声明模块为公开。同样使用pub关键字声明公开模块的成员为公开
  • use: 作用域中可使用use关键字,可以创建捷径减少长路径的重复。如crate::garden::vegetables::Asparagus可使用use crate::garden::vegetables::Asparagus;,然后就可以直接使用Asparagus而不再需要前面那一长串路径
#![allow(unused)]
fn main() {
backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs
}

根文件是src/main.rs,其内容为

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("I'm growing {plant:?}!");
}

pub mod garden;告诉编译器在src/garden.rs中寻找garden模块的代码,其内容为

#![allow(unused)]
fn main() {
pub mod vegetables;
}

pub mod vegetables;表明src/garden/vegetables.rs也被包含。其内容如下

#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct Asparagus {}
}

引用模块树中项的路径

路径有两种

  • 绝对路径: 从crate根开始的全路径。外部crate从crate名开始,当前crate从crate开始
  • 相对路径: 从当前模块开始。使用self,super或当前模块的标识符

绝对和相对路径都使用::分隔

src/lib.rs

#![allow(unused)]
fn main() {
mod front_of_house {//这儿不需要pub是因为eat_at_restaurant是同一模块
    pub mod hosting { //使用pub声明模块,下面才能访问
        pub fn add_to_waitlist() {}//同样添加pub才能使用该函数
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}
}

rust中,所有项目包括函数,方法,结构体,枚举,模块和常量对于它们上级模块默认都是私有的

父模块不能使用子模块中的私有项,但子模块可以使用祖先模块中的项。使用pub公开子模块中的项即可访问

含二进制(binary)和库(libray)的package最佳实践

对于同时包含二进制和库crate的包,通常只在二进制crate中包含调用库crate足够启动可执行程序的代码。模块树应该定义在src/lib.rs中,这样二进制crate可以以包名开始的路径访问所有公开的项。二进制crate就变成了库crate的用户,就像其它完全外部的crate使用库crate一样:只能使用库crate公开的API。这有助于你设计更好的API

用super开始相对路径

#![allow(unused)]
fn main() {
fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order(); 
    }

    fn cook_order() {}
}
}

使用super可访问父模块中的项

公开结构体和枚举

默认情况下,结构体和成员都是私有的。可以使用pub关键字来公开

#![allow(unused)]
fn main() {
mod back_of_house {
    pub struct Breakfast {
        pub toast: String, 
        seasonal_fruit: String, //私有的
    }

    impl Breakfast {
        //构造夏季特供水果的函数
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed 因为seasonal_fruit是私有的
    // meal.seasonal_fruit = String::from("blueberries");
}
}

相反,只要枚举是公开的,其所有变体也会公开

#![allow(unused)]
fn main() {
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;
}
}

因为枚举变体若不公开的话枚举也没有意义,所以枚举变体默认是公开的。但结构体经常会使用私有的字段,所以除非显示添加pub,字段默认是私有的

use引入路径到作用域

无论使用相对还是绝对路径引入模块中的项都是冗长的,所有引入了use来简化流程,使用use创建一次捷径之后就可以使用更简洁的名字在作用域的任意位置

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting; //之后就可以直接使用hosting

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
}

注意use创建的捷径只对其使用时的作用域有效

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;
//mod customer和上面use是不同的作用域,编译会报错
//error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
mod customer {
    //use crate::front_of_house::hosting; 可在模块内引入
    pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();//或者使用super::
    }
}
}

创建惯用的use路径

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
}
}

虽然这段代码和之前的实现相同的任务,但直接用use引入到函数不是惯用的做法。因为显式的指定其父模块可表明其不是当前模块的函数也可以避免同名的冲突

另一方面使用use引入结构体,枚举和其它项时,指定全路径时惯用做法

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

这个习惯用法背后并没有什么强有力的理由: 它只是一个已经出现的约定,人们已经习惯了用这种方式读写 Rust 代码。

作用域出现同名的项时需要显式指定模块避免冲突

#![allow(unused)]
fn main() {
use std::fmt;
use std::io;

fn function1() -> fmt::Result { //重名时显式指定模块
    // --snip--
}

fn function2() -> io::Result<()> {
    // --snip--
}
}

使用as重命名

上面的例子可用as重命名解决

#![allow(unused)]
fn main() {
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
}

fn function2() -> IoResult<()> {
    // --snip--
}
}

使用pub use重新导出

当引入一个私有的模块时,为了使其可用,rust组合了pub use。这个技巧称为re-exporting,因为将该项引入到作用域,同时也让其他可以把该项引入他们的作用域

#![allow(unused)]
fn main() {
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();
}
}

使用外部包

比如引入rand包,需要先在Cargo.toml中添加依赖

#![allow(unused)]
fn main() {
rand = "0.8.5"
}

这将告知Cargo去 crates.io下载rand及其依赖到我们的项目中

use rand::Rng; //引入rand包

fn main() {
    let secret_number = rand::thread_rng().gen_range(1..=100);
}

注意std标准库虽然也是外部的crate,但rust中已经将其内置。所以不需要在Cargo.toml中添加,直接use即可

#![allow(unused)]
fn main() {
use std::collections::HashMap;
}

使用嵌套路径整理use列表

#![allow(unused)]
fn main() {
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
//可使用嵌套路径简化为
// --snip--
use std::{cmp::Ordering, io};
// --snip--
}
#![allow(unused)]
fn main() {
use std::io;
use std::io::Write;
//可以简化为
use std::io::{self, Write};
}

使用 glob *通配符

如果想引入所有公开项到作用域,可以使用*通配符

#![allow(unused)]
fn main() {
use std::collections::*;
}

分隔模块到不同文件

将之前的src/lib.rs中的front_of_house模块提取为一个单独的文件src/front_of_house.rs

src/lib.rs

#![allow(unused)]
fn main() {
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
}

src/front_of_house.rs

#![allow(unused)]
fn main() {
pub mod hosting {
    pub fn add_to_waitlist() {}
}
}

注意在模块树中只需要使用mod声明加载模块一次 接着继续提取hosting模块到src/front_of_house/hosting.rs 注意front_of_house.rs将变更为 src/front_of_house.rs

#![allow(unused)]
fn main() {
pub mod hosting;
}

src/front_of_house/hosting.rs

#![allow(unused)]
fn main() {
pub fn add_to_waitlist() {}
}

对于上面的例子中模块front_of_house编译器会在以下文件中寻找模块代码

  • src/front_of_house.rs (上面是这种)
  • src/front_of_house/mod.rs (older style, still supported path)

hosting模块一样

  • src/front_of_house/hosting.rs (上面是这种)
  • src/front_of_house/hosting/mod.rs (older style, still supported path)

同一模块中可以使用上面两种路径的任意一种,但混用就会引起编译错误。不同模块倒可以混用,但不推荐,会降低项目代码可读性。想想如果你使用第二种方式,很多文件都叫mod.rs,在编辑器中同时打开时够你受的

mod声明模块,编译器会去指定的位置寻找模块同名的文件

总结

  1. rust划分包package到不同的crate,crate又由不同的模块module构成
  2. 可使用相对或绝对路径引入其它模块,也可使用use简化路径
  3. 模块中代码默认都是私有,使用pub公开