这一篇主要介绍Rust项目应该如何组织代码~
在我看来代码组织对实际使用代码进行开发来说是非常重要的,但初学Rust的时候被模块系统的内容特别是Package和Crate的关系搞蒙了。后面结合像java的maven对项目的组织方式,从Cargo对项目的组织有一点新的理解。
mod
关键字和use
关键字:控制代码组织,作用域、私有路径。Cargo.toml
文件,并在其中描述如何构建这些crate。cargo new package-name
:创建package,主要内容包含配置文件Cargo.toml
,以及一个binary crate的入口文件src/main.rs
。cargo new --lib package-name
:创建package,主要内容包含配置文件Cargo.toml
,以及一个library crate的入口文件src/lib.rs
。|-package-name/
|-Cargo.toml
|-src/
|-main.rs
|-lib.rs
|-bin/
|-bin1.rs
|-bin2.rs
Cargo.toml
:使用cargo进行package内容管理的配置文件。src/main.rs
:binary crate的crate root,从main.rs
作为入口编译的crate的名字与package相同,编译出来的可执行文件名字与crate名字相同。src/lib.rs
:library crate的crate root,从lib.rs
作为入口的crate的名字与package相同。src/bin/xxx.rs
:binary crate的,从xxx.rs
作为入口编译的crate的名字,编译出来的可执行文件名为xxx
,其中xxx
是自定义的名字,src/bin/
下面可以有多个crate入口文件。Cargo.toml
里面可以进行非常多的配置(比如开发配置和生产配置,可执行文件名称等),无需把所有内容都了解了才能使用,遇到什么不会的再查文档就可以了。Cargo.toml
的默认配置:[package] # package的配置
name = "package-name" # package名字
version = "0.1.0" # 版本号
edition = "2021" # rust批次号
[dependencies] # 依赖第三方crate管理
rand = "0.5.5"
src/main.rs
、src/lib.rs
、src/xxx.rs
等crate root,在package根目录(Cargo.toml
所在目录)执行cargo命令:
cargo build
;src/lib.rs
作为入口的libary crate,执行carge build --lib
(因为一个package里面只能有一个libary crate,所以无需指定名字);src/main.rs
作为入口的binary crate,执行cargo build --bin package-name
;src/bin/xxx.rs
作为入口的binary crate,执行cargo build --bin xxx
;cargo run --bin package-name
或cargo run --bin xxx
;定义module来控制作用域和私有性。
module(模块):
建立module:
mod
关键字mod 模块名 {
// 模块内容
}
例子:
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn task_payment() {}
}
}
Module Tree(模块树):
src/main.rs
和src/lib.rs
为Crate Root,这两个文件(任意一个)的内容形成名为crate
的模块,位于整个模块树的根部。crate
模块上,即crate
模块为所有模块的祖先模块。Module Tree例子:
crate
└── front_of_house // 模块
├── hosting // 子模块
│ ├── add_to_waitlist // 子模块中的内容
│ └── seat_at_table
└── serving // 子模块
├── take_order // 子模块中的内容
├── serve_order
└── take_payment
为了在Rust的模块中找到某个项(条目),需要使用路径。
路径的两种形式:
crate
名或者字面值crate
开头。self
(当前模块)、super
(上一级模块)或当前模块的标识符开头。路径至少由一个标识符组成,标识符之间使用::
进行连接。
要使用绝对路径还是相对路径,取决于项目需要。
绝对路径与相对路径的例子:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn task_payment() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径访问
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径访问
front_of_house::hosting::add_to_waitlist();
}
fn main() {
}
super
关键字:用来访问父级模块路径中的内容,即在路径中super
代表的是当前模块的上一级模块。super
的使用例子:mod restaurant {
fn serve_order() {}
mod back_of_houst {
fn cook_order() {}
fn action() {
super::serve_order();
cook_order();
}
}
}
模块不仅可以组织代码,还可以定义私有边界。
如果想把函数、struct等项设置为私有,可以将它们放入到某个模块中。
rust中所有项(函数、方法、struct、enum、模块和常量)默认都是私有的。
pub
关键字标记它。父级模块无法访问子模块中的私有项(条目):因为私有性就是为了隐藏实现细节。
子模块里可以使用所有祖先模块中的公有和私有项(条目)。
个人理解:注意这里所说的A使用XX项,并不是说能使用XX及其内部的项,更准确地说,应该是能知道XX的存在,但是并不知道XX内部有什么存在,如果想要察觉到XX内部的YY的存在,就必须满足两个条件:1.XX对A是可见的(可能XX与A是同一级的兄弟项,也可能是XX向外暴露了自己的存在),2.YY是在XX中是向外暴露的。
如何向外暴露一个项的存在:使用pub
关键字修饰这个项。一个项公有的,但是其子项默认还是私有的。
也就是说一个项被知晓的程度既取决于自身是否是公有项,也取决于其父级项是否是公有项。
一个项知晓哪些项的存在:
self
super
crate
语法:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
front_of_house
是eat_at_restaurant
的兄弟项,所以front_of_house
没有暴露eat_at_restaurant
也能发现它。super
的使用主要是为了避免当一个项中有子项和兄弟项同名时无法定位的问题。
pub
关键字。pub
修饰字段使其变成公有的。mod back_of_house {
// Beakfast在back_of_house同级中可见
pub struct Breakfast {
pub toast: String, // toast字段外部可见
seasonal_fruit: String, // seasonal_fruit字段外部不可见
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_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);
// 因为seasonal_fruit字段不是公共的,所以在外部无法访问。
// meal.seasonal_fruit = String::from("blueberries");
}
pub
关键字。pub
关键字),因为如果enum的变体不能被访问就失去了它们的意义了。mod back_of_house {
// Apperizer被标记为pub,即使它的变体没有被标记为pub,也全是外部可见的
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let salad = back_of_house::Appetizer::Salad;
}
use
关键字将路径导入到作用域内。
use
引入路径时可以引入绝对路径,也可以引入相对路径。use
的习惯用法:
as
关键字可以为引入的路径指定本地的别名。use
支持的语法:use crate::xxx:yyy; // 使用yyy,yyy可以是各种项
use crate::xxx:*; // 引入xxx下所有内容
use crate::{xxx:yyy, zzz}; // 嵌套引入:一行里面引入多个同一棵模块子树下面的项
use crate::xxx:zzz as z3; // 起别名
pub use crate::xxx:yyy; // 重导出(re-exporting)
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
pub fn get_waitlist() {}
fn some_function() {}
}
}
mod restaurant {
// use crate::front_of_house::hosting;
use front_of_house::hosting;
// crate::front_of_house::hosting::add_to_waitlist();
use front_of_house::hosing::get_waitlist as gw
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
// hosting::some_function(); // 访问不到
gw();
}
}
pub use
导入路径。pub use
:重导出
Cargo.toml
添加依赖的包(package)。具体做法是在Cargo.toml
的dependencies
下加入依赖名 = "语义版本描述"
,比如:rand = "0.5.5"
。cargo会从https://crates.io/
中获取这个包和这个包的依赖项并下载到本地。use
将包中的项引入到需要用到的作用域中,use 依赖名::xxx
。std
是属于标准库,也会被当成外部包,但是它已经默认被引入了,不需要在Cargo.toml
中配置就可以直接使用。但如果要用里面的一些项,还是要引入,如:use std::collections::HashMap;
。$CARGO_HOME
中的.package-cache
删掉,然后从新拉取。foo
模块没有子模块,将foo
模块的代码放在foo.rs
文件中。foo
模块由子模块,有两种处理方式:
foo
模块的代码放在foo.rs
文件中,并将其子模块所在文件存放在foo/
目录。foo
模式的代码放在foo/mod.rs
中,并将其子模块所在文件存放在foo/
目录。(类似于python的模块结构)例子:
|-phrases_lib/
|-Cargo.toml # package配置文件
|-src/
|-lib.rs # library crate入口
| # 使用第一种方式管理模块
|-chinese.rs # > 该文件存放chinese模块的内容,并包含使用mod关键字挂载它的子模块的语句
|-chinese/ # > 该目录下存放chinese模块下属的子模块的内容
|-farewells.rs
|-greetings.rs
| # 使用第二种方式管理模块
|-english/ # > 该目录下存放english模块自身内容及子模块的内容
|-mod.rs # > 该目录下存放english模块自身的内容,并包含使用mod关键字挂载它的子模块的语句
|-farewells.rs # > 该文件存放english模块的子模块的内容
|-greetings.rs # > 该文件存放english模块的子模块的内容
mod
关键字和use
关键字的功能的区别:
mod
:用于声明模块之间的结构关系,在一个模块的源文件中使用mod
起到的作用是声明挂载子模块,当然如果子模块比较简单的时候也可以在模块中声明的同时定义子模块内部逻辑。use
:用于声明要引入其它模块的项的信息,引入一个项,从而可以在当前上下文中直接使用这个项。当然很多时候引入的项会是模块、struct、enum等。它实际上是简化路径的一种技术。src/main.rs
和src/lib.rs
是模块crate
的作用域,假设在不进行内容拆分之前,所有代码都放在src/main.rs
中,则目录结构和代码分别如下。目录结构:
|-src/
|-main.rs
代码:
// src/main.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() { println!("add to wait list"); }
pub mod inner_hosting {
pub fn inner_serve() {
println("inner_serve");
}
}
}
}
mod back_of_house {
}
pub fn eat_at_restaurant() {
use front_of_house::{hosting, hosting::inner_hosting};
hosting::add_to_waitlist();
inner_hosting::inner_serve();
}
从上面代码可知模块树结构如下:
|-crate
|-front_of_house[mod]
|-hosting[mod]
|-inner_hosting[mod]
|-add_to_waitlist[fn]
|-back_of_house[mod]
|-eat_at_restaurant[fn]
代码目录结构:
|-src
|-main.rs
|-front_of_house.rs
|-front_of_house/
|-hosting.rs
|-hosting/
|-inner_hosting.rs
|-back_of_house.rs
src/main.rs
:
mod front_of_house;
mod back_of_house;
pub fn eat_at_restaurant() {
use front_of_house::{hosting, hosting::inner_hosting};
hosting::add_to_waitlist();
inner_hosting::inner_serve();
}
src/front_of_house.rs
:
pub mod hosting;
src/front_of_house/hosting.rs
:
pub mod inner_hosting;
pub fn add_to_waitlist() { println!("add to wait list"); }
src/front_of_house/hosting/inner_hosting.rs
:
pub fn inner_serve() {
println!("inner serve");
}
src/back_of_house.rs
:
// 空
前面有讲到,一个Package可以管理多个Crate,但是想要只使用一个Package管理一个大项目还是有所不妥。
Cargo工作空间(workspaces)的功能类似于Maven的Module,即将一个庞大的项目拆分成多个功能相对独立的Package,比如说将项目拆分为三Package:product-management
,order-management
,user-management
。
把整个大项目的每一个Package都放到一个工作空间中进行管理,使用工作空间后项目的结构大致如下:
my-web-shop/
├── Cargo.toml # 工作空间的配置文件
├── Cargo.lock
├── product-management/ # 第一个Package
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs
│ └── lib.rs
├── order-management/ # 第一个Package
│ ├── Cargo.toml
│ └── src
│ ├── main.rs
│ └── lib.rs
├── user-management # 第二个Package
│ ├── Cargo.toml
│ └── src
│ ├── main.rs
│ └── lib.rs
└── target/ # 编译输出
my-web-shop的Cargo.toml
(包含工作空间的配置信息)如下:
[workspace]
members = [
"product-management",
"order-management",
"user-management"
]
cargo new
指令无法之间创建工作空间,因此还是建议先新建一个目录,然后手动创建一个Cargo.toml
文件,然后手动加上[workspace]
以指定该目录是一个工作空间;cargo new
创建Package时,cargo并不会直接在Cargo.toml
的workspace.members
中增加新Package的信息(即不会把新创建的包纳入工作空间管理中),终端只会返回信息当前目录是一个工作目录,此时需要手动设置一下workspace.members
;工作空间在顶级目录有一个 target 目录,每一个Package内没有target目录,即使进入子Package中运行cargo build
,生成的结果依然会保存在工作空间/target
中,这样让所有的Package共享一个target目录,可以避免其他Package多余的重复构建。
子Package之间互相依赖:默认情况下cargo不假定工作空间中的crate会互相依赖,所欲要显式声明crate之间的依赖关系。具体做法为,比如order-management
依赖同级的user-management
,则在order-management/Cargo.toml
中需要声明:
[dependencies]
user-management = { path = "../user-management" }
如果要在工作空间中运行指定的二进制crate,需要增加-p
参数和包名称来指定:
cargo run -p product-management
由此可见Cargo.toml
可以作为Package的配置,也可以作为工作空间的配置。
工作空间中使用外部依赖:
Cargo.lock
,因此工作空间根上的src/*.rs
使用的依赖与每一个子Package的依赖的所有版本信息都交由工作空间根目录的Cargo.lock
约束。Cargo.toml
中声明使用a,那么只有Package A能使用外部库a,但是约束信息还是会保存在根Cargo.lock
。Cargo.lock
的存在,会强制要求工作空间中任何地方都只能用相同版本的外部库a。工作空间中测试:
cargo test
cargo test -p mp-core
工作空间中发布:如果需要单独发布每一个子Package,需要进入到对应的目录中执行cargo publish
。
cargo build
时采用的 dev
配置。dev
配置被定义为开发时的好的默认配置。cargo build --release
的 release
配置。release
配置则有着良好的发布构建的默认配置。Cargo.toml
文件中没有任何 [profile.*]
部分的时候,Cargo 会对每一个配置都采用默认设置。通过增加任何希望定制的配置对应的 [profile.*]
部分,可以选择覆盖任意默认设置的子集。
dev
的优化等级为0,release
的优化等级为3。更多的参数设置可以查看Cargo的文档[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
///
开头的Markdown文本,要求文档注释位于文档的项之前(即是什么函数或者结构体的文档就写在这个函数或者结构体之前)。cargo doc
,可以自动根据文档注释生成对应的HTML文档,并放在target/doc
目录中。cargo doc --open
。# Examples
:例子章节,通常会在这个章节中用三反引号括着调用例子,除了起到说明的作用,还能作为文档测试样例。# Panics
:异常章节,说明会抛出panic!
的场景。# Errors
:错误章节,如果函数返回Result
,此部分会描述代码会出现什么错误并且出现这种错误的原因,方便调用者编写代码处理错误。# Safety
:如果函数使用了unsafe
代码,这一部分应该会涉及到期望函数调用者支持的确保 unsafe
块中代码正常工作的不变条件(invariants)。cargo test
的时候被作为测试样例运行。//!
):专门用于crate根文件(通常是src/lib.rs
)或模块的根文件为crate或模块整体提供文档。用于做一个整体说明。下面是某个src/lib.rs
的开头
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--
cargo login 你的token
,这个命令会通知 Cargo 你的 API token 并将其储存在本地的 ~/.cargo/credentials
文件中。Cargo.toml
的[package]
部分增加一些本crate
的元信息,常用元信息的例子如下:[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name " ]
edition = "2018"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
[dependencies]
...
cargo publish
Cargo.toml
中的version
字段,根据语义化版本规则规定下一个版本号。Cargo.lock
的项目的依赖不会被破坏,同时任何新生成的 Cargo.lock 将不能使用被撤回的版本。cargo yank --vers 1.0.1
--undo
:cargo yank --vers 1.0.1 --undo
cargo install
命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包;它意在作为一个方便 Rust 开发者们安装其他人已经在 crates.io 上共享的工具的手段。只有拥有二进制目标文件的包能够被安装。二进制目标 文件是在 crate 有 src/main.rs
或者其他指定为二进制文件时所创建的可执行程序,这不同于自身不能执行但适合包含在其他程序中的库目标文件。通常 crate 的 README 文件中有该 crate 是库、二进制目标还是两者都是的信息。rustup.rs
安装的 Rust 且没有自定义任何配置,这将是 $HOME/.cargo/bin
。确保将这个目录添加到 $PATH 环境变量中就能够运行通过 cargo install
安装的程序了。$PATH
中有类似 cargo-something
的二进制文件,就可以通过 cargo something
来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 cargo --list
来展示出来。