banner
魔法少女秋小日

Qiuxiaori

github
email

"Rust Programming Language Bible" Study Notes

Online reading address: Rust Programming Language Bible

Reading progress: 60%

image


Development Tools#

VSCode#

Plugins

RustRover#

Plugins

Settings

  • Code formatting

image.png

image.png


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 to cargo. 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 construct Cargo.toml in a reasonable manner.
  • The Cargo.lock file is a detailed list of project dependencies generated by the cargo tool based on the same project's toml file, so we generally do not need to modify it; we just need to work with the Cargo.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 the Cargo.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 the output 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.

    1. 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.

    2. 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 the x parameter will be automatically assigned to the return value &i32, so this function is equivalent to fn foo<'a>(x: &'a i32) -> &'a i32.

    3. 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 a method, 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 the unsafe 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 the std::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 the std::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 {}.

Display Trait

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 the Display 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.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.