Online reading address: Rust Programming Language Bible
Reading progress: 60%
Development Tools#
VSCode#
Plugins
- rust-analyzer, Rust language plugin
- Even Better TOML, supports full features of .toml files
- Error Lens, better error display
- CodeLLDB, Debugger program
RustRover#
Plugins
Settings
- Code formatting
Cargo#
The biggest advantage of using the cargo
tool is its convenient, unified, and flexible management of various dependencies for the project.
Cargo.toml and Cargo.lock#
Cargo.toml
is the project data description file unique tocargo
. It stores all the metadata configuration information for the project. If Rust developers want their Rust project to be built, tested, and run as expected, they must constructCargo.toml
in a reasonable manner.- The
Cargo.lock
file is a detailed list of project dependencies generated by thecargo
tool based on the same project'stoml
file, so we generally do not need to modify it; we just need to work with theCargo.toml
file.
In Cargo.toml
, various dependency sections are mainly used to describe the project's dependencies:
[dependencies]
rand = "0.3"
hammer = { version = "0.5.0"} // Described based on the Rust official repository crates.io through version specification
color = { git = "https://github.com/bjz/color-rs" } // Described based on the project's source code git repository address through URL
geometry = { path = "crates/geometry" } // Described based on the absolute or relative path of the local project through a Unix-like path
-
Cargo.toml file content
The
Cargo.toml
file is used in Rust projects to configure and manage project dependencies, compilation options, metadata, and other information. Below are common sections in theCargo.toml
file and their purposes:1.
[package]
#This section defines the basic information of the package, including name, version, author, description, etc.
[package] name = "my_project" # Project name version = "0.1.0" # Project version edition = "2021" # Rust language version authors = ["Your Name <[email protected]>"] # Author description = "A brief description of the project" # Project description license = "MIT" # License used by the project repository = "<https://github.com/yourusername/my_project>" # Project source repository
2.
[dependencies]
#In this section, the libraries and their versions that the project depends on are defined.
[dependencies] serde = "1.0" # Add serde library as a dependency rand = { version = "0.8", features = ["std"] } # Specify version and additional features
3.
[dev-dependencies]
#This section is used to define dependencies required in the development environment, which are typically only used in testing or development tools.
[dev-dependencies] tokio = { version = "1", features = ["full"] } # Only used in the development phase
4.
[build-dependencies]
#This section is used to define dependencies in the build script (
build.rs
).[build-dependencies] cc = "1.0" # Library used to build native code
5.
[features]
#Used to define optional features (functionalities) of the project. These features allow users to selectively enable or disable certain functionalities.
[features] default = ["serde"] # Features enabled by default extras = ["tokio"] # Optional features
6.
[package.metadata]
#This section is usually used for configuring custom tools, and its content and structure vary by tool.
[package.metadata] docs = { rsdoc = true } # Configuration for custom documentation generator
7.
[workspace]
#If the project is a workspace (containing multiple sub-projects), this section is used to define the structure of the workspace.
[workspace] members = ["project1", "project2"] # Member projects in the workspace
8.
[patch]
and[replace]
#Used to replace or patch the versions of dependencies, typically used when dealing with dependency conflicts or during development.
[patch.crates-io] serde = { git = "<https://github.com/serde-rs/serde>", branch = "master" } [replace] foo = { path = "../foo" } # Replace dependency library
9.
[profile]
#This section allows you to customize compilation options for different compilation configurations (such as
dev
,release
).[profile.dev] opt-level = 1 # Optimization level used in development mode [profile.release] opt-level = 3 # Optimization level used in release mode
10.
[badges]
#Used to display status badges (such as build status, code coverage, etc.) on documentation generation tools or other services.
[badges] travis-ci = { repository = "rust-lang/crates.io" } # Display Travis CI badge
These are common sections in the
Cargo.toml
file and their purposes. Depending on the complexity and needs of the project, the file content can be richer or simpler.
Image Acceleration#
Downloading dependencies too slowly? - Rust Programming Language Bible (Rust Course)
[http]
check-revoke = false
# proxy = "http://x.x.x.x:8888" // Used as a proxy when developing in a virtual machine without internet access
[source.crates-io]
replace-with = 'rsproxy'
[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"
[net]
git-fetch-with-cli = true
Generics and Traits#
Generics are a form of polymorphism. The main purpose of generics is to provide convenience for programmers, reduce code bloat, and greatly enrich the expressiveness of the language itself.
- Enum
enum Option<T> {
Some(T),
None,
}
- Struct
struct Point<T> {
x: T,
y: T,
}
- Method
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
Performance of Generics#
In Rust, generics are zero-cost abstractions. When using generics, you don't have to worry about performance issues, but it may incur a loss in compilation speed and increase the size of the final generated file.
Rust ensures efficiency by performing monomorphization at compile time for generic code. Monomorphization is the process of converting generic code into specific code by filling in the concrete types used at compile time.
The compiler does the opposite of what we do when creating generic functions; it looks for all the places where generic code is called and generates code for specific types.
Multiple Constraints of Traits#
pub fn notify(item: &(impl Summary + Display)) {} // Syntactic sugar form
pub fn notify<T: Summary + Display>(item: &T) {} // Trait constraint form
Where Constraints
When trait constraints become numerous, the function signature becomes complex:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
By using where
, we can make some formal improvements:
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}
Dynamic Arrays#
https://doc.rust-lang.org/std/collections/
Creation#
let mut v1 = Vec::with_capacity(10);
v1.extend([1, 2, 3]); // Append data to v
println!("Vector length is: {}, capacity is: {}", v.len(), v.capacity());
Common Methods#
let mut v = vec![1, 2];
assert!(!v.is_empty()); // Check if v is empty
v.reserve(100); // Adjust v's capacity to at least 100
v.insert(2, 3); // Insert data at the specified index; index value cannot exceed v's length, v: [1, 2, 3]
assert_eq!(v.remove(1), 2); // Remove the element at the specified position and return it, v: [1, 3]
assert_eq!(v.pop(), Some(3)); // Remove and return the element at the tail of v, v: [1]
assert_eq!(v.pop(), Some(1)); // v: []
assert_eq!(v.pop(), None); // Remember that the pop method returns an Option enum value
v.clear(); // Clear v, v: []
let mut v1 = [11, 22].to_vec(); // Append operation will clear data in v1, increase mutable declaration
v.append(&mut v1); // Append all elements from v1 to v, v1: []
v.truncate(1); // Truncate to the specified length, excess elements are deleted, v: [11]
v.retain(|x| *x > 10); // Retain elements that meet the condition, i.e., delete elements that do not meet the condition
let mut v = vec![11, 22, 33, 44, 55];
// Remove elements in the specified range while obtaining an iterator of the removed elements, v: [11, 55], m: [22, 33, 44]
let mut m: Vec<_> = v.drain(1..=3).collect();
let v2 = m.split_off(1); // Split into two vecs at the specified index, m: [22], v2: [33, 44]
let v2 = vec![11, 22, 33, 44, 55]; // Slice to extract the array
let slice = &v2[1..=3];
assert_eq!(slice, &[22, 33, 44]);
Sorting#
fn main() {
let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];
vec.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(vec, vec![1.0, 2.0, 5.6, 10.3, 15f32]);
}
Sorting Structs
// Custom comparison function for sorting in descending order by age
people.sort_unstable_by(|a, b| b.age.cmp(&a.age));
Sorting requires implementing the Ord
trait. If the struct implements this trait, there is no need to define a custom comparison function. However, implementing Ord
requires us to implement Ord
, Eq
, PartialEq
, and PartialOrd
traits, which can be derived:
#[derive(Debug, Ord, Eq, PartialEq, PartialOrd)] // derive attributes
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
fn main() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("Al".to_string(), 30),
Person::new("John".to_string(), 1),
Person::new("John".to_string(), 25),
];
people.sort_unstable();
println!("{:?}", people);
}
// => [Person { name: "Al", age: 30 }, Person { name: "Al", age: 60 }, Person { name: "John", age: 1 }, Person { name: "John", age: 25 }, Person { name: "Zoe", age: 25 }]
The default implementation of derive
will compare based on the order of attributes. In the above example, when the name
value of Person
is the same, it will use age
for comparison.
HashMap#
Creation#
fn main() {
use std::collections::HashMap;
let teams_list = vec![
("China Team".to_string(), 100),
("USA Team".to_string(), 10),
("Japan Team".to_string(), 50),
];
let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
println!("{:?}",teams_map)
}
Lifetimes 🌟#
Dangling Pointers#
The main purpose of lifetimes is to avoid dangling references, which can lead to the program referencing data that it should not reference.
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
Syntax#
The syntax for lifetimes starts with a '
, and the name is often a single lowercase letter, typically using 'a
as the name for the lifetime. If it is a reference type parameter, the lifetime will be placed after the reference symbol &
and separated from the lifetime and reference parameter by a space:
&i32 // A reference
&'a i32 // A reference with an explicit lifetime
&'a mut i32 // A mutable reference with an explicit lifetime
Function Signatures
When using lifetime parameters, you need to declare <'a>
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Static Lifetime
In Rust, there is a very special lifetime, which is 'static
, and references with this lifetime can live as long as the entire program.
Previously, we learned that string literals are hardcoded into Rust's binary files, so all these string variables have a 'static
lifetime:
let s: &'static str = "I have no advantages, just live long, hehe";
Lifetime Elision#
Before we start, there are a few points to note:
-
The elision rules are not universal; if the compiler cannot determine that something is correct, it will directly consider it incorrect, and you will still need to manually annotate the lifetime.
-
In functions or methods, the lifetime of parameters is called the
input lifetime
, and the lifetime of the return value is called theoutput lifetime
. -
Three Elision Rules
The compiler uses three elision rules to determine which scenarios do not require explicit lifetime annotations. The first rule applies to input lifetimes, while the second and third apply to output lifetimes. If the compiler finds that none of the three rules apply, it will raise an error, prompting you to manually annotate the lifetime.
-
Each reference parameter gets its own lifetime
For example, a function with one reference parameter has one lifetime annotation:
fn foo<'a>(x: &'a i32)
, while a function with two reference parameters has two lifetime annotations:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
, and so on. -
If there is only one input lifetime (only one reference type in the function parameters), that lifetime will be assigned to all output lifetimes, meaning all return value lifetimes equal that input lifetime.
For example, the function
fn foo(x: &i32) -> &i32
, the lifetime of thex
parameter will be automatically assigned to the return value&i32
, so this function is equivalent tofn foo<'a>(x: &'a i32) -> &'a i32
. -
If there are multiple input lifetimes, and one of them is
&self
or&mut self
, then the lifetime of&self
is assigned to all output lifetimes.Having a
&self
parameter indicates that the function is amethod
, and this rule greatly enhances the usability of methods.
-
A Complex Example: Generics and Trait Constraints#
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>( // Declare lifetime and generic
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann); // Since we need to output ann using formatting {}, it needs to implement the Display trait.
if x.len() > y.len() {
x
} else {
y
}
}
Using Fn
Trait to Solve Closure Lifetimes#
fn main() {
let closure_slision = fun(|x: &i32| -> &i32 { x });
assert_eq!(*closure_slision(&45), 45);
// Passed!
}
fn fun<T, F: Fn(&T) -> &T>(f: F) -> F {
f
}
NLL (Non-Lexical Lifetime)#
Previously, we discussed this concept in References and Borrowing. In simple terms: The lifetime of a reference should normally last from the time it is borrowed until the end of its scope, but this rule complicates situations where multiple references coexist:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// In the new compiler, the scopes of r1 and r2 end here
let r3 = &mut s;
println!("{}", r3);
}
According to the above rules, this code will raise an error because the immutable references r1
and r2
will last until the end of the main
function, and within that scope, we borrowed a mutable reference r3
, which violates the borrowing rules: Either multiple immutable borrows or one mutable borrow.
Fortunately, this rule was changed with the introduction of NLL
in version 1.31 to: The lifetime of a reference starts from the borrowing point and lasts until the last usage point.
Following the latest rules, let's analyze the above code again. The immutable borrows r1
and r2
are no longer used after println!
, so their lifetimes end, allowing the mutable borrow of r3
to no longer violate the borrowing rules.
Reborrow#
https://course.rs/advance/lifetime/advance.html#reborrow - 再借用
- todo
Error Handling#
Panic#
https://course.rs/basic/result-error/panic.html#panic - 原理剖析
Backtrace Stack Expansion
If we want to know which call paths the program went through before throwing a panic
, run the program with RUST_BACKTRACE=1 cargo run
or $env:RUST_BACKTRACE=1 ; cargo run
.
Two Termination Methods on Panic
When a panic!
occurs, the program provides two ways to handle the termination process: stack unwinding and immediate termination.
The default method is stack unwinding
, which means Rust will backtrack the data and function calls on the stack, leading to more cleanup work. The benefit is that it can provide sufficient error messages and stack call information for post-issue review. Immediate termination
, as the name suggests, exits the program directly without cleaning up data, leaving the cleanup work to the operating system.
For most users, using the default choice is best, but if you care about the size of the final compiled binary executable, you can try using the immediate termination method, for example, by modifying the configuration in the Cargo.toml
file to achieve immediate termination on panic
in release
mode:
[profile.release]
panic = 'abort'
Packages and Modules#
Re-exporting Imported Items#
When an external module item A
is imported into the current module, its visibility is automatically set to private. If you want to allow other external code to reference our module item A
, you can re-export it:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
As shown above, using pub use
can achieve this. Here, use
represents importing the hosting
module into the current scope, and pub
indicates that the imported content is made visible again.
When you want to hide internal implementation details or organize code for a specific purpose, you can use pub use
to re-export. For example, if you want to provide an API uniformly through a module, that module can import APIs from other modules and then re-export them, so that for users, all APIs are provided by a single module.
Comments and Documentation#
Code Comments#
// Line comment
/**
Block comment
*/
Documentation Comments#
/// `add_one` adds one to the specified value
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
/** `add_two` adds two to the specified value
let arg = 5;
let answer = my_crate::add_two(arg);
assert_eq!(7, answer);
*/
pub fn add_one(x: i32) -> i32 {
x + 1
}
Common Documentation Titles
In documentation comments, besides the # Examples
title, there are some common ones that can be used as needed in the project:
- Panics: Exceptional situations that may occur in the function, allowing callers to avoid them in advance.
- Errors: Describes possible errors and what situations may lead to errors, helping callers take different handling approaches for different errors.
- Safety: If the function uses
unsafe
code, the caller needs to pay attention to certain usage conditions to ensure the proper functioning of theunsafe
code block.
Package and Module Level Comments#
Also divided into line comments //!
and block comments /*! ... */
.
Viewing Documentation#
cargo doc
cargo doc --open
Documentation Testing#
https://course.rs/basic/comment.html# 文档测试 doc-test
Retain Tests, Hide Documentation
Sometimes, we want to retain the functionality of documentation tests but hide the content of certain test cases from the documentation:
/// ```
/// # // Lines starting with # will be hidden in the documentation but will still run in the documentation tests
/// # fn try_main() -> Result<(), String> {
/// let res = world_hello::compute::try_div(10, 0)?;
/// # Ok(()) // returning from try_main
/// # }
/// # fn main() {
/// # try_main().unwrap();
/// #
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Divide-by-zero"))
} else {
Ok(a / b)
}
}
In the above documentation comment, we use #
to hide content we do not want users to see, but it does not affect the execution of test cases. Ultimately, users will only see the line that is not hidden: let res = world_hello::compute::try_div(10, 0)?;
.
Formatted Output#
https://course.rs/basic/formatted-output.html
{} and#
Unlike other languages that commonly use %d
, %s
, Rust uniquely chooses {}
as the formatting placeholder. This choice has proven to be very correct, helping users reduce many usage costs. You no longer need to choose specific placeholders for specific types; you can uniformly use {}
instead, leaving the details of type inference to Rust.
Similar to {}
, {:?}
is also a placeholder:
{}
is suitable for types that implement thestd::fmt::Display
trait, used to format text in a more elegant and user-friendly way, such as displaying to users.{:?}
is suitable for types that implement thestd::fmt::Debug
trait, used in debugging scenarios.
The choice between the two is simple: when you need to debug, use {:?}
, and for other scenarios, choose {}
.
Unlike most types that implement Debug
, there are not many Rust types that implement the Display
trait, and often we need to customize the desired formatting:
let i = 3.1415926;
let s = String::from("hello");
let v = vec![1, 2, 3];
let p = Person {
name: "sunface".to_string(),
age: 18,
};
println!("{}, {}, {}, {}", i, s, v, p);
After running, you can see that v
and p
cannot compile because they do not implement the Display
trait, but you cannot derive Display
like you can with Debug
, so you have to look for other methods:
- Use
{:?}
or{:#?}
- Implement the
Display
trait for custom types - Use
newtype
to implement theDisplay
trait for external types
Let's take a look at these three methods one by one.
{:#?}
is almost the same as {:?}
, with the only difference being that it can output content more beautifully:
// {:?}
[1, 2, 3], Person { name: "sunface", age: 18 }
// {:#?}
[
1,
2,
3,
], Person {
name: "sunface",
}
Therefore, for types that do not support Display
, you can consider using {:#?}
for formatting, although theoretically it is more suitable for debugging output.
Implementing the Display
Trait for Custom Types
If your type is defined in the current scope, you can implement the Display
trait for it to be used for formatted output:
struct Person {
name: String,
age: u8,
}
use std::fmt;
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Respected one, please accept my bow. My name is {}, I am {} years old, with no land or car at home, living a hard life.",
self.name, self.age
)
}
}
fn main() {
let p = Person {
name: "sunface".to_string(),
age: 18,
};
println!("{}", p);
}
As shown above, by implementing the fmt
method in the Display
trait, we can add custom output for the custom struct Person
.