三、rust 项目组织结构

一、项目

(一)项目是什么

项目也叫包,英文是package,其实就是个文件夹。
包名就是目录名。
一个包必须有一个Cargo.toml文件。

项目可以嵌套。

(二)如何创建项目

使用cargo new命令创建包。

(三)项目目录结构

项目目录结构如下

.
├── Cargo.lock
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── main.rs
│   └── bin/
│       ├── named-executable.rs
│       ├── another-executable.rs
│       └── multi-file-executable/
│           ├── main.rs
│           └── some_module.rs
├── benches/
│   ├── large-input.rs
│   └── multi-file-bench/
│       ├── main.rs
│       └── bench_module.rs
├── examples/
│   ├── simple.rs
│   └── multi-file-example/
│       ├── main.rs
│       └── ex_module.rs
└── tests/
    ├── some-integration-tests.rs
    └── multi-file-test/
        ├── main.rs
        └── test_module.rs

这是使用Cargo创建的包目录结构,解释如下:

  • Cargo.toml和Cargo.lock保存在包根目录下
  • 源代码放在src目录下
  • 默认库箱的根是src/lib.rs
  • 默认二进制箱的根是src/main.rs
    • 其它二进制箱的根放在src/bin/ 目录下
  • 基准测试benchmark放在benches目录下
  • 示例代码放在examples目录下
  • 集成测试代码放在tests目录下

(四)配置项目

在Cargo.toml文件中的[package]下配置项目。
像下面这样:

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

[package],是包片段,表明下面的语句用来配置包。
接下来的三行是包的名称、包的版本、使用的Rust版本。

二、箱

(一)箱是什么

箱,英文是crate。
箱就是要构建的目标文件,所以也可以叫target,构建对象。
箱的根,英文是crate root,它是一个源文件,Rust编译器以它为起始点构建crate。每个箱都必须有一个根。

(二)箱的分类

箱根据生成目的分为

1、库(Library)

库箱用于生成一个库。
一个项目最多只能有一个库箱。
默认库箱的根是src/lib.rs,默认库箱的名称跟项目名是一致的。

2、二进制(Binaries)

二进制箱可以生成可执行文件。
默认二进制箱的根是src/main.rs,默认二进制箱的名称跟项目名也是相同的。
除了默认二进制箱,一个项目还可以包含多个其他二进制箱,这些箱的根放在src/bin/ 目录下。
我们可以使用cargo run --bin <bin-name> 来运行指定的二进制箱。

3、示例(Examples)

示例箱在examples目录中。示例编译后的文件会存储在target/debug/examples目录下。
使用cargo build --example <example-name> 构建。
使用cargo run --example <example-name> 命令构建并运行。
使用cargo install --example <example-name> 来将示例编译出的可执行文件安装到默认的目录中。
cargo test命令默认会编译示例,以防止示例因为长久没运行,导致严重过期。

4、测试(Tests)

测试位于tests目录中。
使用cargo test命令编译运行。

5、基准性能(Benches)

基准性能位于benches目录下。
可以通过cargo bench命令来编译运行。

(三)包与箱的关系

一个包至少包含一个库箱或二进制箱,这些箱中最多一个库箱,但可以有任意数量的二进制箱。
示例、测试、基准性能则是可有可无的。
只有包含库箱的包,才能发布为依赖包;当添加依赖时,添加的也必然是包含库箱的包。也就是说,依赖包与库箱是一一对应的,所以可以把依赖包和库箱当成一回事。

如果使用cargo new proj创建包,它就包含一个默认二进制箱。src目录下会默认生成一个main.rs源文件,这是默认二进制箱的根。默认二进制箱的名字与包名相同。
如果使用cargo new --lib proj命令创建包,它就包含一个默认库箱。src目录下会默认生成一个lib.rs源文件,这是默认库箱的根。默认库箱的名字与包名相同。
如果把源文件放在src/bin目录下,一个包可以拥有多个二进制箱:src/bin下的每个文件都会编译成一个独立的二进制箱。src/bin目录下的箱的名字与包名不同,而与源文件的名字相同。

(四)配置箱

在 Cargo.toml中的 [lib]、[[bin]]、[[example]]、[[test]] 、[[bench]] 下配置箱。
大家可能会疑惑 [lib] 和 [[bin]] 的写法为何不一致,因为[[]]这种写法表示它是个数组,可以有多个,而[]写法表示只能有一个。

由于它们的配置内容都是相似的,因此我们以 [lib] 为例来说明相应的配置项:

[lib]
name = "foo" # 箱的名称
path = "src/lib.rs" # 箱的根
test = true # 能否被测试,默认是true
doctest = true # 文档测试是否开启,默认是true
bench = true # 基准测试是否开启
doc = true # 文档功能是否开启
plugin = false # 是否可以用于编译器插件(deprecated)
proc-macro = false # 是否是过程宏类型
harness = true # 是否使用libtest harness
edition = "2015" # 使用的Rust版本
crate-type = ["lib"] # 箱类型
required-features = [] # 构建所需的Features

1、name
对于库和默认二进制( src/main.rs ),默认名称是项目的名称( package.name)。
对于其它箱,默认是目录或文件名。
除了 [lib] 外,name字段对于其他箱都是必须的。

2、proc-macro
该字段的使用方式在过程宏章节有详细的介绍。

3、edition
使用的Rust 版本。
如果没有设置,则默认使用 [package] 中配置的package.edition,通常来说,这个字段不应该单独设置,只有在一些特殊场景中才可能用到:例如将一个大型项目逐步升级。

4、crate-type
该字段定义了箱类型,主要表示这个箱是可执行的,还是不可执行的。
它是一个数组,因此为同一个对象指定多个箱类型。
需要注意的是,只有库和示例可以修改箱类型,因为其他的二进制、测试、基准性能只能是bin这个箱类型。
可用的选项包括bin、lib、rlib、dylib、cdylib、staticlib、proc-macro

5、required-features
所需的 features 列表。
该字段只对 [[bin]]、 [[bench]]、 [[test]] 、[[example]] 有效,对于 [lib] 没有任何效果。

[features]
postgres = []
sqlite = []
tools = []
[[bin]]
name = "my-pg-tool"
required-features = ["postgres", "tools"]

例子
以下是一种自定义配置:

[lib]
crate-type = ["cdylib"]
bench = false

[[bin]]
name = "cool-tool"
test = false
bench = false

[[bin]]
name = "frobnicator"
required-features = ["frobnicate"]

[[example]]
name = "foo"
crate-type = ["staticlib"]

三、模块

模块,英文叫module。
rust模块就是命名空间。

(一)声明模块

使用mod关键字来声明模块。

mod hello {
     pub fn say_hello() {
         println!("hello world");
     }
}

模块内的项默认为private,外部不可见,如果要外部可见需要加pub。

模块可以嵌套,形成模块树(module tree)。

mod nation {
     mod government {
         fn govern() {}
     }
     mod congress {
         fn legislate() {}
     }
     mod court {
         fn judicial() {}
     }
}

每个箱都是一个模块树。箱的根之所以叫做箱的根,是因为箱的根源文件为模块树创建了一个名为 crate 的根模块。
模块树如下所示:

crate
└──nation
         ├──government
         │     └──govern
         ├──congress
         │     └──legislate
         └──court
                 └──judicial

(二)使用模块

1.模块的路径

要在模块树中找到一个项,就要使用路径,就像在文件系统使用路径一样。为了调用一个函数,我们需要知道它的路径。
路径有两种形式:

  • 绝对路径(absolute path),是以箱的根模块开头的全路径。引用外部箱代码时,是以外部箱名开头的绝对路径;引用当前箱代码时,不是以箱名,而是以根模块名crate开头。
  • 相对路径(relative path),是从当前所在模块开始,以self、super开头的路径。
    路径以双冒号(::)为分割符。

例如:

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();
     // 相对路径
     self::front_of_house::hosting::add_to_waitlist();
}

(1)使用以 super 开头的相对路径

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

(2)使用外部箱
必须先添加依赖包,依赖包就是外部箱所在的包。
在Cargo.toml中的[dependencies]下添加外部箱所在的包。
比如,

[dependencies]
rand = "0.8.5"

Cargo要从 crates.io 下载 rand 和其依赖。
这样就可以使用绝对路径使用外部箱了。
例子:

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

(3)std
std也是外部箱。因为std随Rust语言一同分发,无需修改 Cargo.toml 来引入 std。
比如,

let mut guess = String::new();
std::io::stdin().read_line(&mut guess).expect("failed readline");

2.use语句

无论是使用绝对路径还是相对路径都不方便,我们可以使用 use 关键字创建一个短路径。

(1)use关键字将标识符引入当前模块下
实例

mod nation {
    pub mod government {
        pub fn govern() {}
    }
}
use crate::nation::government::govern;
fn main() {
    govern();
}

use关键字把govern标识符导入到了当前的模块下,可以直接使用。

(2)可以使用use as为标识符添加别名:
实例

mod nation {
    pub mod government {
        pub fn govern() {}
    }
    pub fn govern() {}
}
use crate::nation::government::govern;
use crate::nation::govern as nation_govern;
fn main() {
    nation_govern();
    govern();
}

这里有两个govern函数,一个是nation下的,一个是government下的,我们用as将nation下的取别名nation_govern。这样两个govern函数就可以同时使用了。

(3)use关键字可以与pub关键字配合使用:
实例

mod nation {
    pub mod government {
        pub fn govern() {}
    }
    pub use government::govern;
}
fn main() {
    nation::govern();
}

(4)使用大括号引入相同模块的多个子模块,可以显著减少 use 语句的数量
比如,

use std::{cmp::Ordering, io};

(5)使用通配符*引入所有子模块
例子

use std::collections::*;

将 std::collections 中所有公有项引入当前模块

(三)将模块分割进不同文件

到目前为止,都是在一个文件中定义多个模块。当模块变得更大时,你可能想要将它们移动到单独的文件中,从而使代码更容易阅读。
例如,

文件名: src/lib.rs

mod front_of_house;     //声明front_of_house模块,其内容将位于src/front_of_house.rs中
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
     hosting::add_to_waitlist();
}
文件名: src/front_of_house.rs

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

在src/front_of_house.rs中定义front_of_house模块。
在mod front_of_house后使用分号,而不是代码块,表示在文件中定义模块。Rust会在与模块同名的文件中查找模块的代码。

继续重构我们例子,将hosting模块也提取到其自己的文件中。

文件名: src/front_of_house.rs

pub mod hosting;

创建一个src/front_of_house目录,在src/front_of_house/hosting.rs文件中定义hosting模块:

文件名: src/front_of_house/hosting.rs

pub fn add_to_waitlist() {}

四、工作空间

工作空间,英文是workspace。
随着项目的深入,库箱持续增大,而你希望将其拆分成多个库箱。但是因为一个项目最多只能包含一个库箱,所以你必须将一个项目拆分成多个项目。此时,Cargo提供了一个叫 工作空间(workspace)的功能。
工作空间是一个目录,其中包含一系列项目,它们共享同样的Cargo.lock和输出目录。

我们以一个例子说明工作空间的用法:
创建一个工作空间,工作空间中有一个二进制项目和两个库项目。二进制会依赖另两个项目。
一个库会提供add_one方法,另一个库会提供add_two方法。

(一)创建一个工作空间。

1、新建工作空间目录add

$ mkdir add
$ cd add

2、在add目录中,创建Cargo.toml文件。
这个Cargo.toml文件配置了整个工作空间。它不会包含 [package],而是包含 [workspace]。
为工作空间增加成员,如下会加入二进制项目adder:

文件名: Cargo.toml
[workspace]
members = [
    "adder",
]

(二)在工作空间中创建项目adder

1、在add目录运行cargo new新建adder项目

$ cargo new adder

到此为止,可以运行cargo build来构建工作空间。
add目录中应该看起来像这样:

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

工作空间目录有一个target目录;adder并没有自己的target目录。即使进入adder目录运行cargo build,构建结果也位于add/target而不是add/adder/target。

2、修改工作空间的Cargo.toml,添加add_one成员:

文件名: Cargo.toml
[workspace]
members = [
"adder",
"add-one",
]

(三)在工作空间中创建项目add_one

1、在add目录运行cargo new --lib新建add_one项目

$ cargo new add_one --lib

现在add目录应该有如下目录和文件:

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

2、在add_one/src/lib.rs文件中,增加一个add_one函数:

文件名: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
    x + 1
}

(四)为adder项目添加依赖add_one

1、在adder/Cargo.toml文件中增加add_one依赖:

文件名: adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }

2、在adder中使用库函数add_one。
打开adder/src/main.rs在顶部增加一行use add_one。
接着修改main函数来调用add_one函数。

文件名: adder/src/main.rs
use add_one;
fn main() {
    let num = 10;
    println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num));
}

3、在add目录中运行cargo build来构建工作空间

$ cargo build

4、通过cargo run -p adder,运行adder

$ cargo run -p adder

这会运行adder/src/main.rs中的代码,其依赖add_one

(五)在工作空间中依赖外部箱

工作空间只在根目录有一个Cargo.lock,而不是在每一个项目目录都有Cargo.lock。这确保了所有的项目都使用相同版本的依赖。如果在Cargo.toml和add_one/Cargo.toml中都增加rand依赖,则Cargo会将其都解析为同一版本并记录到唯一的Cargo.lock中。
1、在add_one/Cargo.toml中的 [dependencies] 部分增加rand:

文件名: add_one/Cargo.toml
[dependencies]
rand = "0.5.5"

现在就可以在add_one/src/lib.rs中增加use rand; 了,

2、在add目录运行cargo build构建整个工作空间,就会引入并编译rand:

$ cargo build

任何依赖rand的项目,都要单独在他们的Cargo.toml中添加rand。例如,如果在adder/src/main.rs中增加use rand;,会得到一个错误,为了修复错误,就要修改adder/Cargo.toml,添加rand依赖。
构建adder会将rand加入到Cargo.lock中adder的依赖列表中,但是这并不会下载rand的额外副本。Cargo确保了工作空间任何项目都使用相同版本的rand。

(六)向 crates.io发布工作空间中的项目

工作空间中的每一个项目需要单独发布。cargo publish命令并没有 --all或者 -p参数,所以必须进入每一个项目的目录并运行cargo publish来发布工作空间中的每一个项目。

现在向工作空间增加add-two来练习!

五、总结

组织结构图如下
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Rust 提供了一种叫做'Enum'的数据类型,它可以帮助你实现树结构。Enum 类型可以把一系列可能的值作为一个单一的变量,而且可以为每个值提供不同的子结构。这样,你就可以用 Rust 来实现树结构了。 ### 回答2: 在Rust中,可以使用结构体和枚举类型来实现树结构。 首先,我们可以创建一个表示节点的结构体,该结构体包含要存储的数据以及指向子节点的引用。代码示例如下: ```rust struct Node<T> { data: T, children: Vec<Node<T>>, } ``` 在这个结构体中,`data`字段用于存储节点所包含的数据,`children`字段用于存储子节点的引用。 接下来,我们可以实现树结构相关的方法,例如插入节点、删除节点、查找节点等。具体实现取决于树结构的需求。 ```rust impl<T> Node<T> { // 在当前节点添加一个子节点 fn add_child(&mut self, child: Node<T>) { self.children.push(child); } // 删除子节点 fn remove_child(&mut self, index: usize) { self.children.remove(index); } // 在子节点中查找特定的值 fn find_child(&self, value: &T) -> Option<&Node<T>> { for child in &self.children { if &child.data == value { return Some(child); } } None } } ``` 这些方法可以根据需要进一步扩展和优化,以满足具体的功能需求。 使用这个树结构的示例代码如下: ```rust fn main() { let root = Node { data: 1, children: vec![ Node { data: 2, children: vec![], }, Node { data: 3, children: vec![Node { data: 4, children: vec![], }], }, ], }; // 查找节点 let target = root.find_child(&4); if let Some(node) = target { println!("Found node: {}", node.data); } else { println!("Node not found."); } } ``` 这段代码创建了一个树结构,并从根节点开始查找值为4的节点。如果找到了该节点,则打印出节点的数据。 总之,在Rust中实现树结构,可以通过结构体和枚举类型来表示节点和子节点,并根据实际需求实现相关的方法。 ### 回答3: Rust 是一种强大的系统编程语言,它提供了许多灵活的方法来实现树结构。 首先,树结构由节点组成。在 Rust 中,可以使用结构体来定义节点,结构体可以包含与节点相关的数据和指向其子节点的指针。例如,可以创建一个名为 `Node` 的结构体,该结构体有一个 `data` 字段用于存储节点数据,以及一个 `children` 字段用于存储该节点的子节点。 在 `Node` 结构体中,可以使用 `Box` 类型的指针来分配和管理子节点的内存。`Box` 类型在 Rust 中提供了堆分配内存的能力,并通过使用 `*` 操作符来引用和解引用节点子节点。 使用 `Option<Box<Node>>` 类型的字段可以实现子节点的可选性,即一个节点可能没有子节点。通过使用 `Option` 类型,可以轻松地处理树结构中可能不存在子节点的情况。 另外,可以为 `Node` 结构体实现方法来操作树结构,例如添加子节点、遍历节点、查找特定节点等。可以使用 Rust 的模式匹配功能来处理不同情况下的操作逻辑。 最后,使用适当的数据结构和算法来支持树结构的功能。例如,可以使用深度优先搜索或广度优先搜索等算法来遍历树的节点。 总结而言,Rust 提供了灵活且安全的方式来实现树结构。通过结构体、指针、可选类型和模式匹配的组合,可以高效地创建和操作树结构,并通过适当的数据结构和算法来优化树的功能和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值