Rust快速入门(5) - 包 & Crate & 模块

随着项目规模的增长,组织代码变得越来越重要。本章介绍了Rust的模块系统,它允许开发者将代码拆分为多个文件,管理可见性,并控制作用域。

  • (*Packages*): Cargo 的一个功能,它允许你构建、测试和分享 crate。
  • Crates :一个模块的树形结构,它形成了库或二进制项目。
  • 模块(*Modules*)和 use: 允许你控制作用域和路径的私有性。
  • 路径(*path*):一个命名例如结构体、函数或模块等项的方式

1. 包和Crate

1.1 Crate基础

Crate是Rust编译器一次考虑的最小代码单位,分为两种形式:

  • 二进制crate:可编译为可执行文件,必须有main函数
  • 库crate:定义可共享的功能,没有main函数,不编译为可执行文件

crate根是编译器开始编译的源文件,也是crate的根模块:

  • 二进制crate:通常是src/main.rs
  • 库crate:通常是src/lib.rs

1.2 包的结构

包(Package)是一个或多个crate的集合,提供一组功能,包含一个Cargo.toml文件:

  • 一个包可以包含任意数量的二进制crate
  • 一个包最多只能包含一个库crate
  • 包必须至少包含一个crate(库或二进制)

创建包的例子:

$ cargo new my-project
$ ls my-project
Cargo.toml src
$ ls my-project/src
main.rs

Cargo约定:

  • src/main.rs是与包同名的二进制crate的crate根
  • src/lib.rs是与包同名的库crate的crate根
  • 包可以通过在src/bin目录放置文件来拥有多个二进制crate

2. 模块定义与作用域控制

2.1 模块基础

模块(module)用于将代码组织成逻辑单元,控制项目的可见性(公有或私有)。

模块速查表:

  1. 从crate根开始:编译器首先在crate根文件中查找代码

  2. 声明模块

:在crate根文件中,可以使用

   mod garden;

声明模块,编译器会在以下位置查找代码:

  • 内联,大括号中的代码:mod garden { ... }

  • 文件:src/garden.rs

  • 文件:src/garden/mod.rs(旧风格)

  • 声明子模块

:在非crate根文件中,可以声明子模块,编译器会在以下位置查找:

  • 内联,大括号中的代码

  • 文件:src/garden/vegetables.rs

  • 文件:src/garden/vegetables/mod.rs(旧风格)

  • 模块中代码的路径:一旦模块是crate的一部分,可以使用路径引用其代码

  • 私有与公有:默认情况下,模块中的代码对父模块是私有的,使用pub关键字使其公有

  • use关键字:创建简短路径的快捷方式,减少重复的长路径

2.2 模块树示例

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() {}
    }
}

模块树表现为:

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

3. 路径与可见性

3.1 引用模块树中的项目

路径有两种形式:

  • 绝对路径:从crate根开始,以crate关键字开头
  • 相对路径:从当前模块开始,使用selfsuper或当前模块中的标识符
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();

// 相对路径
front_of_house::hosting::add_to_waitlist();

3.2 使用pub关键字公开路径

默认情况下,Rust中的所有项目对父模块都是私有的。要使项目可见,需要使用pub关键字:

mod front_of_house {
    pub mod hosting {  // 公有模块
        pub fn add_to_waitlist() {}  // 公有函数
    }
}

pub fn eat_at_restaurant() {
    // 可以访问,因为hosting是公有的,add_to_waitlist也是公有的
    front_of_house::hosting::add_to_waitlist();
}

3.3 使用super开始相对路径

可以使用super关键字构建以父模块开始的相对路径:

fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order(); // 使用super访问父模块中的函数
    }

    fn cook_order() {}
}

3.4 公有结构体和枚举

结构体

  • 使用pub标记结构体时,结构体是公有的,但字段仍然是私有的
  • 需要单独为每个字段添加pub使其公有
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标记枚举时,所有变体都是公有的
  • 枚举变体默认是公有的
mod back_of_house {
    pub enum Appetizer {
        Soup,     // 自动公有
        Salad,    // 自动公有
    }
}

4. 使用use关键字引入路径

4.1 基本用法

use关键字可以将路径引入作用域,简化代码:

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();
}

4.2 惯用的use路径

  • 引入函数时,惯用做法是引入父模块而不是函数本身:
  use crate::front_of_house::hosting;
  hosting::add_to_waitlist();
  • 引入结构体、枚举等项目时,惯用做法是指定完整路径:
  use std::collections::HashMap;
  let mut map = HashMap::new();

4.3 解决名称冲突

当引入同名项目时有几种解决方案:

  1. 使用父模块区分:
   use std::fmt;
   use std::io;
   
   fn f1() -> fmt::Result { ... }
   fn f2() -> io::Result<()> { ... }
  1. 使用as关键字重命名:
   use std::fmt::Result;
   use std::io::Result as IoResult;
   
   fn f1() -> Result { ... }
   fn f2() -> IoResult<()> { ... }

4.4 重新导出名称 (pub use)

使用pub use可以重新导出名称,使外部代码也能引用这个名称:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

这样,外部代码可以使用restaurant::hosting::add_to_waitlist()而不是restaurant::front_of_house::hosting::add_to_waitlist()

4.5 嵌套路径

当从同一crate或模块引入多个项目时,可以使用嵌套路径减少代码量:

// 不使用嵌套路径
use std::cmp::Ordering;
use std::io;

// 使用嵌套路径
use std::{cmp::Ordering, io};

如果路径有共同部分:

// 不使用嵌套路径
use std::io;
use std::io::Write;

// 使用嵌套路径
use std::io::{self, Write};

4.6 通配符

使用*可以引入路径下的所有公有项目:

use std::collections::*;

通配符应谨慎使用,因为它会使作用域中的名称来源变得不清晰。

5. 将模块分割到不同文件

随着模块增大,可以将它们移到单独的文件中:

  1. 在crate根文件中声明模块:
   // src/lib.rs
   mod front_of_house;
   
   pub use crate::front_of_house::hosting;
   
   pub fn eat_at_restaurant() {
       hosting::add_to_waitlist();
   }
  1. 创建对应的文件:
   // src/front_of_house.rs
   pub mod hosting {
       pub fn add_to_waitlist() {}
   }

也可以进一步拆分子模块:

  1. 在父模块文件中声明子模块:
   // src/front_of_house.rs
   pub mod hosting;
  1. 创建子模块文件:
   // src/front_of_house/hosting.rs
   pub fn add_to_waitlist() {}

5.1 替代文件路径样式

Rust支持两种文件路径样式:

  • 现代风格(推荐):
    • src/front_of_house.rs
    • src/front_of_house/hosting.rs
  • 旧风格(仍支持):
    • src/front_of_house/mod.rs
    • src/front_of_house/hosting/mod.rs

不推荐混合使用这两种风格,因为可能造成混淆。

总结

Rust的模块系统允许:

  • 将大型代码库拆分为多个文件和模块
  • 控制项目的可见性(公有/私有)
  • 使用路径引用模块树中的项目
  • 使用use创建简短的路径名
  • 使用pub use重新导出名称

合理使用这些特性,可以创建组织良好、易于理解和维护的代码库。


好好学习,天天向上