十八、代码组织
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调用。