Rust Organization Management
Any programming language that cannot organize code is difficult to delve into, and almost no software product is compiled from a single source file.
All the programs in this tutorial so far have been written in a single file, primarily for the convenience of learning Rust's syntax and concepts.
For a project, organizing code is crucial.
Rust has three important organizational concepts: crates, packages, and modules.
Crate
A "crate" is a binary program file or a library file, existing within a "package."
A "crate" has a tree-like structure, with its root being the program compiled from the source file that the compiler starts with.
Note: A "binary program file" is not necessarily an "executable binary file"; it is only confirmed to be a file containing machine language, with the file format varying with the compilation environment.
Package
When we create a Rust project using the cargo new
command, a Cargo.toml
file is established in the project directory. The essence of the project is a package, which must be managed by a Cargo.toml
file describing the package's basic information and dependencies.
A package can contain at most one library "crate" and any number of binary "crates," but it must contain at least one "crate" (either a library or a binary "crate").
After creating the package with the cargo new
command, a main.rs
source file is generated under the src
directory. Cargo defaults this file as the root of the binary crate, and the compiled binary crate will have the same name as the package.
Module
For a software project, we often organize it according to the organizational conventions of the programming language used. The main structure for organizing modules is typically a tree. Java organizes functional modules primarily by classes, while JavaScript organizes modules mainly by functions.
These advanced languages' organizational units can be nested, similar to the directory structure of a file system. Rust's organizational unit is the module.
mod nation {
mod government {
fn govern() {}
}
mod congress {
fn legislate() {}
}
mod court {
fn judicial() {}
}
}
This is a program describing a rule-of-law country: the nation includes the government, congress, and court, each with administrative, legislative, and judicial functions. We can convert it into a tree structure:
nation
├── government
│ └── govern
├── congress
│ └── legislate
└── court
└── judicial
In the file system, directory structures are often represented by slashes in path strings to indicate the location of objects. Rust's path separator is ::
.
Paths are divided into absolute paths and relative paths. Absolute paths start with the crate
keyword. Relative paths start with self
or super
keywords or an identifier. For example:
crate::nation::government::govern();
is an absolute path describing the govern function, and the relative path can be represented as:
nation::government::govern();
Now you can try defining a similar module structure in a source program and using paths in the main function.
If you do this, you will surely find that it is incorrect: the government module and its functions are private, and you are not allowed to access them.
Access Permissions
Rust has two simple access rights: public and private.
By default, if not modified, the access rights of members in a module will be private.
To use public permissions, the pub
keyword is required.
Example
mod nation {
pub mod government {
pub fn govern() {}
}
mod congress {
pub fn legislate() {}
}
mod court {
fn judicial() {
super::congress::legislate();
}
}
}
fn main() {
nation::government::govern();
}
This program can be compiled. Note the access method of super
in the court module.
If a struct is defined in a module, the struct itself is private by default, as are its fields. Therefore, to use the struct and its fields in a module, a pub
declaration is required:
Example
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() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
}
fn main() {
eat_at_restaurant()
}
Run result:
I'd like Wheat toast please
Enum variants can contain fields, but they do not have similar properties:
Example
mod SomeModule {
pub enum Person {
King {
name: String
},
Quene
}
}
fn main() {
let person = SomeModule::Person::King {
name: String::from("Blue")
};
match person {
SomeModule::Person::King { name } => {
println!("{}", name);
}
_ => {}
}
}
Run result:
Blue
Hidden Modules
Developers who have used Java often dislike the outermost class block—its name is identical to the file name, as it represents the file container. Although it is cumbersome, we have to write it to emphasize "this class is contained within the file."
However, there are some benefits: it at least makes developers clearly aware of the existence of class packaging and can explicitly describe the inheritance relationship of classes.
In Rust, modules are like class packaging in Java, but you can write a main function at the beginning of the file. How should this be explained?
The content of every Rust file is a "hidden" module.
Let's reveal this with two files:
main.rs File
// main.rs
mod second_module;
fn main() {
println!("This is the main module.");
println!("{}", second_module::message());
}
second_module.rs File
// second_module.rs
pub fn message() -> String {
String::from("This is the 2nd module.")
}
Run result:
This is the main module.
This is the 2nd module.
use Keyword
The use
keyword can bring module identifiers into the current scope:
Example
mod nation {
pub mod government {
pub fn govern() {}
}
}
use crate::nation::government::govern;
fn main() {
govern();
}
This program can be compiled.
Because the use
keyword imports the govern
identifier into the current module, it can be used directly.
This solves the problem of overly long local module paths.
Of course, there are cases where two identical names need to be imported, and we can use the as
keyword to add aliases to the identifiers:
Example
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();
}
Here are two govern functions, one under the nation and one under the government. We use as
to alias the one under nation as nation_govern
. Both names can be used simultaneously.
The use
keyword can be used in conjunction with the pub
keyword:
Example
mod nation {
pub mod government {
pub fn govern() {}
}
pub use government::govern;
}
fn main() {
nation::govern();
}
Referencing the Standard Library
Rust official standard library dictionary: https://doc.rust-lang.org/stable/std/all.html
After learning the concepts in this chapter, we can easily import system libraries to facilitate program development:
Example
use std::f64::consts::PI;
fn main() {
println!("{}", (PI / 2.0).sin());
}
Execution result:
1
All system library modules are imported by default, so you only need to use the use
keyword to simplify the path for convenient usage.