Wang's blog

十八、代码组织

Published on

Rust提供了一系列特性用于管理代码的组织结构,包括:

  • Package(包):Cargo提供的可以建立、测试与分享箱的结构
  • Crate(箱):模块组成的树,构成一个库或可执行文件
  • Module(模块)与use(引入):用于控制代码结构、作用域与路径的隐私性
  • Path(路径):命名结构体、函数或模块的方式

Crate(箱)

箱是编译器每次编译的最小单位。箱分为两类:

  • 二进制箱:是可以编译为可执行文件的程序,包含main函数
  • 库箱:不包含main函数,用于分享功能

每个箱从一个根文件开始编译,可能包含多个文件。

Package(包)

包是包管理器Cargo提供的结构,它包含一个配置文件Cargo.toml。一个包可以包含多个箱,但是最多有一个库箱。默认情况下,src/main.rs是二进制箱的根文件,src/lib.rs是库箱的根文件。

Module(模块)

使用模块可以对代码进行组织以提高可读性与可用性,每个箱都由一组树状的模块组成。

使用以下命令建立一个库:

cargo new restaurant --lib

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

以上代码形成的模块树如下,其中根自动命名为crate:

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

Path(路径)

使用路径在模块树中引用项目

路径有两种形式:

  • 绝对路径:从根开始的路径。对于箱外部代码,绝对路径以箱名开始;对于箱内部代码,绝对路径以crate开始
  • 相对路径:从当前模块开始,使用self, super或当前模块的表达式
// 此处仅供示意,不能通过编译
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关键字暴露路径

上面的例子不能通过编译,因为模块hosting与函数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作为相对路径的开始

通过使用super,可以使相对路径从父模块开始。

fn deliver_order() {}

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

    fn cook_order() {}
}

将结构体与枚举设为公有

使用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 fn eat_at_restaurant() {
    // 可以使用公有结构体Breakfast的公有关联函数
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // 可以访问公有字段toast
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // 错误,不能访问私有字段seasonal_fruit
    meal.seasonal_fruit = String::from("blueberries");
}

相对的,使用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关键字将路径加入作用域

使用use关键字可以将一个路径加入当前作用域,从而简化路径的使用,此时仍然会检查隐私性。

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

// 将hosting加入当前作用域
use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    // 此时可以直接使用hosting
    hosting::add_to_waitlist();
}

注意use只能将路径加入到其所在的作用域中,其它作用域中无效:

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

// 将hosting加入当前作用域
use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        // 错误,不能使用hosting,因为目前在customer模块作用域中
        hosting::add_to_waitlist();
    }
}

use的推荐用法

导入函数、结构体或枚举时,有两种方式:

// 导入上层模块,通过模块名使用函数
use crate::front_of_house::hosting;
hosting::add_to_waitlist();

// 直接导入函数并使用
use crate::front_of_house::hosting::add_to_waitlist;
add_to_waitlist();

推荐使用第一种方式,这样不会产生冲突。

use std::fmt;
use std::io;

// 使用两个不同的Result,不会产生冲突
fn function1() -> fmt::Result {
}

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

使用as设置别名

在use后可以使用as设置别名,同样可以避免冲突:

use std::fmt::Result;
// 使用别名
use std::io::Result as IoResult;

fn function1() -> Result {
}

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

使用pub use重导出名称

使用use时,导入的名称只在当前作用域内有效,为了使调用此代码的代码也能访问该名称,可以使用pub use重导出。重导出可以简化对外接口结构,方便用户调用。

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

// 外部可以直接使用restaurant::hosting
pub use crate::front_of_house::hosting;

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

使用外部包

要使用外部包,首先在Cargo.toml中添加包信息:

rand = "0.8.5"

之后可以在代码中使用该包,或用use导入其中的项目:

use rand::Rng;

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

使用嵌套路径简化大型use列表

使用同一个箱中的多个项目时,可以将多行use简化:

// std库的两个use语句
use std::cmp::Ordering;
use std::io;

// 可简化为一行
use std::{cmp::Ordering, io};

// 当一个项目是另一个的子项目时
use std::io;
use std::io::Write;

// 可简化为
use std::io::{self, Write};

使用通配符

use语句中可以使用*表示导入所有项目,但是这样容易引起冲突,需要小心使用。

use std::collections::*;

将模块树放入多个文件

一个箱至少有一个根文件(一般为src/lib.rs或src/main.rs),所有模块可以全部放在此文件中。但是如果模块树过大,也可以将它们分为多个文件。

在根文件src/lib.rs中,可以只定义模块,不包含实现:

mod front_of_house;

编译器会在src/front_of_house.rs文件中寻找front_of_house模块的实现。该模块也可以只包含子模块的声明:

pub mod hosting;

编译器会在src/front_of_house/hosting.rs文件中寻找hosting模块的实现:

pub fn add_to_waitlist() {}

Workspace(工作空间)

一个包中只能有一个库箱,对于更大的项目,可能希望将代码划分为多个库箱。为此,Cargo提供了工作空间,它可以包含多个包,并保证它们具有相同的Cargo.lock文件,即保证它们使用的外部包具有相同版本。

要使用工作空间,首先建立文件夹:

mkdir add
cd add

之后在该目录下建立整个工作空间的配置文件Cargo.toml,在members字段添加要加入工作空间的包名,并使用cargo new创建这些包:

[workspace]

members = [
    "adder",
    "add_one",
]

此时工作空间目录结构如下,整个工作空间只有一个Cargo.lock文件:

├── Cargo.lock
├── Cargo.toml
├── add_one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

如果要在adder包中使用add_one包,可以在adder/Cargo.toml进行如下配置:

[dependencies]
add_one = { path = "../add_one" }

将箱发布至Crates.io

添加文档注释

一些特殊注释会被cargo doc自动生成文档。

  • ///后的文档支持Markdown格式,将其放在需要生成文档的项目前即可。其中的代码块会自动生成测试
  • //!对包含注释的项目进行说明

使用pub use导出方便的公用API

pub use会自动生成对应文档。

登录Crates.io账号

使用cargo login登录账号。

添加meta信息

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"

[dependencies]

发布至Crates.io

使用cargo publish命令发布。

发布新版本

修改版本号并重新使用cargo publish发布。

弃用版本

使用cargo yank阻止新项目使用一个版本,老项目不受影响。

其它

自定义发行版编译配置

在Cargo.toml的[profile.*]小节中进行自定义配置。可使用的全部配置在这里

使用cargo install安装可执行文件

crates.io上也有可执行箱,可以使用cargo install安装。默认安装在$HOME/.cargo/bin中,将其加入环境变量$PATH即可使用。

扩展cargo命令

将名为cargo-something的命令加入环境变量$PATH中,即可通过cargo something调用。