1. Installation and Debugging
    1. Installation
    2. Debugging
  2. Cargo
    1. Create a Project
    2. Build a Cargo Project
    3. Build and Run a Cargo Project
    4. cargo check
    5. Release build
  3. Variables and Mutability
    1. Variables
    2. Constants
    3. Shadowing
    4. Allowing unused variables.
  • Data types.
    1. Scalar types.
      1. integer type
      2. floating-point types
      3. Sequence
      4. Boolean type
      5. Character type
      6. Unit type
    2. Compound types
      1. Tuple type
      2. Array type
    3. Type Casting
      1. Basic type conversion using as
      2. From/Into
        1. Implement the From trait for a custom type
      3. TryFrom / TryInto
      4. Other Conversions
        1. Converting any type to String
        2. Parsing a String
        3. Custom implementation of the FromStr trait
        4. transmute
    4. Functions
      1. Parameters
      2. Statements and Expressions
      3. Return value
        1. Return type is ()
        2. Return type is never
        3. Diverging function
    5. Control flow
      1. if expressions
      2. Using if in let statements
      3. Loops
        1. loop
          1. Returning Values from Loops
          2. Loop Labels
      4. while
      5. for
        1. Iterating over an array by index and value
  • Ownership, References, and Borrowing
    1. Stack and Heap
    2. Ownership Rules
      1. When ownership is transferred, mutability can also change accordingly.
    3. Variables and Scope
    4. str and &str
    5. String type
    6. Memory and Allocation
    7. Ways Variables and Data Interact
      1. move
        1. partial move
      2. clone
      3. copy
    8. Ownership and Functions
      1. Return values and scope
    9. References and Borrowing
      1. Rust automatically dereferences in certain situations
      2. Mutable References
      3. Dangling References
      4. ref
      5. Summary of reference rules (borrowing rules)
      6. None Lexical Lifetimes(NLL)
    10. Slice Type
      1. string slice
      2. String literals are slices
      3. String slices as parameters
      4. Other types of slices
  • Struct
    1. Defining a struct
    2. Instantiation
    3. Access
    4. Field Init Shorthand
    5. Struct Update Syntax
    6. Tuple Structs
    7. Unit-like structs with no fields
    8. Ownership in structs
    9. Printing structs
    10. Methods of a struct
    11. Method call operators
    12. Associated functions
    13. Example
  • Enum
    1. Define an enum
    2. Enum values
    3. Attaching data to enum variants
    4. Defining functions in enums
    5. The Option enum
    6. Implementing a linked list with an enum
  • Pattern matching
    1. The match control flow construct
      1. matches!
      2. Pattern that binds values
      3. Matching Option
      4. Match must be exhaustive
    2. if let concise control flow
    3. Variable shadowing in pattern matching
  • Workspace, Package,Crate,Module
    1. Cargo conventions
    2. define modules to control scope and privacy
    3. Paths
    4. Privacy boundary
    5. super
    6. pub struct
    7. pub enum
    8. The use keyword
    9. Idiomatic use of use
    10. Providing a new name with the as keyword
    11. Re-exporting with pub use
    12. pub(in Crate)
    13. Using external packages
    14. Use nested paths to clean up large use statements
    15. Wildcard *
    16. Splitting modules into different files
  • Common collections
    1. Vector
      1. Create a vector
      2. Adding elements
      3. Deleting a Vector
      4. Reading values from a Vector
      5. Reference borrowing rules
      6. Iterating over a Vector
      7. Extending a Vector
      8. Use enum to store multiple data types in Vec
      9. Use trait objects to store multiple data types in Vec
      10. Convert type X (using From/Into traits) to Vec
      11. Slice
      12. capacity
    2. String
      1. What is a string?
      2. Other string types
      3. Conversion between String and &str
      4. String escaping
      5. Create a new String
      6. Updating a String
        1. push_str()
        2. push()
        3. Concatenating strings with +
        4. format!
      7. access by indexString
      8. byte string
      9. Bytes, Scalar Values, Grapheme Clusters
      10. Slicing a String
      11. Iterating over Strings
    3. HashMap
      1. Creating HashMap<K,V>
      2. HashMap and Ownership
      3. Accessing values in a HashMap
      4. Updating a HashMap
        1. Overwriting a value
        2. Inserting only if the key has no value
        3. Update a value based on the old value
        4. Hash function
      5. HashMap key restrictions
      6. Capacity
      7. Ownership
      8. Third-party Hash Libraries
  • Error Handling
    1. Unrecoverable errors and panic!
      1. panic = ‘abort’
      2. Locate the problematic code by calling the backtrace information of the panic! function
    2. Result enum and recoverable errors
      1. Match different errors
      2. unwrap
      3. expect
    3. propagating errors
    4. ? operator
    5. ? and the from function
    6. ? and the main function
    7. map,and_then
    8. When to panic!
      1. General principle
      2. Scenarios
      3. Create custom types for validation
    9. Using Result in fn main
  • Generics
    1. Define generics in functions
    2. Define generics in structs
    3. Define generics in enums
    4. Generics in method definitions
    5. Const generics
    6. Performance of Generic Code
  • Trait: Defining Shared Behavior
    1. Defining a Trait
    2. Implementing a trait on a type
    3. Constraints for implementing a trait
    4. Default implementations of functions in a trait
    5. Traits as parameters
      1. impl trait syntax:
      2. trait bound syntax:
      3. Specifying multiple trait bounds with +
      4. Simplifying trait bounds with where
    6. Returning types that implement a trait
    7. Trait objects, using trait objects in arrays
    8. &dyn and Box
    9. Static dispatch and dynamic dispatch
    10. Use trait bounds to fix the largest function
    11. Conditionally implement methods using trait bounds
    12. Derive macro implementations
  • Lifetimes
    1. Borrow Checker
    2. Generic Lifetimes in Functions
    3. Lifetime Annotation Syntax
    4. Deep Understanding of Lifetimes
    5. Lifetime annotations in struct definitions
    6. Lifetime elision
    7. Lifetime annotations in method definitions
    8. Static lifetime
    9. Combining generic type parameters, trait bounds, and lifetimes
  • Writing automated tests
    1. Anatomy of a Test Function
      1. Writing Test Functions
      2. Running Tests
      3. Test Failure
    2. Assert
      1. Use the assert! macro to check test results
      2. Using assert_eq! and assert_ne! macros to test equality
      3. Custom error messages
    3. Verifying error handling scenarios
      1. Make should_panic more precise
    4. Using Result<T, E> in tests
    5. Controlling test execution
      1. Running tests in parallel
        1. The --test-threads argument
      2. Showing function output
      3. Running a Subset of Tests by Name
        1. Running a Single Test
        2. Filtering to Run Multiple Tests
        3. Ignoring Some Tests
    6. Test Organization
      1. Unit Tests
        1. Testing private functions
      2. Integration tests
        1. tests directory
        2. Run a specific integration test
        3. Submodules in integration tests
        4. Integration tests for binary crates
  • IO Project: Building a Command Line Program
    1. Accepting Command Line Arguments
    2. Separation of Concerns for Binary Projects
    3. TDD (Test-Driven Development)
    4. Writing the minigrep code
  • Cover image for rust language basics

    rust language basics


    Timeline

    Timeline

    2025-09-11

    init

    2025-10-19

    modify some format errors

    2025-10-22

    move something to another post

    This article introduces the basics of the Rust language, discusses in detail the installation and debugging of the Rust environment, the usage of the Cargo package management tool, and summarizes the core syntax of variables and mutability mechanisms as well as scalar data types such as integers, floating-point numbers, booleans, and characters.

    Installation and Debugging

    Installation

    On Linux

    1
    2
    3
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    # Follow the prompts to install
    rustc --version

    Debugging

    Usage on Linux

    1
    rust-gdb target/debug/your_program

    Cargo

    Create a Project

    cargo new project_name

    Help Information

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Create a new cargo package at <path>

    Usage: cargo.exe new [OPTIONS] <path>

    Arguments:
    <path>

    Options:
    -q, --quiet Do not print cargo log messages
    --registry <REGISTRY> Registry to use
    --vcs <VCS> Initialize a new repository for the given version control system (git, hg, pijul, or fossil) or do not initialize any version control at all (none), overriding a global configuration. [possible values: git, hg, pijul, fossil, none]
    --bin Use a binary (application) template [default]
    -v, --verbose... Use verbose output (-vv very verbose/build.rs output)
    --lib Use a library template
    --color <WHEN> Coloring: auto, always, never
    --edition <YEAR> Edition to set for the crate generated [possible values: 2015, 2018, 2021]
    --frozen Require Cargo.lock and cache are up to date
    --name <NAME> Set the resulting package name, defaults to the directory name
    --locked Require Cargo.lock is up to date
    --offline Run without accessing the network
    --config <KEY=VALUE> Override a configuration value
    -Z <FLAG> Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
    -h, --help Print help information

    Run `cargo help new` for more detailed information.

    Cargo.toml

    TOML (Tom’s Obvious, Minimal Language) format is the configuration format for Cargo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [package]#Section header, indicating that the following is used to configure the package
    name = "hello" #Project Name
    version = "0.1.0" #Project Version
    authors = ["cauchy <731005515@qq.com>"] #Author
    edition = "2021" #Rust Version Used

    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

    [dependencies]#Dependencies

    In Rust, a package of code is calledcrate

    Build a Cargo Project

    1
    cargo build

    Create executable target/debug/hello_cargo or target\debug\hello_cargo.exe(Windows)

    Run .\target\debug\hello_cargo.exe

    The first run generates the cargo.lock file

    This file tracks the exact versions of project dependencies; no need to manually modify it

    Build and Run a Cargo Project

    1
    cargo run

    If previously compiled and code unchanged, it will execute directly

    cargo check

    1
    cargo check

    Check code to ensure it compiles, but produce no executable file

    cargo check is much faster than cargo build

    Release build

    1
    cargo build --release

    Optimizations are performed during compilation, making the code run faster but increasing compilation time

    The executable will be generated in target/release instead of target/debug

    Variables and Mutability

    Variables

    Declaration and usageletKeyword

    By default, variables are immutable

    When declaring a variable, addmutkeyword to make the variable mutable

    let mut x = 3;

    Constants

    Similar to immutable variables,_Constants_are values bound to a name that are not allowed to change, but constants differ from variables in some ways.

    1. The mut keyword is not allowed with constants. Constants are not just immutable by default—they are always immutable.

    2. Use the const keyword instead of let to declare constants, and**must** annotate the type of the value

    3.Constants can be declared in any scope, including the global scope,

    1. The last difference is that constants can only be set toconstant expressions, andcannot be any other value that can only be computed at runtime
    1
    2
    const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
    const MAX_POINTS:u32 = 100_000;

    Naming convention: all uppercase, separated by underscores

    Shadowing

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fn main() {
    let x = 5;

    let x = x + 1;

    {
    let x = x * 2;
    println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
    }

    We can define a new variable with the same name as a previous variable,the new variable shadows the previously declared variable with the same name

    Shadowing and marking a variable as mutare different

    • If the let keyword is not used, assigning a value to a non-mut variable will cause a compile-time error.
    • A new variable with the same name declared using let is also immutable.
    • A new variable with the same name declared using let,its type can be different from the previous one.

    Allowing unused variables.

    Two ways.

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let _x = 1;
    }
    #[allow(unused_variables)]
    fn main() {
    let x = 1;
    }

    Data types.

    Rust isa statically compiled language, the types of all variables must be known at compile time.

    Scalar types.

    Rust has four basic scalar types:IntegerFloating-pointBooleanandcharacter type

    integer type

    If we do not explicitly give a variable a type, the compiler will automatically infer one for us.

    1
    2
    3
    4
    5
    6
    7
    8
    fn main(){
    let x = 5;
    assert_eq!("i32".to_string(),type_of(&x));
    }
    // The following function can get the type of the passed parameter and return the string form of the type.
    fn type_of<T>(_: &T) -> String{
    format!("{}",std::any::type_name::<T>())
    }

    If an integer is not given a type, it defaults to the i32 type.

    1
    2
    3
    fn main() {
    let v: u16 = 38_u8 as u16;
    }
    lengthsignedunsigned
    8-biti8u8
    16-biti16u16
    32-biti32u32
    64-biti64u64
    128-biti128u128
    archisizeusize

    The isize and usize types depend on the computer architecture running the program: they are 64-bit on 64-bit architectures and 32-bit on 32-bit architectures.

    1
    2
    3
    4
    fn main() {
    assert_eq!(i8::MAX, 127);
    assert_eq!(u8::MAX, 255);
    }

    integer literals

    numeric literalsexample
    Decimal98_222
    Hex0xff
    Octal0o77
    Binary0b1111_0000
    Byte (single-byte character, only for u8)b’A’

    Rust’s default numeric type is i32. isize or usize are mainly used as indices for certain collections.

    Integer Overflow

    For example, a u8 can hold values from 0 to 255. So what happens when you try to change it to 256? This is called “integer overflow” (integer overflow), which leads to one of two behaviors. Whencompiling in debug mode, Rust checks for such issues and causes the program to panic, a term Rust uses to indicate the program exits due to an error.

    In release builds, Rust does not detect overflow; instead, it performs an operation known as two’s complement wrapping. In short, values larger than the maximum the type can hold wrap around to the minimum, so 256 becomes 0, 257 becomes 1, and so on. Relying on integer wrapping is considered an error, even if this behavior can occur. If you truly need this behavior, the standard library provides a type, Wrapping, that explicitly offers this functionality. To explicitly handle the possibility of overflow, you can use the following methods provided by the standard library on primitive numeric types:

    • Available in all modeswrapping_* methods for wrapping, such as wrapping_add
    • Ifchecked_* methodreturns None if overflow occurs
    • withoverflowing_* methodreturns a value and a boolean indicating whether overflow occurred
    • withsaturating_* methodsaturates at the minimum or maximum value
    1
    2
    3
    4
    5
    6
    // fix errors and`panic`
    fn main() {
    let v1 = 251_u8 + 8;
    let v2 = i8::checked_add(251, 8).unwrap();
    println!("{},{}",v1,v2);
    }

    modify

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fn main() {
    let v1 = 247_u8 + 8;
    let v2 = i8::checked_add(119, 8).unwrap();
    println!("{},{}",v1,v2);
    }
    #[allow(unused_variables)]
    fn main() {
    let v1 = 251_u16 + 8;
    let v2 = u16::checked_add(251, 8).unwrap();
    println!("{},{}",v1,v2);
    }

    floating-point types

    Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits respectively. The default type is f64 because on modern CPUs it is roughly the same speed as f32 but offers higher precision. All floating-point types are signed.

    1
    2
    3
    4
    5
    fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
    }

    Floating-point numbers are represented according to the IEEE-754 standard. f32 is a single-precision float, and f64 is a double-precision float.

    1
    2
    3
    4
    5
    fn main() {
    let x = 1_000.000_1; // f64
    let y: f32 = 0.12; // f32
    let z = 0.01_f64; // f64
    }

    numeric operations

    All number types in Rust support basic mathematical operations: addition, subtraction, multiplication, division, and remainder. Integer division truncatesRound downTo the nearest integer

    1
    2
    3
    4
    5
    6
    fn main() {
    assert_eq!(0.1+0.2,0.3);//Report error
    }
    thread 'main' panicked at 'assertion failed: `(left == right)`
    left: `0.30000000000000004`,
    right: `0.3`', src\main.rs:5:5

    Two modification methods

    1
    2
    3
    4
    5
    6
    fn main() {
    assert!(0.1+0.2>=0.3);
    }
    fn main() {
    assert!(0.1_f32+0.2_f32==0.3_f32);
    }

    Calculate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    use std::fmt::Display;

    #[allow(unused_variables)]
    fn print_something<T >(something:T)
    where T : Display
    {
    println!("{}",something);
    }
    fn main() {
    // Integer addition
    print_something(1u32 + 2 );

    // Integer subtraction
    print_something(1i32 - 2 );
    print_something(1i8 - 2);

    print_something(3 * 50 );

    print_something(9 / 3 == 3); // Error! Modify it to make the code work

    print_something(24 % 5 );

    // Logical AND, OR, NOT operations
    print_something(true && false );
    print_something(true || false );
    print_something(!true );

    // Bitwise operations
    println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);
    println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);
    println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101);
    println!("1 << 5 is {}", 1u32 << 5);
    println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);
    }

    Sequence

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn main() {
    let mut sum = 0;
    for i in -3..2 {
    println!("i is {}",i);//-3 to 1 excluding 2
    }

    for c in 'a'..='z' {
    println!("{}",c);//a-z including z
    }
    }
    #[allow(unused_variables)]
    // Fix errors in the code and`panic`
    use std::ops::{Range, RangeInclusive};
    fn main() {
    assert_eq!((1..5), Range{ start: 1, end: 5 });
    assert_eq!((1..=5), RangeInclusive::new(1, 5));
    }

    Boolean type

    The boolean type in Rust is represented by bool

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
    }
    fn main() {
    let f = true;
    let t = true && false || true;//Boolean operations
    assert_eq!(t, f);

    println!("Success!")
    }

    Character type

    Rust’s char type is the most primitive alphabetic type in the language

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
    }
    fn main() {
    let c1 = '中';
    print_char(c1);
    }

    fn print_char(c : char) {
    println!("{}", c);
    }

    Char literals are declared with single quotes, whereas string literals are declared with double quotes. The size of Rust’s char type isfour bytes(four bytes), and represents a Unicode Scalar Value, which means it can represent more than ASCII. In Rust, accented letters, Chinese, Japanese, Korean characters, emoji, and zero-length whitespace characters are all valid char values. Unicode scalar values range from U+0000 to U+D7FF and U+E000 to U+10FFFF. However, ‘character’ is not a concept in Unicode, so what people intuitively consider a ‘character’ may not correspond to Rust’s char.

    Size

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #[allow(unused_variables)]

    use std::mem::size_of_val;
    fn main() {
    let c1 = 'a';
    assert_eq!(size_of_val(&c1),4); //One character is 4 bytes

    let c2 = '中';
    assert_eq!(size_of_val(&c2),4);

    println!("Success!")
    }

    Unit type

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fn main() {
    let _v: () = ();

    let v = (2, 3);
    assert_eq!(_v, implicitly_ret_unit());

    println!("Success!")
    }

    fn implicitly_ret_unit() {
    println!("I will return a ()")
    }

    The memory occupied by the unit type is 0!!!

    1
    2
    3
    4
    5
    6
    7
    use std::mem::size_of_val;
    fn main() {
    let unit: () = ();
    assert!(size_of_val(&unit) == 0);

    println!("Success!")
    }

    Compound types

    Compound typesCompound types) can combine multiple values into one type. Rust has two primitive compound types: tuples and arrays.

    Tuple type

    TupleFixed length: Once declared, its length cannot increase or decrease

    Create a tuple using a comma-separated list of values enclosed in parentheses. Each position in the tuple has a type, and the types of these different valuesdo not have to be the same

    1
    2
    3
    fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
    }

    The tup variable is bound to the entire tuple because a tuple is a single compound element. To get individual values from a tuple, you can usepattern matching(pattern matching) todestructure(destructure) the tuple value, like this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
    }
    fn main() {
    let (x, y, z);

    // Fill in the blank
    (y,z,x) = (1, 2, 3);

    assert_eq!(x, 3);
    assert_eq!(y, 1);
    assert_eq!(z, 2);
    }

    The program first creates a tuple and binds it to the tup variable. Then it uses let and a pattern to split tup into three different variables, x, y, and z. This is calleddestructuringdestructuring), because it splits a tuple into three parts.

    You can alsoUse a dot (.) followed by the index of the value to directly access them. The first index of a tuple is 0. For example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
    }

    A tuple without any values has a special name, called theunittuple. This value and its corresponding type are both written as (), representing an empty value or empty return type. If an expression does not return any other value, it implicitly returns the unit value.

    Tuples that are too long cannot be printed

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // Fix the code error
    fn main() {
    let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
    println!("too long tuple: {:?}", too_long_tuple);
    }
    //Fix
    fn main() {
    let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
    println!("too long tuple: {:?}", too_long_tuple);
    }

    Array type

    Unlike tuples, every element in an array must have the same type. Arrays in Rust are different from arrays in some other languages; in Rust, array length is fixed.

    The type of an array is [T; Length]The length of an array is part of its type signature, so the length must be known at compile time,

    The vector type is an array-like collection type provided by the standard library that allows its length to grow and shrink. When unsure whether to use an array or a vector, you should probably use a vector.

    You can write the type of an array like this: within square brackets, include the type of each element, followed by a semicolon, then the number of elements in the array.

    let a: [i32; 5] = [1, 2, 3, 4, 5];

    Here, i32 is the type of each element. After the semicolon, the number 5 indicates that the array contains five elements.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    // Often, we can omit part or all of the array's type and let the compiler infer it for us.
    let arr0 = [1, 2, 3];
    let arr: [char; 3] = ['a', 'b', 'c'];

    // Arrays are allocated on the stack,`std::mem::size_of_val`the function returns the entire memory space occupied by the array.
    // Each char element in the array occupies 4 bytes of memory because in Rust, char is a Unicode character.
    assert!(std::mem::size_of_val(&arr) == 12);
    }

    You can also create an array by specifying the initial value, a semicolon, and then the number of elements in square brackets.an array where every element has the same value.

    1
    let a = [3; 5];

    The array named a will contain 5 elements, all initially set to 3. This syntax is equivalent to let a = [3, 3, 3, 3, 3]; but more concise.

    Accessing Array Elements

    An array is a single memory block of a known fixed size that can be allocated on the stack. You can access array elements using an index, like this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
    }
    fn main() {
    let names = [String::from("Sunfei"), "Sunface".to_string()];

    // `get`returns`Option<T>`type, making its use very safe.
    let name0 = names.get(0).unwrap();

    // However, subscript indexing carries the risk of going out of bounds.
    let _name1 = &names[1];
    }

    Invalid Array Access

    If we access an element beyond the end of the array, the program causes aruntimeError. The program exits with an error message. Whentrying to access an element by index, Rust checks whether the specified index is less than the array length. If the index exceeds the array length, Rust will panic, which is a Rust term used for when a program exits due to an error.

    This check must be performed at runtime, especially in certain cases, because the compiler cannot know what value the user will input when running the code later.

    Type Casting

    Basic type conversion using as

    1. Rust doesnot provide implicit type coercion for basic types, but we can perform explicit conversion using as.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #[allow(unused_variables)]
    fn main() {
    let decimal = 97.123_f32;

    let integer: u8 = decimal as u8;

    let c1: char = decimal as u8 as char;
    let c2 = integer as char;

    println!("c1 is {}",c1);
    assert_eq!(integer, 'b' as u8 - 1);

    println!("Success!")
    }

    2.By default, numeric overflow causes a compilation error, but we can avoid the compilation error by adding a global annotation**#![allow(overflowing_literals)]**(overflow will still occur)

    1
    2
    3
    4
    5
    #![allow(overflowing_literals)]
    fn main() {
    assert_eq!(u8::MAX, 255);
    let v = 1000 as u8;
    }
    1. When converting any numeric value to an unsigned integer type T, if the current value is not within the range of the new type, we canadd or subtract (increase or decrease by T::MAX + 1) from the current value, until the latest value is within the range of the new type. Suppose we want to convert 300 to u8 type. Since the maximum value of u8 is 255, 300 is not within the range of the new type and is greater than the maximum value of the new type, so we need to subtract T::MAX + 1, which is 300 - 256 = 44.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #![allow(overflowing_literals)]
    fn main() {
    assert_eq!(1000 as u16, 1000);

    assert_eq!(1000 as u8, 232);

    // In fact, the previously mentioned rule for positive integers is the following modulo operation
    println!("1000 mod 256 is : {}", 1000 % 256);

    assert_eq!(-1_i8 as u8, 255);

    // Starting from Rust 1.45, when a floating-point number exceeds the range of the target integer, the conversion directly takes the maximum or minimum value of the positive integer range
    assert_eq!(300.1_f32 as u8, 255);
    assert_eq!(-100.1_f32 as u8, 0);


    // The above floating-point conversion has a slight performance cost. If there are extreme performance requirements for a certain piece of code,
    // you can consider the following methods, but the results of these methods may overflow and return some meaningless values
    // In short, please use them with caution
    unsafe {
    // 300.0 is 44
    println!("300.0 is {}", 300.0_f32.to_int_unchecked::<u8>());
    // -100.0 as u8 is 156
    println!("-100.0 as u8 is {}", (-100.0_f32).to_int_unchecked::<u8>());
    // nan as u8 is 0
    println!("nan as u8 is {}", f32::NAN.to_int_unchecked::<u8>());
    }
    }
    1. Raw pointers can be converted to and from integers representing memory addresses
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    fn main() {
    let mut values: [i32; 2] = [1, 2];
    let p1: *mut i32 = values.as_mut_ptr();
    let first_address = p1 as usize;
    let second_address = first_address + 4; // 4 == std::mem::size_of::<i32>()
    let p2 = second_address as *mut i32;
    unsafe {
    *p2 += 1;
    }
    assert_eq!(values[1], 3);

    println!("Success!")
    }
    fn main() {
    let arr :[u64; 13] = [0; 13];
    assert_eq!(std::mem::size_of_val(&arr), 8 * 13);
    let a: *const [u64] = &arr;
    let b = a as *const [u8];
    unsafe {
    assert_eq!(std::mem::size_of_val(&*b), 13)
    }
    }

    From/Into

    1. The From trait allows a typeto define how to create itself based on another type, thus it provides a very convenient way of type conversion.
    2. From and Into are paired; as long as we implement the former, the latter will be automatically implemented: As long as impl Fromfor U is implemented, the following two methods can be used: let u: U = U::from(T) and let u: U = T.into(). The former is provided by the From trait, while the latter is provided by the automatically implemented Into trait.
    3. It should be noted that when using the into method, explicit type annotation is required, because the compiler is likely unable to deduce the required type for us.

    Example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    fn main() {
    let my_str = "hello";

    // The following three conversions all rely on the fact that String implements the From<&str> trait
    let string1 = String::from(my_str);
    let string2 = my_str.to_string();
    // An explicit type annotation is needed here
    let string3: String = my_str.into();
    }
    fn main() {
    // impl From<bool> for i32
    let i1:i32 = false.into();
    let i2:i32 = i32::from(false);
    assert_eq!(i1, i2);
    assert_eq!(i1, 0);

    // Fix the error below using two methods
    // 1. Which type implements the From trait: impl From<char>for ? , we can check the previously mentioned documentation to find the appropriate type
    // 2. A keyword introduced in the previous chapter
    let i3: i32 = 'a'.into();

    // Use two methods to resolve the error
    let s: String = 'a' as String;

    println!("Success!")
    }

    //First method
    fn main() {
    // impl From<bool> for i32
    let i1:i32 = false.into();
    let i2:i32 = i32::from(false);
    assert_eq!(i1, i2);
    assert_eq!(i1, 0);

    let i3:u32 = 'a'.into();

    let s: String = 'a'.into();

    println!("Success!")
    }

    //Second method
    fn main() {
    // impl From<bool> for i32
    let i1:i32 = false.into();
    let i2:i32 = i32::from(false);
    assert_eq!(i1, i2);
    assert_eq!(i1, 0);

    let i3: u32 = 'a' as u32 ;

    let s: String = String::from('a');
    }

    Implement the From trait for a custom type

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // From is included in`std::prelude`, so we don't need to manually bring it into the current scope
    // use std::convert::From;

    #[derive(Debug)]
    struct Number {
    value: i32,
    }

    impl From<i32> for Number {
    // Implement`from`method
    fn from(item: i32) -> Self {
    Number { value: item }
    }
    }

    // Fill in the blank
    fn main() {
    let num = Number::from(30);
    assert_eq!(num.value, 30);

    let num: Number = 30.into();
    assert_eq!(num.value, 30);

    println!("Success!")
    }

    When performing error handling, implementing the From trait for our custom error type is very useful. This way, we canautomatically convert a certain error type into our custom error type using the ? operator

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    use std::fs;
    use std::io;
    use std::num;

    enum CliError {
    IoError(io::Error),
    ParseError(num::ParseIntError),
    }

    impl From<io::Error> for CliError {
    fn from(error: io::Error) -> Self {
    CliError::IoError(error)
    }
    }

    impl From<num::ParseIntError> for CliError {
    fn from(error: num::ParseIntError) -> Self {
    CliError::ParseError(error)
    }
    }

    fn open_and_parse_file(file_name: &str) -> Result<i32, CliError> {
    // ? automatically converts io::Error to CliError
    let contents = fs::read_to_string(&file_name)?;
    // num::ParseIntError -> CliError
    let num: i32 = contents.trim().parse()?;
    Ok(num)
    }

    fn main() {
    println!("Success!")
    }

    TryFrom / TryInto

    Similar to From and Into, TryFrom and TryInto are also generic traits for type conversion.

    However, unlike From/Into,TryFrom and TryInto can handle failures after conversion and return a Result

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn main() {
    let n: i16 = 256;

    // The Into trait has a method`into`,
    // Therefore, TryInto has a method called ?
    let n: u8 = match n.try_into() {
    Ok(n) => n,
    Err(e) => {
    println!("there is an error when converting: {:?}, but we catch it", e.to_string());
    0
    }
    };

    assert_eq!(n, 0);

    println!("Success!")
    }

    Custom Implementation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #[derive(Debug, PartialEq)]
    struct EvenNum(i32);

    impl TryFrom<i32> for EvenNum {
    type Error = ();

    // Implement`try_from`
    fn try_from(value: i32) -> Result<Self, Self::Error> {
    if value % 2 == 0 {
    Ok(EvenNum(value))
    } else {
    Err(())
    }
    }
    }

    fn main() {
    assert_eq!(EvenNum::try_from(8), Ok(EvenNum(8)));
    assert_eq!(EvenNum::try_from(5), Err(()));

    // Fill in the blank
    let result: Result<EvenNum, ()> = 8i32.try_into();
    assert_eq!(result, Ok(EvenNum(8)));
    let result: Result<EvenNum, ()> = 5i32.try_into();
    assert_eq!(result,Err(()));

    println!("Success!")
    }

    Other Conversions

    Converting any type to String

    As long as the ToString trait is implemented for a type, any type can be converted to String. In fact, this approach is not the best; you can use the fmt::Display trait? It controls how a type is printed, and when implemented, it automatically implements ToString. Because to_string is based on fmt::Display

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    use std::fmt;

    struct Point {
    x: i32,
    y: i32,
    }

    impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    write!(f, "The point is ({}, {})", self.x, self.y)
    }
    }

    fn main() {
    let origin = Point { x: 0, y: 0 };
    assert_eq!(origin.to_string(), "The point is (0, 0)");
    assert_eq!(format!("{}", origin), "The point is (0, 0)");

    println!("Success!")
    }

    Parsing a String

    Using the parse method, a String can be converted to an i32 number, because the standard library implements FromStr for the i32 type: impl FromStr for i32

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // To use `from_str` method, you needs to introduce this trait into the current scope.
    use std::str::FromStr;
    fn main() {
    let parsed: i32 = "5".parse().unwrap();
    let turbo_parsed = "10".parse::<i32>().unwrap();
    let from_str = i32::from_str("20").unwrap();
    let sum = parsed + turbo_parsed + from_str;
    assert_eq!(sum, 35);

    println!("Success!")
    }

    Custom implementation of the FromStr trait

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    use std::str::FromStr;
    use std::num::ParseIntError;

    #[derive(Debug, PartialEq)]
    struct Point {
    x: i32,
    y: i32
    }

    impl FromStr for Point {
    // Associated type: A type parameter defined within a trait that is 'bound' to that trait.
    type Err = ParseIntError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
    let coords: Vec<&str> = s.trim_matches(|p| p == '(' || p == ')' )
    .split(',')
    .collect();

    let x_fromstr = coords[0].parse::<i32>()?;
    let y_fromstr = coords[1].parse::<i32>()?;

    Ok(Point { x: x_fromstr, y: y_fromstr })
    }
    }
    fn main() {
    let p = "(3,4)".parse::<Point>();
    assert_eq!(p.unwrap(), Point{ x: 3, y: 4} )
    }

    transmute

    std::mem::transmuteis an unsafe function that caninterpret one type as another type at the bit level, where these two typesmust have the same number of bits

    transmute is equivalent to moving one type bitwise into another type; it copies all bits of the source value into the target value and then forgets the source value. This function is similar to the memcpy function in C.

    Because of this,transmute **it is extremely unsafe!**The caller must ensure the safety of the code themselves, which is, of course, the purpose of unsafe.

    Example

    1. transmute can convert a pointer into a function pointer; this conversion is not portable because function pointers and data pointers may have different sizes on different machines.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fn foo() -> i32 {
    0
    }

    fn main() {
    let pointer = foo as *const ();
    let function = unsafe {
    std::mem::transmute::<*const (), fn() -> i32>(pointer)
    };
    assert_eq!(function(), 0);
    }
    1. transmute can also extend or shorten the lifetime of an invariant, i.e.,an ‘illegal conversion’ of lifetimes!
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // R is a struct that wraps a reference. It does not store the i32 itself, but rather a reference to an i32
    struct R<'a>(&'a i32);

    unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> {
    std::mem::transmute::<R<'b>, R<'static>>(r)
    }

    unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>)
    -> &'b mut R<'c> {
    std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r)
    }
    1. In fact, we can also use some safe methods to replace transmute.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    fn main() {
    /*Turning raw bytes(&[u8]) to u32, f64, etc.: */
    let raw_bytes = [0x78, 0x56, 0x34, 0x12];

    let num = unsafe { std::mem::transmute::<[u8; 4], u32>(raw_bytes) };

    // use `u32::from_ne_bytes` instead
    // Native endianness
    let num = u32::from_ne_bytes(raw_bytes);
    // or use `u32::from_le_bytes` or `u32::from_be_bytes` to specify the endianness
    // Little endian
    let num = u32::from_le_bytes(raw_bytes);
    assert_eq!(num, 0x12345678);
    // Big endian
    let num = u32::from_be_bytes(raw_bytes);
    assert_eq!(num, 0x78563412);

    /*Turning a pointer into a usize: */
    let ptr = &0;
    let ptr_num_transmute = unsafe { std::mem::transmute::<&i32, usize>(ptr) };

    // Use an `as` cast instead
    let ptr_num_cast = ptr as *const i32 as usize;

    /*Turning an &mut T into an &mut U: */
    let ptr = &mut 0;
    let val_transmuted = unsafe { std::mem::transmute::<&mut i32, &mut u32>(ptr) };


    // ptr as *mut i32: convert &mut i32 to raw pointer *mut i32.
    // as *mut u32: convert the raw pointer further to *mut u32.
    // &mut *(...): convert the raw pointer back to a mutable reference.

    let val_casts = unsafe { &mut *(ptr as *mut i32 as *mut u32) };

    /*Turning an &str into a &[u8]: */
    // this is not a good way to do this.
    let slice = unsafe { std::mem::transmute::<&str, &[u8]>("Rust") };
    assert_eq!(slice, &[82, 117, 115, 116]);

    // You could use `str::as_bytes`
    let slice = "Rust".as_bytes();
    assert_eq!(slice, &[82, 117, 115, 116]);

    // Or, just use a byte string, if you have control over the string
    // literal
    assert_eq!(b"Rust", &[82, 117, 115, 116]);
    }

    Functions

    In Rust code,function and variable namesuse**snake case**snake case style. In snake case, all letters are lowercase and words are separated by underscores

    We define functions in Rust by typing fn followed by the function name and a pair of parentheses. Curly braces tell the compiler where the function body begins and ends.

    Parameters

    We can define as havingparameterparameters) function, parameters are special variables that are part of the function signature. When a function has parameters (formal parameters), specific values (actual arguments) can be provided for these parameters. Technically, these specific values are called arguments (arguments

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    another_function(5);
    }

    fn another_function(x: i32) {
    println!("The value of x is: {x}");
    }

    When defining multiple parameters, separate them with commas

    Statements and Expressions

    The function body consists of a series of statements and an optional trailing expression.

    StatementStatementsis an instruction that performs some action but does not return a value

    ExpressionExpressionsevaluates and produces a value

    Statements do not return values, while expressions evaluate to a value.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    fn main() {
    let x = 5u32;

    let y = {
    let x_squared = x * x;
    let x_cube = x_squared * x;

    // The value of the following expression will be assigned to`y`
    x_cube + x_squared + x
    };

    let z = {
    // A semicolon turns an expression into a statement, so what is returned is no longer the expression`2 * x`'s value, but the value of the statement`()`
    2 * x;
    };

    println!("x is {:?}", x);
    println!("y is {:?}", y);
    println!("z is {:?}", z);
    }

    In the statement let y = 6;, the 6 is an expression that evaluates to the value 6.A function call is an expressionA macro call is an expressionA new block scope created with curly braces is also an expression, for example:

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let y = {
    let x = 3;
    x + 1
    };

    println!("The value of y is: {y}");
    }

    This expression:

    1
    2
    3
    4
    {
    let x = 3;
    x + 1
    }

    is a code block whose value is 4. There is no semicolon at the end of the expression. If you add a semicolon at the end of the expression, it becomes a statement, and statements do not return a value.

    Return value

    Do not name the return value, but declare its type after the arrow (->)

    In Rust, the return value of a function is equivalent to the value of the last expression in the function body. Use the return keyword with a specified value to return early from a function; however, most functions implicitly return the last expression.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
    }

    fn plus_one(x: i32) -> i32 {
    x + 1
    }

    Return type is ()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main(){
    println!("{}",type_of(&println!("helloworld")))
    }
    fn type_of<T>(_: &T) -> String{
    format!("{}",std::any::type_name::<T>())
    }
    //output:
    //helloworld
    //()

    Return type is never

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    use std::thread;
    use std::time;

    fn never_return() -> ! {
    // implement this function, don't modify fn signatures
    loop {
    println!("I return nothing");
    // sleeping for 1 second to avoid exhausting the cpu resource
    thread::sleep(time::Duration::from_secs(1))
    }
    }

    fn main() {
    never_return();
    }

    Diverging function

    A diverging function does not return any value, so it can be used in places where any value is expected to be returned

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    fn main() {
    println!("Success!");
    }

    fn get_option(tp: u8) -> Option<i32> {
    match tp {
    1 => {
    // TODO
    }
    _ => {
    // TODO
    }
    };

    // Here, instead of returning None, use a diverging function instead
    never_return_fn()
    }

    // Implement the following diverging function using three methods
    fn never_return_fn() -> ! {

    }
    fn main() {
    println!("Success!");
    }

    fn get_option(tp: u8) -> Option<i32> {
    match tp {
    1 => {
    // TODO
    }
    _ => {
    // TODO
    }
    };

    never_return_fn()
    }

    // IMPLEMENT this function
    // DON'T change any code else
    fn never_return_fn() -> ! {
    unimplemented!()
    }
    // IMPLEMENT this function in THREE ways
    fn never_return_fn() -> ! {
    panic!()
    }

    // IMPLEMENT this function in THREE ways
    fn never_return_fn() -> ! {
    todo!();
    }
    // IMPLEMENT this function in THREE ways
    fn never_return_fn() -> ! {
    loop {
    std::thread::sleep(std::time::Duration::from_secs(1))
    }
    }

    The difference between unimplemented! and [todo] is that while todo! conveys an intent of implementing the functionality later and the message is “not yet implemented”, unimplemented! makes no such claims. Its message is “not implemented”. Also some IDEs will mark todo!s.

    Call

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #[allow(unused)]

    fn main() {
    get_option(3);
    println!("Success!");
    }

    fn get_option(tp: u8) -> Option<i32> {
    match tp {
    1 => {
    // TODO
    }
    _ => {
    // TODO
    }
    };

    never_return_fn()
    }

    // IMPLEMENT this function
    // DON'T change any code else
    fn never_return_fn() -> ! {
    loop {
    std::thread::sleep(std::time::Duration::from_secs(1))
    }
    }

    Using unimplemented!() and todo!(); will cause the following error

    thread ‘main’ panicked at ‘not implemented’, src\main.rs:24:5

    1
    2
    3
    4
    5
    6
    7
    8
    let _v = match b {
    true => 1,
    // Diverging functions can also be used in`match`expressions, to substitute for any type of value
    false => {
    println!("Success!");
    panic!("we have no value for `false`, but we can panic")
    }
    };

    Control flow

    if expressions

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let number = 3;

    if number < 5 {
    println!("condition was true");
    } else {
    println!("condition was false");
    }
    }

    The code blocks associated with conditions in if expressions are sometimes called *arms

    if/else can be used as an expression for assignment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fn main() {
    let n = 5;

    let big_n =
    if n < 10 && n > -10 {
    println!(" 数字太小,先增加 10 倍再说");

    10 * n
    } else {
    println!("数字太大,我们得让它减半");

    n / 2
    };

    println!("{} -> {}", n, big_n);
    }

    Note

    1. The condition in the codemust be a boolvalue. If the condition is not a bool value, we will get an error. Rust does not attempt to automatically convert non-boolean values to boolean. You must always explicitly use a boolean value as the condition for if.

    2. If more than one else if is used, it’s better to refactor the code using match

    Using if in let statements

    Because if is an expression, we can use it on the right side of a let statement

    1
    2
    3
    4
    5
    6
    fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
    }

    The possible return values of each branch of if must be of the same type

    Note

    If the expression in the if block returns an integer, while the expression in the else block returns a string. This is not feasible because a variable must have only one type. Rust needs to know the exact type of a variable at compile time.

    Loops

    loop

    1
    2
    3
    4
    5
    fn main() {
    loop {
    println!("again!");
    }
    }
    Returning Values from Loops
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let mut counter = 0;

    let result = loop {
    counter += 1;

    if counter == 10 {
    break counter * 2;
    }
    };

    println!("The result is {result}");
    }
    Loop Labels

    If there are nested loops, break and continue apply to the innermost loop at that point. You can optionally specify aloop labelloop label) on a loop, and then use the label with break or continue to have those keywords apply to the labeled loop instead of the innermost loop

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    fn main() {
    let mut count = 0;
    'counting_up: loop {
    println!("count = {count}");
    let mut remaining = 10;

    loop {
    println!("remaining = {remaining}");
    if remaining == 9 {
    break;
    }
    if count == 2 {
    break 'counting_up;
    }
    remaining -= 1;
    }

    count += 1;
    }
    println!("End count = {count}");
    }

    while

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fn main() {
    let mut number = 3;

    while number != 0 {
    println!("{number}!");

    number -= 1;
    }

    println!("LIFTOFF!!!");
    }

    for

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
    println!("the value is: {element}");
    }
    }

    Using a for loop to iterate over collection elements enhances code safety compared to a while loop, and eliminates bugs that could arise from going beyond the end of an array or not traversing far enough and missing some elements

    For iterable objects that do not implement Copy, for in takes ownership

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fn main() {
    let names = [String::from("liming"),String::from("hanmeimei")];
    for name in &names {
    // do something with name...
    }

    println!("{:?}", names);

    let numbers = [1, 2, 3];
    // The elements in numbers implement Copy, so there is no need to transfer ownership
    for n in numbers {
    // do something with name...
    }

    println!("{:?}", numbers);
    }

    Iterating over an array by index and value

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let a = [4,3,2,1];

    // Iterating over an array by index and value`a`
    for (i,v) in a.iter().enumerate() {
    println!("第{}个元素是{}",i+1,v);
    }
    }

    Range

    It is a type provided by the standard library used to generate a sequence of all numbers starting from one number up to but not including another number. (The ending number is excluded)

    The rev method can reverse a range

    1
    2
    3
    4
    5
    6
    fn main() {
    for number in (1..4).rev() {
    println!("{number}!");
    }
    println!("LIFTOFF!!!");
    }

    Ownership, References, and Borrowing

    Stack and Heap

    Both the stack and the heap are memory available to your code to use at runtime, but they are structured differently. The stack stores values in the order it gets them and removes the values in the opposite order. This is also referred to aslast in, first outlast in, first out)。

    Adding data is calledpushing onto the stackpushing onto the stack), and removing data is calledpopping off the stackpopping off the stack). All data on the stack must occupy a known, fixed size.

    Data with an unknown size at compile time or a size that might change must be stored on the heap instead.The heap is less organized: when you put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot in the heap that is big enough, marks it as being in use, and returns a pointer, which is the address of that location.pointerpointer). This process is calledallocating on the heapallocating on the heap), or sometimes just “allocating.” (Pushing values onto the stack is not considered allocating.)**Because the pointer to the data placed on the heap is known and its size is fixed, you can store that pointer on the stack.**However, when the actual data is needed, the pointer must be accessed.

    **Pushing onto the stack is faster than allocating memory on the heap because the allocator does not need to search for memory space to store new data; its location is always at the top of the stack.**In contrast, allocating memory on the heap requires more work because the allocator must first find a memory space large enough to hold the data and then make some records to prepare for the next allocation.

    Accessing data on the heap is slower than accessing data on the stack because it must be accessed through a pointer. Modern processors are faster with fewer jumps in memory (caching), and for the same reason, processors work better when the data they are processing is close together (like on the stack) rather than far apart (like possibly on the heap).

    When your code calls a function, the values passed to the function (including pointers that may point to data on the heap) and the function’s local variables are pushed onto the stack. When the function ends, these values are popped off the stack.

    Tracking which parts of code are using which data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap to ensure space is not exhausted—these are the problems that the ownership system addresses. Once you understand ownership, you won’t need to think about the stack and heap often, but understanding that the main purpose of ownership is to manage heap data helps explain why ownership works the way it does.

    Ownership Rules

    1. Each value in Rust has an owner.
    2. There can be only one owner at a time.
    3. When the owner (variable) goes out of scope, the value will be dropped.

    Example of Ownership:

    Modify the code below:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let s = give_ownership();
    println!("{}", s);
    }

    // Only modify the code below!
    fn give_ownership() -> String {
    let s = String::from("hello, world");
    // convert String to Vec
    // Convert String to Vec type
    let _s = s.into_bytes();//into_bytes transfers ownership
    s
    }

    method

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fn main() {
    let s = give_ownership();
    println!("{}", s);
    }

    // Only modify the code below!
    fn give_ownership() -> String {
    let s = String::from("hello, world");
    // convert String to Vec
    let _s = s.as_bytes();//as_bytes does not transfer ownership
    s
    }

    or

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    let s = give_ownership();
    println!("{}", s);
    }

    // Only modify the code below!
    fn give_ownership() -> String {
    let s = String::from("hello, world");
    s
    }

    When ownership is transferred, mutability can also change accordingly.

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let s = String::from("hello, ");

    let mut s1 = s;

    s1.push_str("world")
    }

    Variables and Scope

    A scope is the range within a program for which an item is valid. Consider a variable like this:

    let s = “hello”;

    The variable s is bound to a string literal, which ishardcodedinto the program code. This variable is valid from the point it is declared until the currentscopeends. The comments in Example 4-1 indicate where the variable s is valid.

    1
    2
    3
    4
    5
    {                      // s is not valid here, it is not yet declared
    let s = "hello"; // From this point onward, s is valid

    // using s
    } // This scope has ended, s is no longer valid
    • When senters scope, it is valid.
    • This continues until itleaves scope.

    str and &str

    Normally we cannot use the str type, but we can use &str instead

    1
    2
    3
    fn main() {
    let s: &str = "hello, world";
    }

    To use the str type, it must be paired with Box.

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let s: Box<str> = "hello, world".into();
    greetings(s)
    }

    fn greetings(s: Box<str>) {
    println!("{}",s)
    }

    & can be used to convert Boxto &str type, Rust’sDeref coercionwill&Box<str>automatically convert to&str

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let s: Box<str> = "hello, world".into();
    greetings(&s)
    }

    fn greetings(s: &str) {
    println!("{}",s)
    }

    String type

    String is a type defined in the standard library, allocated on the heap, and can grow dynamically. Its underlying storage is a dynamic byte array (Vec), but unlike a byte array,String is UTF-8 encoded

    Difference between Unicode and encoding methods

    • Unicode
    • is a character set that specifies a uniquecode point for each character
    • Code point form:U+0000~U+10FFFF
    • For example:
      • U+4F60→ ‘you’
      • U+1F600→ 😀
    • UTF-8 / UTF-16 / UTF-32
    • is the method of converting Unicode code points intobyte sequencesspecific method
    • In other words, Unicode is the ‘character table’, and UTF-8 is the ‘encoding rule for how to store or transmit these characters’

    String.chars() and String.bytes() iterate over Unicode characters and bytes respectively.

    • The length of a Unicode character is not fixed
    • A Unicode character is not necessarily a fully displayed character

    In Unicode and text processing,a grapheme clusteris a user-perceived ‘character unit’, meaning it is a complete character seen by the user, but it may consist of multiple Unicode scalar values (char).

    Simply put:

    • A grapheme cluster ≈ ‘a fully displayed character’
    • Unlike Rust’scharcharis a single Unicode scalar value (which could be a letter, a Chinese character, or a component of an emoji)
    • A grapheme cluster may contain:
      • Base character + combining marks (such as diacritics)
      • Emoji sequences (e.g., 👨‍👩‍👧‍👦 family emoji, composed of multiple emojis and zero-width joiners)

    To iterate over grapheme clusters, a third-party crate is needed, such as:

    String manages data allocated on the heap, so it can store text whose size is unknown at compile time. You can create a String from a string literal using the from function

    1
    let s = String::from("hello");

    OkayModify this type of string:

    1
    2
    3
    4
    5
    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() appends a literal to a string

    println!("{}", s); // will print`hello, world!`

    Memory and Allocation

    In the case ofstring literals, we know their contents at compile time, sothe text is directly hardcoded into the final executable. This makes string literals fast and efficient. However, these properties come only from the immutability of string literals. Unfortunately, we cannot put a blob of memory into the binary for each piece of text whose size is unknown at compile time and whose size might change while the program is running.

    For the String type, in order to support a mutable, growable piece of text, we need to allocate an amount of memory on the heap, unknown at compile time, to hold the content. This means:

    • The memory must be requested from the memory allocator at runtime.
    • We need a way of returning this memory to the allocator when we’re done with our String (garbage collection in some languages).

    Rust takes a different path:The memory is automatically freed once the variable that owns it goes out of scope.. Below is a version of the scoping example from Listing 4-1 that uses a String instead of a string literal:

    1
    2
    3
    4
    5
    6
    {
    let s = String::from("hello"); // From this point forward, s is valid

    // using s
    } // This scope is now over,
    // s is no longer valid

    This is a natural point to return the memory needed by the String to the allocator: when s goes out of scope. When a variable goes out of scope, Rust calls a special function for us. This function is calleddrop, where the author of String can place the code to free the memory. Rust calls drop automatically at the closing }.

    Ways Variables and Data Interact

    move

    Stack-Only Data

    1
    2
    let x = 5;
    let y = x;

    binds 5 to x; then makes a copy of the value in x and binds it to y. Now we have two variables, x and y, both equal to 5

    Because integers are simple values with a known, fixed size, these two 5s are placed on the stack.

    For such data, moving and cloning are the same

    1
    2
    let s1 = String::from("hello");
    let s2 = s1;

    A String is made up of three parts:

    1. a pointer to the memory that holds the contents of the string
    2. a length, len, which is the number of bytes that the contents of the string is currently using
    3. A capacity refers to the total number of bytes a String has obtained from the operating system.

    The aboveStored on the stack, the part storing the string content is stored on the heap

    When we assign s1 to s2, if the String’s data is copied, it means we copy its pointer, length, and capacity from the stack. We do not copy the heap data pointed to by the pointer.

    When the variable goes out of scope, drop is called, leading to a double free

    To ensure memory safety

    • Rust does not attempt to copy the allocated memory
    • Rust invalidates s1, meaning that when variable s1 goes out of scope, nothing needs to be freed (corresponding to ownership rule 2: a value has exactly one owner at any given time)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}, world!", s1);
    warning: unused variable: `s2`
    --> src\main.rs:3:9
    |
    3 | let s2 = s1;
    | ^^ help: if this is intentional, prefix it with an underscore: `_s2`
    |
    = note: `#[warn(unused_variables)]` on by default

    error[E0382]: borrow of moved value: `s1`
    --> src\main.rs:5:28
    |
    2 | let s1 = String::from("hello");
    | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
    3 | let s2 = s1;
    | -- value moved here
    4 |
    5 | println!("{}, world!", s1);
    | ^^ value borrowed here after move
    |
    = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

    For more information about this error, try `rustc --explain E0382`.
    warning: `loop_test` (bin "loop_test") generated 1 warning
    error: could not compile `loop_test` due to previous error; 1 warning emitted

    Rust’s approach differs from a shallow copy becauseit invalidates the copied entity while performing a shallow copy, thus using a new term: Move

    Implicit design principle : Rust does not automatically create deep copies of data

    Because in terms of runtime performance, any automatic assignment operation is cheap.

    partial move

    When destructuring a variable, you can use both move and reference pattern bindings simultaneously. When doing so, a partial move occurs:Ownership of part of the variable is transferred to other variables, while we obtain a reference to another part.

    In this case,the original variable can no longer be used, but the part whose ownership was not transferred can still be used, that is, the part that was previously referenced.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    fn main() {
    #[derive(Debug)]
    struct Person {
    name: String,
    age: Box<u8>,
    }

    let person = Person {
    name: String::from("Alice"),
    age: Box::new(20),
    };

    // Through this destructuring pattern matching, ownership of person.name is transferred to the new variable`name`
    // However, here`age`variable is a reference to person.age, and the use of ref here is equivalent to: let age = &person.age
    let Person { name, ref age } = person;

    println!("The person's age is {}", age);

    println!("The person's name is {}", name);

    // Error! The reason is that part of person has already had its ownership transferred, so we can no longer use it
    //println!("The person struct is {:?}", person);

    // Although`person`as a whole can no longer be used,`person.age`can still be used
    println!("The person's age from person struct is {}", person.age);
    }

    clone

    If you want to deep copy data on the heap, not just data on the stack, you can use the clone method

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let s1=String::from("Hello");
    let s2=s1.clone();
    println!("{},{}",s1,s2);
    #[allow(unused)]

    fn main() {
    let t = (String::from("hello"), String::from("world"));

    let (s1, s2) = t.clone();

    println!("{:?}, {:?}, {:?}", s1, s2, t); // -> "hello", "world", ("hello", "world")
    }

    copy

    Copy trait, which can be used for types like integers that are entirely placed on the stack

    • If a type implements the Copy trait, the old variable remains usable after assignment.
    • If a type or any part of it implements the Drop trait, Rust does not allow it to also implement the Copy trait.

    Any simple scalar type and its combinations are Copy.

    Anything that requires memory allocation or some resource is not Copy.

    Some types that have the Copy trait:

    • All integer types, such as u32.
    • The Boolean type, bool, with values true and false.
    • All floating-point types, such as f64.
    • The character type, char.
    • Tuples, but only if their contained types also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not.

    Ownership and Functions

    Passing a value to a function is similar to assigning it to a variable. Passing a value to a function may move or copy it, just like an assignment statement.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    fn main() {
    let s = String::from("hello"); // s enters scope

    takes_ownership(s); // s's value moves into the function...
    // ... so it is no longer valid here

    let x = 5; // x enters scope

    makes_copy(x); // x should be moved into the function,
    // but i32 is Copy,
    // so x can still be used later

    } // Here, x goes out of scope first, then s. But because s's value has been moved,
    // nothing special

    fn takes_ownership(some_string: String) { // some_string enters scope
    println!("{}", some_string);
    } // Here, some_string goes out of scope and calls`drop`method.
    // the occupied memory is freed

    fn makes_copy(some_integer: i32) { // some_integer enters scope
    println!("{}", some_integer);
    } // Here, some_integer goes out of scope. Nothing special

    When trying to use s after calling takes_ownership, Rust throws a compile-time error.

    Return values and scope

    Return values can also transfer ownership

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    fn main() {
    let s1 = gives_ownership(); // gives_ownership moves the return value
    // to s1

    let s2 = String::from("hello"); // s2 comes into scope

    let s3 = takes_and_gives_back(s2); // s2 is moved into
    // takes_and_gives_back,
    // it also moves the return value to s3
    } // Here, s3 goes out of scope and is dropped. s2 also goes out of scope but was moved,
    // so nothing happens. s1 goes out of scope and is dropped

    fn gives_ownership() -> String { // gives_ownership will
    // move the return value to
    // the function that calls it

    let some_string = String::from("yours"); // some_string comes into scope.

    some_string // returns some_string
    // and moves it out to the calling function
    //
    }

    // takes_and_gives_back will take a string and return that value
    fn takes_and_gives_back(a_string: String) -> String { // a_string enters scope
    //

    a_string // returns a_string and moves it out to the calling function
    }

    The ownership of variables always follows the same pattern:

    • moving the value when assigning it to another variable
    • when a variable holdinga value on the heap goes out of scope, its value will be cleaned up via drop, unless the data has been moved to be owned by another variable.

    If you want a function to use a value without taking ownership, you need to return the parameter passed in

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
    }

    fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of the string

    (s, length)
    }

    This is too cumbersome; Rust provides a feature for using values without taking ownership, calledreferencesreferences)。

    References and Borrowing

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
    }

    fn calculate_length(s: &String) -> usize {
    s.len()
    }

    A referencereference) is like a pointer, becauseit is an address, we can access data belonging to other variables stored at that address.

    Unlike pointers,references guarantee they point to a valid value of a particular type.

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let x = 5;
    // Fill in the blank
    let p = &x;

    println!("x 的内存地址是 {:p}", p); // output: 0x16fa3ac84
    }

    Note: The opposite of using & to reference isdereferencingdereferencing), which uses the dereference operator " * "

    1
    2
    3
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    The &s1 syntax lets us create areference tothe value s1, but does not own it. Because we don’t own the value,when the reference goes out of use, the value it points to is not dropped

    We call the action of creating a referenceborrowingborrowing

    Just as variables are immutable by default, so are references.References (by default) do not allow modifying the value they refer to.

    Rust automatically dereferences in certain situations

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let mut s = String::from("hello, ");

    let p = &mut s;

    p.push_str("world");
    }

    Example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fn main() {
    let mut s = String::from("hello, ");

    borrow_object(&s)
    }

    fn borrow_object(s: &String) {}
    fn main() {
    let mut s = String::from("hello, ");

    push_str(&mut s)
    }

    fn push_str(s: &mut String) {
    s.push_str("world")
    }

    Mutable References

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let mut s = String::from("hello");

    change(&mut s);
    }

    fn change(some_string: &mut String) {
    some_string.push_str(", world");
    }

    Mutable references have a big restriction:Only one mutable reference can exist at a time

    1
    2
    3
    4
    5
    6
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);

    The benefit of this restriction is that Rust can prevent data races at compile time.Data Racedata race) Similar to a race condition, it can be caused by these three behaviors:

    • Two or more pointers access the same data at the same time.
    • At least one pointer is used to write data.
    • There is no mechanism to synchronize data access.

    You can use curly braces to create a new scope, allowing multiple mutable references, but they cannotsimultaneouslyexist:

    1
    2
    3
    4
    5
    6
    7
    let mut s = String::from("hello");

    {
    let r1 = &mut s;
    } // r1 goes out of scope here, so we can create a new reference without any problem

    let r2 = &mut s;

    Another restriction:You cannot have both a mutable reference and an immutable reference at the same time

    Butmultiple immutable references are allowed

    1
    2
    3
    4
    5
    6
    7
    let mut s = String::from("hello");

    let r1 = &s; // No problem
    let r2 = &s; // No problem
    let r3 = &mut s; // Big problem

    println!("{}, {}, and {}", r1, r2, r3);

    Dangling References

    In Rust, the compiler ensures that references will never become dangling: when you have a reference to some data, the compiler guarantees that the data will not go out of scope before the reference does.

    Let’s try to create a dangling reference, Rust will prevent it with a compile-time error:

    Filename: src/main.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    fn main() {
    let reference_to_nothing = dangle();
    }

    fn dangle() -> &String {
    let s = String::from("hello");//s is dropped after the function ends
    &s//Returning a reference, but the address is freed after the function ends
    }
    Compiling loop_test v0.1.0 (C:\Users\cauchy\Desktop\rust\loop_test)
    error[E0106]: missing lifetime specifier
    --> src\main.rs:5:16
    |
    5 | fn dangle() -> &String {
    | ^ expected named lifetime parameter
    |
    = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
    help: consider using the `'static` lifetime
    |
    5 | fn dangle() -> &'static String {
    | +++++++

    For more information about this error, try `rustc --explain E0106`.
    error: could not compile `loop_test` due to previous error

    ref

    ref is similar to &, it can be used to get a reference to a value, but their usage differs.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn main() {
    let c = '中';

    let r1 = &c;

    let ref r2 = c;

    assert_eq!(*r1, *r2);

    // Determine whether the strings at two memory addresses are equal
    assert_eq!(get_addr(r1),get_addr(r2));
    }

    // Get the string representation of the memory address of the passed reference
    fn get_addr(r: &char) -> String {
    format!("{:p}", r)
    }

    Summary of reference rules (borrowing rules)

    • At any given time,eitheryou can have only one mutable reference,oryou can have any number of immutable references.
    • References must always be valid.

    Ok: Borrowing immutable from a mutable object

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let mut s = String::from("hello, ");

    borrow_object(&s);

    s.push_str("world");
    }

    fn borrow_object(s: &String) {}

    None Lexical Lifetimes(NLL)

    Non-lexical lifetimes

    Example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Comment out one line of code to make it work
    fn main() {
    let mut s = String::from("hello, ");

    let r1 = &mut s;
    r1.push_str("world");
    let r2 = &mut s;
    r2.push_str("!");

    println!("{}",r1);
    }

    Just comment out the println

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn main() {
    let mut s = String::from("hello, ");

    let r1 = &mut s;
    r1.push_str("world");//The Rust compiler knows that after this, the borrow of s by r1 has ended its lifetime
    let r2 = &mut s;
    r2.push_str("!");

    //println!("{}",r1);
    }
    fn main() {
    let mut x = 22;

    let p = &mut x; // mutable borrow

    println!("{}", x); // later used
    }

    This code compiles successfully because the compiler knows that the mutable borrow of x does not last until the end of the scope, but ends before x is used again, so there is no conflict.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    let mut s = String::from("hello, ");

    let r1 = &mut s;
    let r2 = &mut s;

    // Add a line of code below to artificially create a compilation error: cannot borrow`s` as mutable more than once at a time
    // You cannot use r1 and r2 simultaneously

    }

    Just add r1.push_str(“world”);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    warning: unused variable: `r2`
    --> src/main.rs:5:9
    |
    5 | let r2 = &mut s;
    | ^^ help: if this is intentional, prefix it with an underscore: `_r2`
    |
    = note: `#[warn(unused_variables)]` on by default

    error[E0499]: cannot borrow `s` as mutable more than once at a time
    --> src/main.rs:5:14
    |
    4 | let r1 = &mut s;
    | ------ first mutable borrow occurs here
    5 | let r2 = &mut s;
    | ^^^^^^ second mutable borrow occurs here
    ...
    9 | r1.push_str("world");
    | -- first borrow later used here

    For more information about this error, try `rustc --explain E0499`.
    warning: `rust_programming` (bin "rust_programming") generated 1 warning
    error: could not compile `rust_programming` (bin "rust_programming") due to 1 previous error; 1 warning emitted

    Slice Type

    **slice**allows you to reference a contiguous sequence of elements in a collection rather than the entire collection. A slice is a kind of reference, so it does not have ownership.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
    if item == b' ' {
    return i;
    }
    }
    s.len()
    }

    This function takes a string of words separated by spaces and returns the first word found in that string. If the function does not find a space in the string, then the entire string is one word, so it should return the whole string.

    The first_word function has a parameter &String. Since we don’t need ownership, this is fine. But what should we return? We don’t actually have a way to get apartof the string. However, we can return the index of the end of the word, indicated by a space

    Because we need to check each element of the String for spaces, we need to convert the String to a byte array using the as_bytes method:

    let bytes = s.as_bytes();

    Next, create an iterator over the byte array using the iter method:

    for (i, &item) in bytes.iter().enumerate() {

    Because the enumerate method returns a tuple, we can use pattern matching to destructure it. So in the for loop, we specify a pattern where i in the tuple is the index and &item in the tuple is a single byte. Since we get a reference to the collection element from .iter().enumerate(), we use & in the pattern.

    However, there is a problem. We return an independent usize, but it is only a meaningful number in the context of the &String. In other words, because it is a value separate from the String, there is no guarantee that it will still be valid in the future.

    string slice

    string slicestring slice) is a reference to a portion of a String, and it looks like this:

    1
    2
    3
    4
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];

    [starting_index…ending_index]

    [starting_index…ending_index]

    where starting_index is the first position of the slice, ending_index is thevalue after the last position of the slice.

    If you want to start from index 0, you can omit the value before the two dots

    1
    2
    3
    4
    let s = String::from("hello");

    let slice = &s[0..2];
    let slice = &s[..2];

    If the slice includes the last byte of the String, you can also drop the trailing number

    1
    2
    3
    4
    5
    6
    let s = String::from("hello");

    let len = s.len();

    let slice = &s[3..len];
    let slice = &s[3..];

    You can also omit both values to get a slice of the entire string

    1
    2
    3
    4
    5
    6
    let s = String::from("hello");

    let len = s.len();

    let slice = &s[0..len];
    let slice = &s[..];

    Note:**The indices of a string slice range must lie within valid UTF-8 character boundaries,**if you try to create a string slice from the middle of a multi-byte character, the program will exit with an error.

    1
    2
    3
    4
    5
    6
    fn main() {
    let s = "你好,世界";
    let slice = &s[0..2];
    println!("{}",slice);
    assert!(slice == "你");
    }
    1
    2
    3
    thread 'main' panicked at src/main.rs:3:19:
    byte index 2 is not a char boundary; it is inside '你' (bytes 0..3) of `你好,世界`
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

    String indexing operations in Rusts[i]do not directly return characters because UTF-8 is a variable-length encoding. You need to usechars()orbytes()to iterate or manipulate string content. For example,chars()can iterate over Unicode characters, whilebytes()single bytes.

    Rewrite the function to return a slice (the return type for a string slice can be written as: &str)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
    if item == b' ' {
    return &s[0..i];
    }
    }

    &s[..]
    }

    Call

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // Error! s needs to be a mutable reference

    println!("the first word is: {}", word);
    }

    When you have an immutable reference to a value, you cannot obtain a mutable reference. Because clear needs to empty the String, it attempts to get a mutable reference. The println! after calling clear uses the reference from word, so this immutable reference must still be valid at that point. Rust does not allow the mutable reference in clear and the immutable reference in word to exist simultaneously, so compilation fails

    String literals are slices

    1
    let s = "Hello, world!";

    Here the type of s is &str:**It is a slice pointing to a specific location in the binary program,**This is why string literals are immutable; &str is an immutable reference.

    String slices as parameters

    After learning that we can obtain slices from literals and Strings, we improved first_word, and here is its signature:

    fn first_word(s: &String) -> &str {

    A more experienced Rustacean would write the following signature, because it allows the same function to be used with both &String and &str values:

    fn first_word(s: &str) -> &str {

    If you have a string slice, you can pass it directly. If you have a String, you can pass a slice of the entire String or a reference to the String. This flexibility leverages_deref coercions_The advantage is that defining a function that takes a string slice instead of a String reference makes our API more general and does not lose any functionality:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    fn main() {
    let my_string = String::from("hello world");

    // `first_word`applies to`String`(the slice), whole or entire
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word`also applies to`String`references of
    // This is equivalent to the entire`String`slice of
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word`applies to string literals, whole or entire
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // because string literals are already string slices
    // This also applies, no slice syntax needed!
    let word = first_word(my_string_literal);
    }

    &String can be implicitly converted to &str type

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let mut s = String::from("hello world");

    // Here, &s is of type`&String`type, but`first_character`the function expects`&str`type.
    // Although the two types are different, the code still works because`&String`is implicitly converted to`&str`type
    let ch = first_character(&s);

    println!("the first character is: {}", ch);
    s.clear();
    }
    fn first_character(s: &str) -> &str {
    &s[..1]
    }

    Other types of slices

    String slices are for strings. However, there is also a more general slice type. Consider this array:

    let a = [1, 2, 3, 4, 5];

    Just as we might want to get a part of a string, we might also want to reference a part of an array. We can do this:

    1
    2
    3
    4
    5
    let a = [1, 2, 3, 4, 5];

    let slice = &a[1..3];

    assert_eq!(slice, &[2, 3]);

    The type of this slice is &[i32]. It works the same way as string slices, by storing a reference to the first element of the collection and the total length of the collection. You can use this kind of slice on all other collections.

    Slices are similar to arrays, butthe length of a slice cannot be known at compile time, so youcannot directly use the slice type

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // Fix the errors in the code, do not add new lines of code!
    fn main() {
    let arr = [1, 2, 3];
    let s1: [i32] = arr[0..2];

    let s2: str = "hello, world" as str;
    }
    //After fixing
    fn main() {
    let arr = [1, 2, 3];
    let s1: &[i32] = &arr[0..2];

    let s2: &str = "hello, world" as &str;
    }
    • A slice reference occupies2 wordssized memory space (from now on, for brevity, unless otherwise specified, we uniformly use slice to refer to a slice reference). The slice’sfirst word is a pointer to the data, and the second word is the length of the slice
    • The size of a word depends on the processor architecture; for example, on x86-64, a word is 64 bits or 8 bytes, so a slice reference is 16 bytes in size.
    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let arr: [char; 3] = ['中', '国', '人'];

    let slice = &arr[..2];

    assert!(std::mem::size_of_val(&slice) == 16);
    }
    • A slice (reference) can be used toborrow a contiguous portion of an array, with the corresponding signature being &[T], which can be compared to the array signature [T; Length].
    1
    2
    3
    4
    5
    6
    fn main() {
    let arr: [i32; 5] = [1, 2, 3, 4, 5];

    let slice: &[i32] = &arr[1..4];
    assert_eq!(slice, &[2, 3, 4]);
    }

    Struct

    Defining a struct

    requires using the struct keyword and providing a name for the entire struct.

    Within the curly braces, define the name and type of each piece of data, which we callfieldsfield

    1
    2
    3
    4
    5
    6
    struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
    }

    Instantiation

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
    };
    }

    You can mark an entire struct as mutable when instantiating it, but Rustdoes not allow us to designate a specific field of a struct as mutable.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct Person {
    name: String,
    age: u8,
    }
    fn main() {
    let age = 18;
    let mut p = Person {
    name: String::from("sunface"),
    age,
    };
    p.age = 30;
    p.name = String::from("sunfei");
    }

    Access

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
    }

    Once an instance of a struct is mutable, all fields in that instance are mutable.

    Field Init Shorthand

    When the field name and the variable name corresponding to the field value are the same, the field init shorthand can be used.

    1
    2
    3
    4
    5
    6
    7
    8
    fn build_user(email: String, username: String) -> User {
    User {
    email,
    username,
    active: true,
    sign_in_count: 1,
    }
    }

    Struct Update Syntax

    Creating a new instance based on an existing struct instance

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    // --snip--

    let user2 = User {
    active: user1.active,
    username: user1.username,
    email: String::from("another@example.com"),
    sign_in_count: user1.sign_in_count,
    };
    }

    Using struct update syntax

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    // --snip--

    let user2 = User {
    email: String::from("another@example.com"),
    ..user1
    };
    }

    Tuple Structs

    Tuple structs have the meaning provided by the struct name but no specific field names, only the types of the fields.

    Useful for giving a name to the entire tuple and making the tuple a different type from other tuples.

    1
    2
    3
    4
    5
    6
    7
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
    }

    Accessing such structs is the same as accessing tuples:

    1
    2
    3
    4
    5
    6
    struct Point(i32, i32);

    fn main() {
    let p = Point(10, 20);
    println!("x = {}, y = {}", p.0, p.1);
    }

    Unit-like structs with no fields

    Unit-like structs with no fields, they are similar to ()

    1
    2
    3
    4
    5
    struct AlwaysEqual;

    fn main() {
    let subject = AlwaysEqual;
    }

    Ownership in structs

    In the definition of the User struct in Listing 5-1, we used the owned String type rather than the &str string slice type. This is a deliberate choice because we wantThis struct owns all of its dataTherefore, as long as the entire struct is valid, its data is also valid.

    A struct can store references to data owned by other objects, but doing so requires the use oflifetimeslifetimes

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
    }

    fn main() {
    let user1 = User {
    email: "someone@example.com",
    username: "someusername123",
    active: true,
    sign_in_count: 1,
    };
    }

    Error: missing lifetime specifier

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    error[E0106]: missing lifetime specifier
    --> src/main.rs:3:15
    |
    3 | username: &str,
    | ^ expected named lifetime parameter
    |
    help: consider introducing a named lifetime parameter
    |
    1 ~ struct User<'a> {
    2 | active: bool,
    3 ~ username: &'a str,
    |

    error[E0106]: missing lifetime specifier
    --> src/main.rs:4:12
    |
    4 | email: &str,
    | ^ expected named lifetime parameter
    |
    help: consider introducing a named lifetime parameter
    |
    1 ~ struct User<'a> {
    2 | active: bool,
    3 | username: &str,
    4 ~ email: &'a str,
    |

    For more information about this error, try `rustc --explain E0106`.
    error: could not compile `rust_programming` (bin "rust_programming") due to 2 previous errors

    Printing structs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #[derive(Debug)]
    struct Rectangle{
    width: u32,
    length: u32,
    }
    fn main() {
    let rect=Rectangle{
    width:30,
    length:50,
    };
    println!("{}",area(&rect));
    println!("{:#?}",rect)
    }
    fn area(rect: &Rectangle)->u32{
    rect.width*rect.length
    }

    Methods of a struct

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #[derive(Debug)]
    struct Rectangle{
    width: u32,
    length: u32,
    }
    impl Rectangle{
    fn area(&self)->u32{
    self.width*self.length
    }
    }
    fn main() {
    let rect=Rectangle{
    width:30,
    length:50,
    };
    println!("{}",rect.area());
    println!("{:#?}",rect)
    }
    1. Defining methods within an impl block
    2. The first parameter of a method can be &self, or it can take ownership or a mutable borrow, just like other parameters
    3. Better code organization

    Method call operators

    In C/C++, there are two different operators for calling methods: . calls a method directly on an object, while -> calls a method on a pointer to an object, requiring dereferencing the pointer first. In other words, if object is a pointer, then object->something() is like (*object).something().

    Rust does not have an operator equivalent to ->; instead, Rust has a feature calledautomatic referencing and dereferencingautomatic referencing and dereferencing)Method callsare one of the few places in Rustthat have this behavior

    Here’s how it works: when you call a method with object.something(), Rust automatically adds &, &mut, or * to the object so that it matches the method signature. In other words, these are equivalent:

    1
    2
    p1.distance(&p2);
    (&p1).distance(&p2);

    This automatic referencing behavior works becausemethods have a clear receiver———— the type of self. Given the receiver and method name, Rust can definitively determine whether the method is just reading (&self), making modifications (&mut self), or taking ownership (self).

    Associated functions

    1
    2
    3
    4
    5
    6
    7
    8
    impl Rectangle {
    fn square(size: u32) -> Self {
    Self {
    width: size,
    height: size,
    }
    }
    }

    All functions defined within an impl block are calledassociated functionsassociated functions

    Associated functions that are not methods are often used as constructors that return a new instance of the struct. These functions are typically named new, butnew is not a keyword

    Use the struct name and :: syntax to call this associated function: for example, let sq = Rectangle::square(3);. This function is in the struct’s namespace: the :: syntax is used for both associated functions and namespaces created by modules

    Each struct is allowed to have multiple impl blocks.

    Example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    struct Point {
    x: f64,
    y: f64,
    }

    // `Point`The associated functions of are placed in the following`impl`statement block
    impl Point {
    // The usage of associated functions is very similar to constructors
    fn origin() -> Point {
    Point { x: 0.0, y: 0.0 }
    }

    // Another associated function, with two parameters
    fn new(x: f64, y: f64) -> Point {
    Point { x: x, y: y }
    }
    }

    struct Rectangle {
    p1: Point,
    p2: Point,
    }

    impl Rectangle {
    // This is a method
    // `&self`is`self: &Self`syntactic sugar for
    // `Self`is the type of the current calling object, for this example`Self` = `Rectangle`
    fn area(&self) -> f64 {
    // Using the dot operator, you can access`self`the struct fields in
    let Point { x: x1, y: y1 } = self.p1;
    let Point { x: x2, y: y2 } = self.p2;


    // `abs`is a`f64`method of type , which returns the absolute value of the caller
    ((x1 - x2) * (y1 - y2)).abs()
    }

    fn perimeter(&self) -> f64 {
    let Point { x: x1, y: y1 } = self.p1;
    let Point { x: x2, y: y2 } = self.p2;

    2.0 * ((x1 - x2).abs() + (y1 - y2).abs())
    }

    // This method requires the caller to be mutable,`&mut self`is`self: &mut Self`syntactic sugar for
    fn translate(&mut self, x: f64, y: f64) {
    self.p1.x += x;
    self.p2.x += x;

    self.p1.y += y;
    self.p2.y += y;
    }
    }

    // `Pair`holds two integers allocated on the heap
    struct Pair(Box<i32>, Box<i32>);

    impl Pair {
    // This method takes ownership of the caller
    // `self`is`self: Self`syntactic sugar for
    fn destroy(self) {
    let Pair(first, second) = self;

    println!("Destroying Pair({}, {})", first, second);

    // `first`and`second`go out of scope here and are freed
    }
    }

    fn main() {
    let rectangle = Rectangle {
    // Associated functions are called not with the dot operator, but using`::`
    p1: Point::origin(),
    p2: Point::new(3.0, 4.0),
    };

    // Methods are called with the dot operator
    // Note that the method here expects`&self`but we did not use`(&rectangle).perimeter()`to call it, the reason being:
    // The compiler automatically takes a reference for us
    // `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)`
    println!("Rectangle perimeter: {}", rectangle.perimeter());
    println!("Rectangle area: {}", rectangle.area());

    let mut square = Rectangle {
    p1: Point::origin(),
    p2: Point::new(1.0, 1.0),
    };


    // Error!`rectangle`is immutable, but this method requires a mutable object
    //rectangle.translate(1.0, 0.0);
    // TODO ^ Try uncommenting this line to see what happens

    // Yes! Mutable objects can call mutable methods
    square.translate(1.0, 1.0);

    let pair = Pair(Box::new(1), Box::new(2));

    pair.destroy();

    // Error! The previous`destroy`call took`pair`'s ownership
    //pair.destroy();
    // TODO ^ Try uncommenting this line
    }

    Enum

    Define an enum

    1
    2
    3
    4
    enum IpAddrKind {
    V4,
    V6,
    }

    By defining an IpAddrKind enum in the code to represent this concept and listing the possible IP address types, V4 and V6. This is called the enum’svariantsvariants):

    When creating an enum, you can use explicitintegervalues to set the enum variants.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    enum Number {
    Zero,
    One,
    Two,
    }

    enum Number1 {
    Zero = 0,
    One,
    Two,
    }

    // Error, cannot use decimals
    //enum Number2 {
    // Zero = 0.0,
    // One = 1.0,
    // Two = 2.0,
    //}

    // C-like enum
    enum Number2 {
    Zero = 0,
    One = 1,
    Two = 2,
    }

    fn main() {
    // Through`as`you can cast an enum value to an integer type
    assert_eq!(Number::One as u8, Number1::One as u8);
    assert_eq!(Number1::One as u8, Number2::One as u8);
    }

    Enum values

    You can create instances of the two different members of IpAddrKind like this:

    1
    2
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    Values in enum members can be extracted using pattern matching

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
    }

    fn main() {
    let msg = Message::Move{x: 1, y: 1};

    if let Message::Move{x:a,y: b} = msg {
    // It can also be written as
    // if let Message::Move{x, y} = msg {
    assert_eq!(a, b);
    } else {
    panic!("不要让这行代码运行!");
    }

    }

    Attaching data to enum variants

    Using struct

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    enum IpAddrKind {
    V4,
    V6,
    }

    struct IpAddr {
    kind: IpAddrKind,
    address: String,
    }

    let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
    };

    let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
    };

    Simply use the enum and put data directly into each enum member instead of having the enum as part of a struct. The new definition of the IpAddr enum shows that both V4 and V6 members are associated with a String value:

    1
    2
    3
    4
    5
    6
    7
    8
    enum IpAddr {
    V4(String),
    V6(String),
    }

    let home = IpAddr::V4(String::from("127.0.0.1"));

    let loopback = IpAddr::V6(String::from("::1"));

    Wedirectly attach data to each member of the enum, so there is no need for an extra struct.

    1
    2
    3
    4
    5
    6
    7
    8
    enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
    }

    let home = IpAddr::V4(127, 0, 0, 1);

    let loopback = IpAddr::V6(String::from("::1"));

    Note that even though the standard library contains a definition for IpAddr, we can still create and use our own definition without conflict because we have not brought the standard library’s definition into scope.

    Enums can embed multiple types

    1
    2
    3
    4
    5
    6
    enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
    }
    • Quit has no associated data.
    • Move is similar to a struct containing named fields.
    • Write contains a single String.
    • ChangeColor contains three i32s.

    Defining functions in enums

    Structs and enums have another similarity: just asyou can use impl to define methods for structs, you can also define methods on enums. Here is a method called call defined on our Message enum:

    1
    2
    3
    4
    5
    6
    7
    8
    impl Message {
    fn call(&self) {
    // Define the method body here
    }
    }

    let m = Message::Write(String::from("hello"));
    m.call();

    The method body uses self to get the value on which the method is called. In this example, we created a variable m with the value Message::Write(String::from(“hello”)), and that is what self will be in the call method when m.call() runs.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #[derive(Debug)]
    enum TrafficLightColor {
    Red,
    Yellow,
    Green,
    }

    // implement TrafficLightColor with a method
    impl TrafficLightColor {
    fn color(&self) -> String {
    match *self {
    TrafficLightColor::Red => "red".to_string(),
    TrafficLightColor::Yellow => "yellow".to_string(),
    TrafficLightColor::Green => "green".to_string(),
    }
    }
    }

    fn main() {
    let c = TrafficLightColor::Yellow;

    assert_eq!(c.color(), "yellow");

    println!("{:?}", c);
    }

    The Option enum

    Defined in the standard library, in the prelude

    Rust does not have Null, but provides an enum similar to the concept of Null - Option, which is defined in the standard library

    1
    2
    3
    4
    enum Option<T> {
    None,
    Some(T),
    }

    Use

    1
    2
    3
    4
    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;

    When we have a Some value, we know that a value exists and is held within the Some. When we have a None value, in a sense it means the same thing as null: there is no valid value. So, Optionwhy is it better than null?

    In short, because Optionand T (where T can be any type) are different types, the compiler does not allow using Optionas if it were definitely a valid value. For example, this code won’t compile because it tries to add Optionand i8:

    1
    2
    3
    4
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;

    In fact, the error message means Rust doesn’t know how to add Optionand i8 because their types are different. When you have a value of a type like i8 in Rust, the compiler ensures it always has a valid value. We can use it confidently without null checks. Only when using Option(or whatever type is involved) do we need to worry about possibly not having a value, and the compiler ensures we handle the case of being empty before using the value.

    In other words, before performing an operation on Optionyou must convert it to T.

    To have a value that might be null, you must explicitly put it into an Option of the corresponding type. Then, when using that value, you must explicitly handle the case where it is empty. As long as a value is not of type Option, youcanIt is safely assumed that its value is not null.

    This is a well-considered design decision in Rust tolimit the proliferation of null valuesto increase the safety of Rust code.

    Implementing a linked list with an enum

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    #![allow(unused)]

    enum List {
    // Cons: A node in the linked list that contains a value; the node is a tuple type, where the first element is the node's value and the second element is a pointer to the next node.
    Cons(u32, Box<List>),
    // Nil: The last node in the linked list, indicating the end of the list.
    Nil,
    }

    // Implementing some methods for the enum
    impl List {
    // Create an empty linked list
    fn new() -> List {
    // Since there are no nodes, directly return the Nil node
    // The type of the enum member Nil is List
    Nil
    }

    // Add a new node at the front of the old linked list and return the new linked list
    fn prepend(self, elem: u32) -> List {
    Cons(elem, Box::new(self))
    }

    // Return the length of the linked list
    fn len(&self) -> u32 {
    match *self {
    // Here we cannot take ownership of tail, so we need to get a reference to it and compute recursively
    Cons(_,ref tail) => 1 + tail.len(),
    // The length of an empty linked list is 0
    Nil => 0
    }
    }

    // Returns the string representation of the linked list for printing output
    fn stringify(&self) -> String {
    match *self {
    Cons(head, ref tail) => {
    // Recursively generate a string
    format!("{}, {}", head, tail.stringify())
    },
    Nil => {
    format!("Nil")
    },
    }
    }
    }

    fn main() {
    // Create a new linked list (also empty)
    let mut list = List::new();

    // Add some elements
    list = list.prepend(1);
    list = list.prepend(2);
    list = list.prepend(3);

    // Print the current state of the list
    println!("链表的长度是: {}", list.len());
    println!("{}", list.stringify());
    }

    Pattern matching

    The match control flow construct

    Rust has an extremely powerful control flow operator called match that allows us toa valuewitha series of patternscompare and execute code based on the matching pattern

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
    }

    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => 1,
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter => 25,
    }
    }
    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => {
    println!("Lucky penny!");
    1
    }
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter => 25,
    }
    }

    matches!

    matches! looks like match, but it can do something special

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let alphabets = ['a', 'E', 'Z', '0', 'x', '9' , 'Y'];

    // fill the blank with `matches!` to make the code work
    for ab in alphabets {
    assert!(matches!(ab, 'a'..='z' | 'A'..='Z' | '0'..='9'))
    }
    }

    The code below will error because enums do not implement PartialEq by default, so they cannot be compared with ==

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    enum MyEnum {
    Foo,
    Bar
    }

    fn main() {
    let mut count = 0;

    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
    for e in v {
    if e == MyEnum::Foo { // Fix the error, only modify this line of code
    count += 1;
    }
    }

    assert_eq!(count, 2);
    }
    Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo)
    error[E0369]: binary operation `==` cannot be applied to type `MyEnum`
    --> src\main.rs:13:14
    |
    13 | if e == MyEnum::Foo { // Fix error, can only modify this line of code
    | - ^^ ----------- MyEnum
    | |
    | MyEnum
    |
    note: an implementation of `PartialEq<_>` might be missing for `MyEnum`
    --> src\main.rs:3:1
    |
    3 | enum MyEnum {
    | ^^^^^^^^^^^ must implement `PartialEq<_>`
    help: consider annotating `MyEnum` with `#[derive(PartialEq)]`
    |
    3 | #[derive(PartialEq)]
    |

    For more information about this error, try `rustc --explain E0369`.
    error: could not compile `demo` due to previous error

    Change to

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    enum MyEnum {
    Foo,
    Bar
    }

    fn main() {
    let mut count = 0;

    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
    for e in v {
    if matches!(e, MyEnum::Foo) { // Fix error, can only modify this line of code
    count += 1;
    }
    }

    assert_eq!(count, 2);
    }

    Pattern that binds values

    Another useful feature of match arms is that they can bind to parts of the values that match the pattern. This is how you extract values out of enum variants.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #[derive(Debug)] // This lets us see the name of the state immediately
    enum UsState {
    Alabama,
    Alaska,
    // --snip--
    }

    enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
    }

    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => 1,
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter(state) => {
    println!("State quarter from {:?}!", state);
    25
    }
    }
    }
    fn main(){
    let c= Coin::Quarter(UsState::Alaska);
    println!("{}",value_in_cents(c));
    }

    Matching Option

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fn main(){
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
    }
    fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
    None => None,
    Some(i) => Some(i + 1),
    }
    }//It takes an Option<i32>, if it contains a value, adds one to it. If it doesn't contain a value, the function should return None and not attempt to perform any operations.

    Match must be exhaustive

    Usethe _ placeholder(must be placed last)

    1
    2
    3
    4
    5
    match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),
    }

    if let concise control flow

    Handle cases where you only care about one pattern and ignore the rest

    1
    2
    3
    4
    5
    let config_max = Some(3u8);
    match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),
    _ => (),
    }

    Using if let

    1
    2
    3
    4
    let config_max = Some(3u8);
    if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
    }

    In this example, the pattern is Some(max), and max binds to the value inside Some. Then you can use max within the if let block, just like in the corresponding match arm. The code in the if let block does not execute when the pattern does not match.

    Giving up the possibility of exhaustiveness

    You can think of if let as syntactic sugar for match

    Using with else

    1
    2
    3
    4
    5
    6
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
    } else {
    count += 1;
    }

    Variable shadowing in pattern matching

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn main() {
    let age = Some(30);
    if let Some(age) = age { // Create a new variable that has the same name as the previous`age`variable
    assert_eq!(age, 30);
    } // The new`age`variable goes out of scope here

    match age {
    // `match`can also achieve variable shadowing
    Some(age) => println!("age 是一个新的变量,它的值是 {}",age),
    _ => ()
    }
    }
    //output:
    //age is a new variable with the value 30

    Workspace, Package,Crate,Module

    • PackagePackages): A feature of Cargo that lets you build, test, and share crates.
    • Crate: A tree of modules that forms a library or binary project.
    • moduleModules) anduse: Allows you to control the privacy of scopes and paths.
    • pathpath): A way to name items such as structs, functions, or modules
    LevelNameDescriptionExample
    📦 Package“Package”, a unit managed by Cargo (a project)Contains one or more crates, andCargo.tomlthe entire project directory
    🧩 Crate“Single compilation unit”, can be a library or executableEachrustccompilation is a cratesrc/lib.rsorsrc/main.rs
    📁 ModuleModules, organizing the structure of code within a crateSimilar to C++ namespaces or Python modulesmod network;
    🛣️ PathPaths used to access modules, structs, and functionsSimilar tostd::io::Writecrate::foo::bar()

    Types of Crates

    • binary
    • library

    Each crate is an independent compilation unit.
    The Rust compiler compiles only one crate at a time.

    Crate Root

    Is the source code file from which the Rust compiler starts, forming the root module of the crate.
    The crate root isThe starting file when the Rust compiler builds a crate
    It determinesthe module tree’s top-level structure.


    For example

    Suppose we have the simplest project:

    1
    2
    3
    4
    5
    my_project/
    ├── Cargo.toml
    └── src/
    ├── main.rs
    └── lib.rs

    Both of these files arepossible crate roots

    filecrate typepurpose
    src/main.rsbinary crate rootprogram entry point (must containfn main()
    src/lib.rslibrary crate rootlibrary entry point (defines the module structure of the library)

    compiler’s working logic

    When runningcargo build:

    Cargo will:

    1. ReadCargo.toml
    2. Find thecrate roots(e.g.,src/main.rssrc/lib.rs);
    3. The compiler is invoked for each crate root, starting from this file to build the entire crate’s module tree.

    A Package

    • Contains one Cargo.toml, which describes how to build these Crates
    • Can only contain 0-1 library crate
    • Can contain any number of binary crates
    • But mustcontain at least one crate(library or binary)

    Example:

    1
    2
    3
    4
    5
    6
    7
    8
    my_package/
    ├── Cargo.toml
    ├── src/
    │ ├── lib.rs # library crate
    │ ├── main.rs # Main binary crate
    │ └── bin/
    │ ├── tool1.rs # Second binary crate
    │ └── tool2.rs # Third binary crate

    No special configuration is needed in Cargo.toml. Run:

    1
    2
    3
    4
    5
    6
    # main.rs
    cargo run
    # tool1.rs
    cargo run --bin tool1
    # tool2.rs
    cargo run --bin tool1

    workspace

    A workspace is a collection of multiple packages, each of which can have its own library crate. Example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    my_workspace/
    ├── Cargo.toml # Workspace declaration
    ├── app/ # a binary crate package
    │ ├── Cargo.toml
    │ └── src/main.rs
    ├── utils/ # a library crate package
    │ ├── Cargo.toml
    │ └── src/lib.rs
    └── network/ # another library crate package
    ├── Cargo.toml
    └── src/lib.rs

    workspace declaration toml

    1
    2
    [workspace]
    members = ["app", "utils", "network"]

    Cargo conventions

    src/main.rs

    • crate root of a binary crate
    • crate name same as package name

    src/lib.rs

    • package contains a library crate
    • crate root of a library crate
    • crate name same as package name

    a package can contain both src/main.rs and src/lib.rs

    a package can have multiple binary crates:

    files placed under src/bin, each file is a separate binary crate

    define modules to control scope and privacy

    Module

    • group crates within a crate
    • Control the privacy of items: public, private

    Create a module

    • The mod keyword
    • Can be nested
    • Can contain definitions of other items (struct, enum, constants, traits, functions, etc.)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    mod front_of_house {
    mod hosting {
    fn add_to_waitlist() {}

    fn seat_at_table() {}
    }

    mod serving {
    fn take_order() {}

    fn serve_order() {}

    fn take_payment() {}
    }
    }

    Module tree of the above code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    crate
    └── front_of_house
    ├── hosting
    │ ├── add_to_waitlist
    │ └── seat_at_table
    └── serving
    ├── take_order
    ├── serve_order
    └── take_payment

    src/main.rs and src/lib.rs are called crate roots

    • The content of either of these files forms a module named crate, located at the root of the entire module tree
    • The entire module tree is under the implicit crate module

    Paths

    To find an item in Rust’s module system, you need to usepaths

    • Absolute pathStarts from the crate root, using the crate name or the literal crate
    • Relative pathStarting from the current module, use self, super, or the identifier of the current module

    A path consists of at least one identifier, with identifiers separated by ::

    src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    mod front_of_house {
    mod hosting {
    fn add_to_waitlist() {}
    }
    }

    pub fn eat_at_restaurant() {//Public
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
    }

    Privacy boundary

    • All items in Rust (functions, methods, structs, enums, modules, constants)Are private by default
    • Parent modules cannot access private items of child modules
    • Can be used within child modulesAllItems in ancestor modules
    • Sibling modulesCanCall each other
    • The pub keyword can mark something as public

    super

    super: used to access content in the parent module path, similar to … in the file system

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn serve_order() {}

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

    fn cook_order() {}
    }

    pub struct

    Put pub before a struct

    • The struct is public
    • Fields of a struct are private by defaultAdding pub before a field makes it public

    File name: src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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() {
    // Order a rye toast for breakfast in summer
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change your mind and switch the type of bread you want
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // If you uncomment the next line, the code will not compile;
    // It is not allowed to view or modify the seasonal fruit that comes with breakfast
    // meal.seasonal_fruit = String::from("blueberries");
    }

    pub enum

    Put pub before an enum:

    • The enum is public
    • Variants of an enum are also public by default (no need to add the pub keyword)

    The use keyword

    Bringing paths into scope with the use keyword

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    mod front_of_house {
    pub mod hosting {
    pub fn add_to_waitlist() {}
    }
    }

    use crate::front_of_house::hosting;

    pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    }
    • Still following privacy rules
    • Using use to specify relative paths

    Idiomatic use of use

    • Functions: bring the parent module of the function into scope (specify to parent)

    File name: src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    mod front_of_house {
    pub mod hosting {
    pub fn add_to_waitlist() {}
    }
    }

    use self::front_of_house::hosting;

    pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    }
    • Structs, enums, others: specify the full path (specify to itself)

    File name: src/main.rs

    1
    2
    3
    4
    5
    6
    use std::collections::HashMap;

    fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
    }

    Bringing two items with the same name into scope

    File name: src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use std::fmt;
    use std::io;

    fn function1() -> fmt::Result {
    // --snip--
    }

    fn function2() -> io::Result<()> {
    // --snip--
    }

    Providing a new name with the as keyword

    File name: src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use std::fmt::Result;
    use std::io::Result as IoResult;

    fn function1() -> Result {
    // --snip--
    }

    fn function2() -> IoResult<()> {
    // --snip--
    }

    Re-exporting with pub use

    After bringing a path (name) into scope with use,the name is private within this scope

    If you wantexternal modules to also access through your paththat item (such as a function, struct, module, etc.), you can use pub use for re-exporting

    pub use: re-export

    • Bringing items into scope
    • The item can beexternal codebrought into their scope

    pub(in Crate)

    Sometimes we want an item to be visible only to a specific crate, then we can use the pub(in Crate) syntax.

    Example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    pub mod a {
    pub const I: i32 = 3;

    fn semisecret(x: i32) -> i32 {
    use self::b::c::J;
    x + J
    }

    pub fn bar(z: i32) -> i32 {
    semisecret(I) * z
    }
    pub fn foo(y: i32) -> i32 {
    semisecret(I) + y
    }

    mod b {
    pub(in crate::a) mod c {
    pub(in crate::a) const J: i32 = 4;
    }
    }
    }

    Using external packages

    1. Add the dependent package in Cargo.toml

    2. Use ‘use’ to bring specific items into scope

    • The standard library std is also treated as an external package, but you don’t need to modify Cargo.toml to include std
    • You need to use ‘use’ to bring specific items from std into the current scope

    Use nested paths to clean up large use statements

    Common part of the path::{Differing part of the path}

    1
    2
    use std::{cmp::Ordering,io};
    fn main()

    If one of the two use paths is a subpath of the other

    Use self

    1
    2
    3
    //use std::io;
    //use std::io::Write;
    use std::io::{self,Write}

    Wildcard *

    Using * brings all public items in the path into scope

    Use with caution

    Use cases:

    • prelude
    • Testing, to bring all code under test into the tests module

    Splitting modules into different files

    When defining a module, if the module name is followed by “;” instead of a code block

    • Rust will load the content from a file with the same name as the module
    • The module tree does not change

    Example:

    File name: src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mod front_of_house;

    pub use crate::front_of_house::hosting;

    pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    }

    Example: Declare frontof_house module, whose content will be located in _src/front_of_house.rs

    _src/front_of_house.rs_will get front_of_house module’s definition content, as shown in the example.

    Filename: src/front_of_house.rs

    1
    2
    3
    pub mod hosting {
    pub fn add_to_waitlist() {}
    }

    Example:

    Define modulefront_of_house

    Method 1: Write module content directly in the file

    File:src/front_of_house.rs

    1
    2
    3
    pub mod hosting {
    pub fn add_to_waitlist() {}
    }
    • Herefront_of_housemodule directly defines the submodulehosting
    • Advantage: Simple, can be written this way when the module is small.

    Method 2: Split the submodule into a separate file

    1. Infront_of_house.rsonly declare submodules:
    1
    pub mod hosting;
    1. Create directory and file structure:
    1
    2
    3
    4
    5
    src/
    ├── lib.rs
    ├── front_of_house.rs ← front_of_house 模块的主体
    └── front_of_house/ ← front_of_house 的子模块目录
    └── hosting.rs ← 子模块 hosting 的实现
    1. Inhosting.rswrite the actual content:
    1
    pub fn add_to_waitlist() {}
    • At this point,hostingthe content of is completely placed inhosting.rs
    • Benefit: When modules are large, splitting files makes them clearer and more maintainable.

    Traditional style

    Before Rust Edition 2018, use the following style

    1
    2
    3
    4
    src/
    └── front_of_house/
    ├── mod.rs // front_of_house module body
    └── hosting.rs // front_of_submodules of house

    Common collections

    Vector

    Vecis called vector

    Create a vector

    Vec::new function

    let v:Vec=Vec::new();

    Creating a Vec with initial values, using**vec!**macro

    let v = vec![1,2,3];

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let arr: [u8; 3] = [1, 2, 3];

    let v = Vec::from(arr);
    is_vec(v);

    let v = vec![1, 2, 3];
    is_vec(v);

    // vec!(..) and vec![..] are the same macro; the macro can use three forms: [], (), and {}, so...
    let v = vec!(1, 2, 3);
    is_vec(v);

    // ...in the code below, v is Vec<[u8; 3]>, not Vec<u8>
    let v1 = vec!(arr);

    Adding elements

    1
    2
    let mut v:Vec<i32>=Vec::new();
    v.push(1);

    Deleting a Vector

    Like any other struct, a vector is freed when it goes out of scope

    1
    2
    3
    4
    5
    {
    let v = vec![1, 2, 3, 4];

    // Handling variable v
    } // <- here v goes out of scope and is dropped

    Reading values from a Vector

    • Indexing method
    • get method
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let v = vec![1, 2, 3, 4, 5];

    let third: &i32 = &v[2];
    println!("The third element is {}", third);

    match v.get(2) {
    Some(third) => println!("The third element is {}", third),
    None => println!("There is no third element."),
    }
    • When accessing a value beyond the array elements using the indexing method, the program will panic
    • When accessing with the get method, the program returns None
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    fn main() {
    let mut v = Vec::from([1, 2, 3]);
    for i in 0..5 {
    println!("{:?}", v.get(i))
    }

    for i in 0..5 {
    if let Some(x) = v.get(i) {
    v[i] = x + 1
    } else {
    v.push(i + 2)
    }
    }

    assert_eq!(format!("{:?}",v), format!("{:?}", vec![2, 3, 4, 5, 6]));

    println!("Success!")
    }

    Reference borrowing rules

    When we obtain an immutable reference to the first element of a vector and try to add an element at the end of the vector, attempting to reference that element later in the function will not work

    1
    2
    3
    4
    5
    6
    7
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];//Immutable borrow

    v.push(6);//Mutable borrow

    println!("The first element is: {}", first);//Immutable borrow

    Why does the reference to the first element care about changes at the end of the vector? The reason this cannot be done is due to how vectors work: when adding a new element at the end of a vector, if there is not enough space to store all elements contiguously,it may require allocating new memory and copying the old elements to the new space. At this point,the reference to the first element points to deallocated memory. Borrowing rules prevent the program from falling into this situation

    Iterating over a Vector

    1
    2
    3
    4
    let v = vec![100, 32, 57];
    for i in &v {
    println!("{}", i);
    }

    We can also iterate over mutable references to each element of a mutable vector in order to change them

    1
    2
    3
    4
    let mut v = vec![100, 32, 57];
    for i in &mut v {
    *i += 50;
    }

    To modify the value pointed to by a mutable reference, you must use the dereference operator (*) to obtain the value in i before using the += operator.

    Extending a Vector

    A Vec can be extended using the extend method

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn main() {
    let mut v1 = Vec::from([1, 2, 4]);
    v1.pop();
    v1.push(3);
    // v1 is [1,2,3]

    let mut v2 = Vec::new();
    v2.extend([1, 2, 3]);
    // v2 is [1,2,3]

    assert_eq!(format!("{:?}",v1), format!("{:?}",v2));

    println!("Success!")
    }

    Use enum to store multiple data types in Vec

    Define an enum to store different types of data in a vector

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
    }

    let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
    ];

    Use trait objects to store multiple data types in Vec

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    trait IpAddr {
    fn display(&self);
    }

    struct V4(String);

    impl IpAddr for V4 {
    fn display(&self) {
    println!("ipv4: {:?}",self.0)
    }
    }

    struct V6(String);

    impl IpAddr for V6 {
    fn display(&self) {
    println!("ipv6: {:?}",self.0)
    }
    }

    fn main() {
    // Fill in the blanks
    let v: Vec<Box<dyn IpAddr>> = vec![
    Box::new(V4("127.0.0.1".to_string())),
    Box::new(V6("::1".to_string())),
    ];

    for ip in v {
    ip.display();
    }
    }

    Convert type X (using From/Into traits) to Vec

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    fn main() {
    // array -> Vec
    let arr = [1, 2, 3];
    let v1 = Vec::from(arr);
    let v2: Vec<i32> = arr.into();

    assert_eq!(v1, v2);


    // String -> Vec
    let s = "hello".to_string();
    let v1: Vec<u8> = s.into();

    let s = "hello".to_string();
    let v2 = s.into_bytes();
    assert_eq!(v1, v2);

    let s = "hello";
    let v3 = Vec::from(s);
    assert_eq!(v2, v3);

    println!("Success!")
    }

    Slice

    Similar to String slices, Vec can also use slices. If Vec is mutable, its slice is immutable or read-only, and we can obtain a slice via &.

    In Rust,passing slices as parameters is a more common practicefor example, when a function only needs read access, passing slices &[T] / &str of Vec or String is more appropriate.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    fn main() {
    let mut v = vec![1, 2, 3];

    let slice1 = &v[..];
    // Out-of-bounds access will cause a panic.
    // When modifying, you must use`v.len`
    let slice2 = &v[0..v.len()];

    assert_eq!(slice1, slice2);

    // Slices are read-only
    // Note: slices and`&Vec`are different types; the latter is merely`Vec`reference, and can be directly obtained through dereferencing`Vec`
    let vec_ref: &mut Vec<i32> = &mut v;
    (*vec_ref).push(4);
    let slice3 = &mut v[0..];
    // slice3.push(4);

    assert_eq!(slice3, &[1, 2, 3, 4]);

    println!("Success!")
    }

    capacity

    Capacity is the already allocated memory space used to store future elements added to the Vec. Length (len) is the number of elements currently stored in the Vec. When adding a new element would cause the length to exceed the existing capacity, the capacity automatically grows: Rust reallocates a larger memory block and copies the previous Vec over, thus a new memory allocation occurs here.

    If this code occurs frequently, the frequent memory allocations will significantly impact system performance. The best approach is to pre-allocate sufficient capacity to minimize memory allocations.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    fn main() {
    let mut vec = Vec::with_capacity(10);

    assert_eq!(vec.len(), 0);
    assert_eq!(vec.capacity(), 10);

    // Because sufficient capacity was set in advance, the loop here will not cause any memory allocation...
    for i in 0..10 {
    vec.push(i);
    }
    assert_eq!(vec.len(), 10);
    assert_eq!(vec.capacity(), 10);

    // ...but the code below will cause new memory allocations
    vec.push(11);
    assert_eq!(vec.len(), 11);
    assert!(vec.capacity() >= 11);


    // Fill in an appropriate value so that during the`for`loop execution, no memory allocation occurs
    let mut vec = Vec::with_capacity(100);
    for i in 0..100 {
    vec.push(i);
    }

    assert_eq!(vec.len(), 100);
    assert_eq!(vec.capacity(), 100);

    println!("Success!")
    }

    As long as the From trait is implemented for Vectrait, then T can be converted into Vec.

    String

    • Strings are collections of bytes
    • UTF-8 encoding
    • Some methods can parse bytes into text

    What is a string?

    At the core language level of Rust, there is only one string type: the string slice str (or &str)

    • String slice: a reference to a UTF-8 encoded string stored elsewhere
    • String literal: stored in the binary file, also a string slice
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    fn main() {
    let s = String::from("hello, 世界");
    let slice1 = &s[0..1];
    //Hint:`h`occupies only 1 byte in UTF-8 encoding
    assert_eq!(slice1, "h");

    let slice2 = &s[7..10];// Hint:`中`occupies 3 bytes in UTF-8 encoding
    assert_eq!(slice2, "世");

    // Iterate over all characters in s
    for (i, c) in s.chars().enumerate() {
    if i == 7 {
    assert_eq!(c, '世')
    }
    }

    println!("Success!")
    }

    In fact, String is asmart pointer, which is stored as a struct on the stack and points to the underlying string data stored on the heap.

    The smart pointer struct stored on the stack consists of three parts: a pointer pointing to the byte array on the heap, the used length, and the allocated capacity (the used length is less than or equal to the allocated capacity; when the capacity is insufficient, memory space is reallocated).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    use std::mem;

    fn main() {
    let story = String::from("Rust By Practice");

    // Prevent the String's data from being automatically dropped
    let mut story = mem::ManuallyDrop::new(story);

    let ptr = story.as_mut_ptr();
    let len = story.len();
    let capacity = story.capacity();

    assert_eq!(16, len);

    // We can reconstruct a String based on the ptr pointer, length, and capacity.
    // This operation must be marked as unsafe because we need to ensure the operation is safe ourselves
    let s = unsafe { String::from_raw_parts(ptr, len, capacity) };

    assert_eq!(*story, s);

    println!("Success!")
    }

    Other string types

    String type

    From the standard library, also UTF-8 encoded

    String vs Str: owned or borrowed variants

    Other string types

    OsString / OsStr

    1
    2
    3
    4
    5
    use std::ffi::OsString;
    fn main() {
    let mut os_string = OsString::from("hello");
    os_string.push(" world"); // Mutable
    }

    Mainly used for handling system strings such as file paths, command-line arguments, environment variables, etc.

    CString / CStr

    1
    2
    3
    use std::ffi::CString;

    let c_string = CString::new("hello").expect("NUL found!");

    Fromstd::ffi, used forinterfacing with C language

    Characteristics

    • Ends withNUL terminator (\0) string.
    • CStringOwns the data, guarantees no internal NUL.
    • CStris an immutable borrow, similar to&str

    CStringNo similarpush_stror index access methods to modify content; once created, itcannot modify internal bytes

    Conversion between String and &str

    Using to_string()

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let s = "hello, world".to_string();
    greetings(s)
    }

    fn greetings(s: String) {
    println!("{}",s)
    }

    Using String::from()

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let s = String::from("hello, world");
    greetings(s)
    }

    fn greetings(s: String) {
    println!("{}",s)
    }

    String escaping

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    fn main() {
    // You can use escaping to output desired characters. Here we use hexadecimal values, for example \x73 will be escaped to the lowercase letter 's'
    // Fill in the blank to output "I'm writing Rust"
    let byte_escape = "I'm writing Ru\x73__!";
    println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);

    // You can also use Unicode escape characters
    let unicode_codepoint = "\u{211D}";
    let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";

    println!("Unicode character {} (U+211D) is called {}",
    unicode_codepoint, character_name );

    // You can also use \ to concatenate multi-line strings
    let long_string = "String literals
    can span multiple lines.
    The linebreak and indentation here \
    can be escaped too!";
    println!("{}", long_string);
    }

    Sometimes there are many characters to escape, and we want a more convenient way to write strings: raw string.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    fn main() {
    let raw_str = r"Escapes don't work here: \x3F \u{211D}";
    println!("{}", raw_str);

    // If the string contains double quotes, you can add # at the beginning and end
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // If there is still ambiguity, you can continue adding more, with no limit
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
    }
    fn main() {
    let raw_str = "Escapes don't work here: \x3F \u{211D}";
    assert_eq!(raw_str, "Escapes don't work here: ? ℝ");

    // If you need quotes in a raw string, add a pair of #s
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // If you need "# in your string, just use more #s in the delimiter.
    // You can use up to 65535 #s.
    let delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", delimiter);

    // Fill the blank
    let long_delimiter = r###"Hello, "##""###;
    assert_eq!(long_delimiter, "Hello, \"##\"")
    }

    Here r#" marks the start of a raw string, and "# marks the end of a string. If there is still ambiguity, you can continue adding #

    Create a new String

    String::new()

    let mut s = String::new();

    Using to_string()

    1
    2
    3
    4
    5
    6
    let data = "initial contents";

    let s = data.to_string();

    // This method can also be used directly on string literals:
    let s = "initial contents".to_string();

    You can also use string::from()

    let s = String::from(“initial contents”);

    Updating a String

    push_str()

    1
    2
    let mut s = String::from("foo");
    s.push_str("bar");

    The push_str() method does not take ownership of the parameter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2 is {}", s2);

    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(&s2);
    println!("s2 is {}", s2);

    push()

    The push method is defined to take a single character as a parameter and append it to the String

    1
    2
    let mut s = String::from("lo");
    s.push('l');

    Concatenating strings with +

    Can only concatenateStringwith&strtypes, andStringownership of is moved during this process

    1
    2
    3
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // Note that s1 has been moved and can no longer be used

    The + operator uses the add function, whose signature looks like this:

    1
    fn add(self, s: &str) -> String {

    This is not the actual signature in the standard library;

    But the type of &s2 is &String, not &str. So why does it still compile?

    The reason &s2 can be used in the add call is that &String can becoercedcoerced) into &str. When the add function is called, Rust uses a technique calledDeref coercionderef coercion), which you can think of as turning &s2 into &s2[…].

    format!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = s1 + "-" + &s2 + "-" + &s3;
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{}-{}-{}", s1, s2, s3);

    The code generated by the format! macro uses references, soit does not take ownership of any arguments

    s1,s2,s3are all borrowed.

    Rust willcopy the content into the new string(allocated on the heap):

    • The original string is unaffected.
    • This copy is necessary because a new independent string is created.

    access by indexString

    Rust’s stringsdo not support indexing syntaxaccess

    internal representation

    String is a Vecwrapper.

    let hello = String::from(“Hola”);

    Here, the value of len is 4, which means the Vec storing the string “Hola” is 4 bytes long: each letter’s UTF-8 encoding takes up one byte.

    (Note that the first letter in this string is the Cyrillic letter Ze, not the Arabic numeral 3.)

    1
    let hello = String::from("Здравствуйте");

    When asked how long this character is, someone might say 12. However, Rust’s answer is 24. This is the number of bytes needed to encode “Здравствуйте” in UTF-8, because each Unicode scalar value requires two bytes of storage.

    Therefore, indexing into a string’s byte values does not always correspond to a valid Unicode scalar value

    byte string

    byte string or byte array

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    use std::str;

    fn main() {
    // Note, this is not a`&str`type anymore!
    let bytestring: &[u8; 21] = b"this is a byte string";


    // Byte arrays do not implement`Display`feature, so only`Debug`method can be used to print
    println!("A byte string: {:?}", bytestring);

    // Byte arrays can also use escape
    let escaped = b"\x52\x75\x73\x74 as bytes";
    // ...but do not support Unicode escape
    // let escaped = b"\u{211D} is not allowed";
    println!("Some escaped bytes: {:?}", escaped);


    // raw string
    let raw_bytestring = br"\u{211D} is not escaped here";
    println!("{:?}", raw_bytestring);

    // Converting a byte array to`str`type may fail
    if let Ok(my_str) = str::from_utf8(raw_bytestring) {
    println!("And the same as text: '{}'", my_str);
    }

    let _quotes = br#"You can also use "fancier" formatting, \
    like with normal raw strings"#;

    // Byte arrays may not be in UTF-8 format
    let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82\xbb"; // "ようこそ" in SHIFT-JIS

    // but they may not be convertible to`str`type
    match str::from_utf8(shift_jis) {
    Ok(my_str) => println!("Conversion successful: '{}'", my_str),
    Err(e) => println!("Conversion failed: {:?}", e),
    };
    }

    Bytes, Scalar Values, Grapheme Clusters

    Rust has three ways to view strings:

    • Bytes
    1
    2
    3
    4
    5
    6
    fn main(){
    let w = "नमस्ते";
    for b in w.bytes(){
    println!("{}",b);
    }
    }

    Output

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    224
    164
    168
    224
    164
    174
    224
    164
    184
    224
    165
    141
    224
    164
    164
    224
    165
    135
    • Scalar Values
    1
    2
    3
    4
    5
    6
    fn main(){
    let w = "नमस्ते";
    for b in w.chars(){
    println!("{}",b);
    }
    }

    Output

    1
    2
    3
    4
    5
    6






    • Grapheme Clusters (closest to what are called letters)

    Obtaining them is more complex; the standard library no longer provides this. A third-party library is needed.

    One reason Rust does not allow indexing into a String:

    Indexing operations should take constant time O(1)

    But a String cannot guarantee this: it needs to traverse all the content to determine how many valid characters there are

    Slicing a String

    The type that string indexing should return is ambiguous: byte values, characters, grapheme clusters, or a string slice. Therefore, if you really want to use indexing to create a string slice, Rust requires you to be more explicit. To be more explicit about indexing and indicate that you need a string slice, instead of using [] with a single index, you can use [] with a range to create a string slice containing specific bytes:

    1
    2
    3
    let hello = "Здравствуйте";

    let s = &hello[0..4];

    s will be a &str that contains the first four bytes of the string. Earlier, we mentioned that these letters are each two bytes long, so this means s will be “Зд”.

    What happens if you try to get &hello[0…1]? The answer is: Rust will panic at runtime

    Thereforeslicing must not cross string boundaries

    Iterating over Strings

    You cannot access a character in a string by indexing, but you can use slicing with &s1[start…end], provided that start and end fall exactly on character boundaries.

    • For scalar values: the chars() method
    • For bytes: the bytes() method
    • Regarding grapheme clusters: it’s very complex, the standard library does not provide it, and neither Chinese nor English need to focus on character clusters; using chars is sufficient to iterate over Chinese and English.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    fn main() {
    let s1 = String::from("hi,中国");
    let h = &s1[0..1];
    assert_eq!(h, "h");

    let h1 = &s1[3..6];
    assert_eq!(h1, "中");
    }
    fn main() {
    for c in "你好,世界".chars() {
    println!("{}", c)
    }
    }
    //output:
    //you
    //good
    //,
    //world
    //boundary

    HashMap

    HashMap uses the SipHash 1-3 hashing algorithm by default, which is very effective against HashDos attacks. In terms of performance, if your keys are medium-sized, this algorithm works well, but if your keys are small (e.g., integers) or large (e.g., strings), you need to use other algorithms provided by the community to improve performance.

    The hash table algorithm is based on Google’sSwissTable, you can find the C++ implementation athere.

    Creating HashMap<K,V>

    Creating an empty HashMap: the new() function

    1
    2
    3
    4
    5
    6
    7
        use std::collections::HashMap;

    let mut scores = HashMap::new();
    //let mut scores:HashMap<String,i32> = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
    • HashMap is used less frequently,not in the Prelude
    • the standard library provides limited support for it,There is no built-in macro to create a HashMap
    • Data is stored on the heap
    • Homogeneous, meaning K must be one type and V another type
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    use std::collections::HashMap;
    fn main() {
    let teams = [
    ("Chinese Team", 100),
    ("American Team", 10),
    ("France Team", 50),
    ];

    let mut teams_map1 = HashMap::new();
    for team in &teams {
    teams_map1.insert(team.0, team.1);
    }

    let teams_map2: HashMap<_,_> = teams.into_iter().collect();
    // let teams_map2 = HashMap::from(teams);
    assert_eq!(teams_map1, teams_map2);

    println!("Success!")
    }

    The collect method creates a HashMap

    The collect method can gather data into a variety of collection types

    1
    2
    3
    4
    5
    6
    7
    use std::collections::HashMap;

    let teams = vec![String::from("Blue"), String::from("Yellow")];
    let initial_scores = vec![10, 50];

    let mut scores: HashMap<_, _> =
    teams.into_iter().zip(initial_scores.into_iter()).collect();

    If team names and initial scores are in two separate vectors, you can usezipmethod tocreate an iterator of tuples, where “Blue” pairs with 10, and so on. Then you can use the collect method to convert this iterator of tuples into a HashMap

    HashMap and Ownership

    • For types that implement the Copy trait (such as i32), values are copied into the HashMap
    • For values that own their data (such as String), the values are moved and ownership transfers to the HashMap
    • If you insert references into a HashMap, the values themselves are not moved, but the referenced values must remain valid for as long as the HashMap is valid

    Accessing values in a HashMap

    The get method

    1
    2
    3
    4
    5
    6
    7
    8
    9
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    let team_name = String::from("Blue");
    let score = scores.get(&team_name);

    Iterating over a HashMap with a for loop

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    for (key, value) in &scores {
    println!("{}: {}", key, value);
    }

    This will print each key-value pair inan arbitrary order:

    1
    2
    Yellow: 50
    Blue: 10

    Indexing and the get method

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    use std::collections::HashMap;
    fn main() {
    let mut scores = HashMap::new();
    scores.insert("Sunface", 98);
    scores.insert("Daniel", 95);
    scores.insert("Ashley", 69);
    scores.insert("Katie", 58);

    // get returns an Option<&V> enum
    let score = scores.get("Sunface");
    assert_eq!(score, Some(&98));

    if scores.contains_key("Daniel") {
    // Indexing returns a value V
    let score = scores["Daniel"];
    assert_eq!(score, 95);
    scores.remove("Daniel");
    }

    assert_eq!(scores.len(), 3);

    for (name, score) in scores {
    println!("The score of {} is {}", name, score)
    }
    }

    Updating a HashMap

    Overwriting a value

    1
    2
    3
    4
    5
    6
    7
    8
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);

    println!("{:?}", scores);

    This prints {“Blue”: 25}. The original value 10 is overwritten

    Inserting only if the key has no value

    Using the entry methodInserting only if the key does not have a value

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    use std::collections::HashMap;

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    let e=scores.entry(String::from("Yellow"));
    println!("{:?}",e);
    e.or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{:?}", scores);

    Output:

    1
    2
    Entry(VacantEntry("Yellow"))
    {"Blue": 10, "Yellow": 50}

    The entry method

    Check whether the specified K corresponds to a V

    Takes K as a parameter, returns enum Entry: indicates whether the value exists

    Entry’sor_insert() method:

    Returns:

    • If K exists, returns a mutable reference to the corresponding V
    • If K does not exist, inserts the method parameter as a new value for K, returns a mutable reference to this value

    Update a value based on the old value

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    use std::collections::HashMap;

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

    for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
    }

    println!("{:?}", map);

    Here, or_insert returns a mutable reference pointing to the value corresponding to the key just inserted by the entry method

    Hash function

    HashMap uses a hash function called SipHash by default, which can resist Denial of Service (DoS) attacks involving hash tables1However, this is not the fastest algorithm available, but the performance cost is worth it for higher security. If performance monitoring shows this hash function is too slow for your needs, you can specify a different_hasher_to switch to another function. A hasher is a type that implements the BuildHasher trait. You don’t need to implement your own hasher from scratch; there are many libraries on crates.io providing hashers for common hash algorithms.

    HashMap key restrictions

    Any type that implements the Eq and Hash traits can be used as a key for HashMap, including:

    • bool (though rarely used, as it can only represent two kinds of keys)
    • int, uint, and their variants, such as u8, i32, etc.
    • String and &str (Hint: when the key of a HashMap is of type String, you can actually use &str with the get method for querying)

    Note that f32 and f64 do not implement Hash, becausefloating-point precisionissues prevent them from being compared for equality.

    If all fields of a collection type implement Eq and Hash, then the collection type automatically implements Eq and Hash. For example, Vecto implement Hash, T must first implement Hash.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // Hint:`derive`is a good way to implement some common traits
    use std::collections::HashMap;

    #[derive(Hash, Eq, PartialEq, Debug)]
    struct Viking {
    name: String,
    country: String,
    }

    impl Viking {
    fn new(name: &str, country: &str) -> Viking {
    Viking {
    name: name.to_string(),
    country: country.to_string(),
    }
    }
    }

    fn main() {
    // Use HashMap to store viking health points
    let vikings = HashMap::from([
    (Viking::new("Einar", "Norway"), 25),
    (Viking::new("Olaf", "Denmark"), 24),
    (Viking::new("Harald", "Iceland"), 12),
    ]);

    // Use derive to print the current state of viking
    for (viking, health) in &vikings {
    println!("{:?} has {} hp", viking, health);
    }
    }

    Capacity

    Regarding capacity, we have detailed it in the previous Vector section, and HashMap can also adjust capacity: you can initialize with a specified capacity via HashMap::with_capacity(uint), or use HashMap::new(), which provides a default initial capacity.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    use std::collections::HashMap;
    fn main() {
    let mut map: HashMap<i32, i32> = HashMap::with_capacity(100);
    map.insert(1, 2);
    map.insert(3, 4);
    // In fact, although we initialized with a capacity of 100, the map's capacity is likely to be more than 100
    assert!(map.capacity() >= 100);

    // When shrinking capacity, the value you provide is only an allowed minimum. In practice, Rust will automatically set it based on the current amount of stored data. Of course, this value will be as close as possible to the one you provided, while also potentially reserving some adjustment space.

    map.shrink_to(50);
    assert!(map.capacity() >= 50);

    // Let Rust adjust to an appropriate value on its own; the remaining strategy is the same as above.
    map.shrink_to_fit();
    assert!(map.capacity() >= 2);
    println!("Success!")
    }

    Ownership

    For types that implement the Copy trait, such as i32, the value of that type will be copied into the HashMap. For types with ownership, such as String, ownership of their values will be transferred to the HashMap.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    use std::collections::HashMap;
    fn main() {
    let v1 = 10;
    let mut m1 = HashMap::new();
    m1.insert(v1, v1);
    println!("v1 is still usable after inserting to hashmap : {}", v1);

    let v2 = "hello".to_string();
    let mut m2 = HashMap::new();
    // Ownership is transferred here.
    m2.insert(v2, v1);

    assert_eq!(v2, "hello");

    println!("Success!")
    }

    Third-party Hash Libraries

    At the beginning, we mentioned that if the performance of the existing SipHash 1-3 cannot meet the requirements, alternative algorithms provided by the community can be used.

    For example, the usage of one such community library is as follows:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    use std::hash::BuildHasherDefault;
    use std::collections::HashMap;
    // Introducing a third-party hash function
    use twox_hash::XxHash64;


    let mut hash: HashMap<_, _, BuildHasherDefault<XxHash64>> = Default::default();
    hash.insert(42, "the answer");
    assert_eq!(hash.get(&42), Some(&"the answer"));

    Error Handling

    In most cases, errors are indicated at compile time and handled.

    Classification of Errors

    • Recoverable:

    For example, file not found; can try again.

    • Unrecoverable

    bug, e.g., accessing an index out of bounds

    Rust does not have an exception mechanism like C++/Java

    • Recoverable errors:Result<T,E>
    • Unrecoverable: panic! macro

    Unrecoverable errors and panic!

    When the panic! macro executes

    • the program prints an error message
    • unwinds, cleans up the call stack
    • exits the program

    panic = ‘abort’

    By default, when a panic occurs

    • the program unwinds the call stack (heavy workload)

    Rust walks back up the call stack, cleaning up data from each function encountered

    • or immediately aborts the call stack

    without cleanup, directly stops the program; memory needs to be cleaned up by the OS

    To make the binary smaller, change the setting from “unwind” to “abort”

    1
    2
    [profile.release]
    panic = 'abort'

    Panic in your own code

    1
    2
    3
    fn main(){
    panic!("crash and burn");
    }

    So in dependent code: external panic

    1
    2
    3
    4
    fn main(){
    let vector=vec![1,2,3];
    vector[100];
    }

    Output

    1
    2
    3
    4
    5
    6
    Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
    Running `target\debug\demo.exe`
    thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', src\main.rs:3:5
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    error: process didn't exit successfully: `target\debug\demo.exe` (exit code: 101)

    Locate the problematic code by calling the backtrace information of the panic! function

    Run the code using the following command (Windows cmd)

    1
    set RUST_BACKTRACE=1 && cargo run

    Windows Poweshell

    1
    $Env:RUST_BACKTRACE=1 -and (cargo run)

    Under Linux

    1
    export RUST_BACKTRACE=1 && cargo run

    More detailed information

    RUST_BACKTRACE=full

    To obtain a backtrace with debug information, debug symbols must be enabled (without --release)

    Result enum and recoverable errors

    1
    2
    3
    4
    enum Result<T, E> {
    Ok(T),
    Err(E),
    }

    T represents the type of data in the Ok member returned on success,

    And E represents the type of error in the Err member returned on failure

    Result and its variants are also brought into scope by the prelude

    Open file

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use std::fs::File;

    fn main() {
    let f = File::open("hello.txt");

    let f = match f {
    Ok(file) => file,
    Err(error) => panic!("Problem opening the file: {:?}", error),
    };
    }

    Match different errors

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    use std::fs::File;
    use std::io::ErrorKind;

    fn main() {
    let f = File::open("hello.txt");

    let f = match f {
    Ok(file) => file,
    Err(error) => match error.kind() {
    ErrorKind::NotFound => match File::create("hello.txt") {
    Ok(fc) => fc,
    Err(e) => panic!("Problem creating the file: {:?}", e),
    },
    other_error => {
    panic!("Problem opening the file: {:?}", other_error)
    }
    },
    };
    }

    Useunrap_or_else()andclosure

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    use std::fs::File;
    use std::io::ErrorKind;

    fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error| {
    if error.kind() == ErrorKind::NotFound {
    File::create("hello.txt").unwrap_or_else(|error| {
    panic!("Problem creating the file: {:?}", error);
    })
    } else {
    panic!("Problem opening the file: {:?}", error);
    }
    });
    }

    unwrap

    a shortcut method for match expressions

    1
    2
    3
    4
    5
    use std::fs::File;

    fn main() {
    let f = File::open("hello.txt").unwrap();
    }

    equivalent to

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use std::fs::File;

    fn main() {
    let f = File::open("hello.txt");

    let f = match f {
    Ok(file) => file,
    Err(error) => panic!("Problem opening the file: {:?}", error),
    };
    }
    • if the Result is Ok, return the value inside Ok
    • if the Result is Err, call the panic! macro

    expect

    unwrap with a custom error message

    1
    2
    3
    4
    5
    use std::fs::File;

    fn main() {
    let f = File::open("hello.txt").expect("无法打开文件");
    }

    propagating errors

    propagate the error to the caller

    custom implementation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // src/main.rs

    use std::fs::File;
    use std::io::{self, Read};

    fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
    Ok(file) => file,
    Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
    Ok(_) => Ok(s),
    Err(e) => Err(e),
    }
    }

    ? operator

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use std::fs::File;
    use std::io;
    use std::io::Read;

    fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
    }

    ? is defined to work in exactly the same way as the match expression that handles Result values defined in the custom error propagation example.

    if the Result value is Ok, this expression will return the value inside Ok and the program will continue executing.

    if the value is Err, the value inside Err will be returned as the return value of the entire function,as if usingreturnThe keyword is the same, thus the error value is propagated to the caller.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    use std::fs::File;
    use std::io::{self, Read};

    fn read_file1() -> Result<String, io::Error> {
    let f = File::open("hello.txt");
    let mut f = match f {
    Ok(file) => file,
    Err(e) => return Err(e),
    };

    let mut s = String::new();
    match f.read_to_string(&mut s) {
    Ok(_) => Ok(s),
    Err(e) => Err(e),
    }
    }

    fn read_file2() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
    }

    fn main() {
    assert_eq!(read_file1().unwrap_err().to_string(), read_file2().unwrap_err().to_string());
    println!("Success!")
    }

    ? and the from function

    The from function on the trait std::convert::From

    • Used for conversion between errors
    • Errors applied by ? are implicitly handled by the from function
    • When ? calls the from function:

    The error type it receives is converted to the error type defined by the current function’s return type

    Used for:Returning the same error type for different error causes

    As long as each error type implements the from function to convert to the returned error type

    Can be used directly after ?Chained method callsTo further shorten the code

    File name: src/main.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    use std::fs::File;
    use std::io;
    use std::io::Read;

    fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
    }

    Custom Implementation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    use std::fmt;
    use std::fs::File;
    use std::io::{self, Read};


    #[derive(Debug)]
    enum MyError {
    Io(io::Error),
    Parse,
    }

    // Implement Display for MyError (optional but common)
    impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    match self {
    MyError::Io(e) => write!(f, "IO error: {}", e),
    MyError::Parse => write!(f, "Parse error"),
    }
    }
    }

    impl From<io::Error> for MyError {
    fn from(error: io::Error) -> MyError {
    MyError::Io(error)
    }
    }


    fn read_username_from_file() -> Result<String, MyError> {
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
    }

    ? and the main function

    The return type of the main function is ()

    1
    2
    3
    4
    5
    6
    7
    use std::error::Error;
    use std::fs::File;

    fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;
    Ok(())
    }

    map,and_then

    map example — modify theOkvalue inside

    Only process theOkvalue and return a newResult, without changing the error type

    1
    2
    3
    let x: Result<i32, &str> = Ok(2);
    let y = x.map(|n| n * 3);
    assert_eq!(y, Ok(6));

    What if it’s an error?

    1
    2
    3
    let x: Result<i32, &str> = Err("error");
    let y = x.map(|n| n * 3);
    assert_eq!(y, Err("error")); // The error is returned as is

    and_then example — chaining Result operations

    and_thenProcess theOk, and continue returning aResult(chained logic), suitable for chaining multiple fallible steps:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn sq_then_to_string(x: i32) -> Result<String, &'static str> {
    if x < 0 {
    Err("negative")
    } else {
    Ok((x * x).to_string())
    }
    }

    let result = Ok(3).and_then(sq_then_to_string);
    assert_eq!(result, Ok("9".to_string()));

    If an error occurs, automatically stop chain computation:

    1
    2
    let result = Err("bad").and_then(sq_then_to_string);
    assert_eq!(result, Err("bad"));

    Example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    use std::num::ParseIntError;

    // With the return type rewritten, we use pattern matching without `unwrap()`.
    // But it's so Verbose..
    fn multiply(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> {
    match n1_str.parse::<i32>() {
    Ok(n1) => {
    match n2_str.parse::<i32>() {
    Ok(n2) => {
    Ok(n1 * n2)
    },
    Err(e) => Err(e),
    }
    },
    Err(e) => Err(e),
    }
    }

    // Rewriting `multiply` to make it succinct
    // You MUST USING `and_then` and `map` here
    fn multiply1(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> {
    // IMPLEMENT...
    n1_str.parse::<i32>().and_then(|n1| {
    n2_str.parse::<i32>().map(|n2| n1 * n2)
    })
    }

    fn print(result: Result<i32, ParseIntError>) {
    match result {
    Ok(n) => println!("n is {}", n),
    Err(e) => println!("Error: {}", e),
    }
    }

    fn main() {
    // This still presents a reasonable answer.
    let twenty = multiply1("10", "2");
    print(twenty);

    // The following now provides a much more helpful error message.
    let tt = multiply("t", "2");
    print(tt);

    println!("Success!")
    }

    When to panic!

    General principle

    • When defining afunction that might fail, prefer returning Result
    • otherwise panic!

    Scenarios

    • Demonstrating some concepts: unwrap
    • Prototype code: unwrap, expect
    • Testing: unwrap, expect

    Sometimes you have more information than the compiler

    • You can be sure the Result is Ok:unwrap
    1
    2
    use std::net::IpAddr;
    let home: IpAddr = "127.0.0.1".parse().unwrap();

    Calling your code with meaningless parameter values:panic!

    • Calling external uncontrolled code that returns an illegal state, which you cannot fix:panic!

    • If failure is expected:Result

    • When your code operates on values, it should first validate those values:panic! (assert!)

    Create custom types for validation

    One way to implement this is to parse the guess as an i32 instead of only u32, to allow negative inputs, and then check if the number is within range:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    loop {
    // --snip--

    let guess: i32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
    };

    if guess < 1 || guess > 100 {
    println!("The secret number will be between 1 and 100.");
    continue;
    }

    match guess.cmp(&secret_number) {
    // --snip--
    }

    The if expression checks whether the value is out of range, tells the user what went wrong, and calls continue to start the next iteration of the loop, asking for another guess. After the if expression, you can compare guess with the secret number, knowing that guess is between 1 and 100.

    Instead, we can create a new type and put the validation in a function that creates instances of it, rather than repeating these checks everywhere. This way, it is safe to use the new type in function signatures and trust the values they receive. The example shows a way to define a Guess type that only creates instances of Guess when the new function receives a value between 1 and 100:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    pub struct Guess {
    value: i32,
    }

    impl Guess {
    pub fn new(value: i32) -> Guess {
    if value < 1 || value > 100 {
    panic!("Guess value must be between 1 and 100, got {}.", value);
    }

    Guess { value }
    }

    pub fn value(&self) -> i32 {
    self.value
    }
    }

    We implement a method named value that borrows self, has no other parameters, and returns an i32. Such methods are sometimes called_getter_because their purpose is to return the data in the corresponding field. Such a public method is necessary because the value field of the Guess struct is private. The private value field is important so that code using the Guess struct is not allowed to set value directly: callersmustuse the Guess::new method to create an instance of Guess, which ensures there is no Guess whose value has not passed the condition check in the Guess::new function.

    Thus, a function that accepts (or returns) a number between 1 and 100 can be declared to accept (or return) an instance of Guess instead of an i32, and its body does not need any additional checks.

    Using Result in fn main

    A typical main function looks like this:

    1
    2
    3
    fn main() {
    println!("Hello World!");
    }

    In fact, the main function can also return a Result type: if an error occurs inside the main function, that error will be returned and a debug message about the error will be printed.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    use std::num::ParseIntError;

    fn main() -> Result<(), ParseIntError> {
    let number_str = "10";
    let number = match number_str.parse::<i32>() {
    Ok(number) => number,
    Err(e) => return Err(e),
    };
    println!("{}", number);
    Ok(())
    }

    Generics

    Define generics in functions

    Find the maximum value in a vec

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    fn largest_i32(list: &[i32]) -> i32 {
    let mut largest = list[0];
    for &item in list {
    if item > largest {
    largest = item;
    }
    }
    largest
    }

    fn largest_char(list: &[char]) -> char {
    let mut largest = list[0];
    for &item in list {
    if item > largest {
    largest = item;
    }
    }
    largest
    }

    fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest_i32(&number_list);
    println!("The largest number is {}", result);
    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest_char(&char_list);
    println!("The largest char is {}", result);
    }

    Using generics

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list {
    if item > largest {
    largest = item;
    }
    }
    largest
    }

    fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest(&number_list);
    println!("The largest number is {}", result);
    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest(&char_list);
    println!("The largest char is {}", result);
    }

    Run

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo)
    error[E0369]: binary operation `>` cannot be applied to type `T`
    --> src\main.rs:5:17
    |
    5 | if item > largest {
    | ---- ^ ------- T
    | |
    | T
    |
    help: consider restricting type parameter `T`
    |
    1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
    | ++++++++++++++++++++++

    For more information about this error, try `rustc --explain E0369`.
    error: could not compile `demo` due to previous error

    Simply put, this error indicates that the function body of largest cannot apply to all possible types of T

    Because the function body needs to compare values of type T, but it can only be used for types we know how to order. To enable comparison, the std::cmp::PartialOrd trait defined in the standard library can implement comparison functionality for types.

    Modification:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    fn largest<T>(list: &[T]) -> &T
    where
    T: PartialOrd,
    {
    let mut largest = &list[0];
    for item in list {
    if *item > *largest {// Adding * is not necessary here, because Rust implements PartialOrd for reference types, provided that the referenced type T: PartialOrd

    largest = item;
    }
    }
    largest
    }

    fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest(&number_list);
    println!("The largest number is {}", result);
    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest(&char_list);
    println!("The largest char is {}", result);
    }

    Define generics in structs

    Filename: src/main.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct Point<T> {
    x: T,
    y: T,
    }

    fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
    }

    Define generics in enums

    1
    2
    3
    4
    enum Option<T> {
    Some(T),
    None,
    }

    Enums can also have multiple generic types.

    1
    2
    3
    4
    enum Result<T, E> {
    Ok(T),
    Err(E),
    }

    Generics in method definitions

    Filename: src/main.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    struct Point<T> {
    x: T,
    y: T,
    }

    impl<T> Point<T> {
    fn x(&self) -> &T {
    &self.x
    }
    }
    impl Point<i32> {
    fn x(&self) -> &i32 {
    &self.x
    }
    }

    fn main() {
    let p = Point { x: 5, y: 10 };
    println!("p.x = {}", p.x());
    }
    • Place T after the impl keyword to indicate implementing methods on type T: impl Point
    • Implement methods only for a specific type: impl Point
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct Point<T> {
    x: T,
    y: T,
    }

    impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
    (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
    }

    fn main() {
    let p = Point{x: 5.0_f32, y: 10.0_f32};
    println!("{}",p.distance_from_origin())
    }
    • The generic type parameters in a struct can differ from those in its methods
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    struct Point<X1, Y1> {
    x: X1,
    y: Y1,
    }

    impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
    Point {
    x: self.x,
    y: other.y,
    }
    }
    }

    fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
    }

    Const generics

    Generics implemented for types abstract over different types, but is there a generic for values? The answer is const generics.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    struct ArrayPair<T, const N: usize> {
    left: [T; N],
    right: [T; N],
    }

    impl<T: Debug, const N: usize> Debug for ArrayPair<T, N> {
    // ...
    }
    fn foo<const N: usize>() {}

    fn bar<T, const M: usize>() {
    foo::<M>(); // ok: matches the first case
    foo::<2021>(); // ok: matches the second case
    foo::<{20 * 100 + 20 * 10 + 1}>(); // ok: matches the third case

    foo::<{ M + 1 }>(); // error: violates the third case, const expressions cannot contain generic parameter M
    foo::<{ std::mem::size_of::<T>() }>(); // error: generic expression contains generic parameter T

    let _: [u8; M]; // ok: matches the first case
    let _: [u8; std::mem::size_of::<T>()]; // error: generic expression contains generic parameter T
    }

    fn main() {}
    • Currently, const generic parameters can only use arguments of the following forms:

      • A single const generic parameter

      • A literal (i.e., integer, boolean, or character).

      • A concrete const expression (the expression cannot contain any generic parameters).

    • Const generics can also help us avoid some runtime checks and improve performance.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    pub struct MinSlice<T, const N: usize> {
    pub head: [T; N],
    pub tail: [T],
    }

    fn main() {
    let slice: &[u8] = b"Hello, world";
    let reference: Option<&u8> = slice.get(6);
    // We know that`.get`returns`Some(b' ')`
    // but the compiler doesn't know.
    assert!(reference.is_some());

    let slice: &[u8] = b"Hello, world";

    // When compiling to construct MinSlice, a length check is performed, meaning we know at compile time that its length is 12.
    // At runtime, once`unwrap`succeeds, within the scope of`MinSlice`, no further checks are needed.
    let minslice = MinSlice::<u8, 12>::from_slice(slice).unwrap();
    let value: u8 = minslice.head[6];
    assert_eq!(value, b' ')
    }

    <T, const N: usize> is part of the struct type, just like array types, meaning different lengths result in different types:Array<i32, 3> and Array<i32, 4> are different types.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #[allow(unused)]

    struct Array<T, const N: usize> {
    data : [T; N]
    }

    fn main() {
    let arrays = [
    Array{
    data: [1, 2, 3],
    },
    Array {
    data: [1, 2, 3],
    },
    Array {
    data: [4,5,6]
    }
    ];
    }

    Fill in the blank.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Fill in the blank.
    fn print_array<__>(__) {
    println!("{:?}", arr);
    }
    fn main() {
    let arr = [1, 2, 3];
    print_array(arr);

    let arr = ["hello", "world"];
    print_array(arr);
    }

    Answer:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn print_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
    }
    fn main() {
    let arr = [1, 2, 3];
    print_array(arr);

    let arr = ["hello", "world"];
    print_array(arr);
    }

    Sometimes we want tolimit the amount of memory a variable occupies., for example in embedded environments, the third form of const generic parameters, const expressions, is very suitable:

    The following code uses a feature that requires the nightly compiler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #![allow(incomplete_features)]
    #![feature(generic_const_exprs)]

    fn check_size<T>(val: T)
    where
    Assert<{ core::mem::size_of::<T>() < 768 }>: IsTrue,
    {
    //...
    }

    // fix the errors in main
    fn main() {
    check_size([0u8; 767]);
    check_size([0i32; 191]);
    check_size(["hello你好"; 47]); // &str is a string reference, containing a pointer and string length in it, so it takes two word long, in x86-64, 1 word = 8 bytes
    check_size([(); 31].map(|_| "hello你好".to_string())); // String is a smart pointer struct, it has three fields: pointer, length and capacity, each takes 8 bytes
    check_size(['中'; 191]); // A char takes 4 bytes in Rust
    }

    pub enum Assert<const CHECK: bool> {}

    pub trait IsTrue {}

    impl IsTrue for Assert<true> {}

    Performance of Generic Code

    Rust implements generics in such a way that code using generic type parameters has no speed penalty compared to using concrete types.

    Rust achieves this efficiency throughmonomorphizationmonomorphization) of generic code at compile time.Monomorphization is the process of converting generic code into specific code by filling in the concrete types used at compile time.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //fn main(){
    // let integer = Some(5);
    // let float = Some(5.0);
    //}
    enum Option_i32 {
    Some(i32),
    None,
    }

    enum Option_f64 {
    Some(f64),
    None,
    }

    fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
    }

    At compile time, Rust expands the generic Option into Optionand Optiontypes

    Trait: Defining Shared Behavior

    A trait tells the Rust compiler about functionality a particular type has and can share with other types

    Defining a Trait

    Group method signatures together to define a set of behaviors necessary to accomplish a purpose

    • Keyword: trait
    • Only method signatures, no concrete implementation

    File name: src/lib.rs

    1
    2
    3
    pub trait Summary {
    fn summarize(&self) -> String;
    }

    Implementing a trait on a type

    Similar to implementing methods for a type

    Difference: impl Xxxx for Tweet

    File: src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
    }

    impl Summary for NewsArticle {
    fn summarize(&self) -> String {
    format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
    }

    pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
    }

    impl Summary for Tweet {
    fn summarize(&self) -> String {
    format!("{}: {}", self.username, self.content)
    }
    }

    File src/main.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use demo::{Summary,Tweet};
    fn main(){
    let tweet = Tweet{
    username: String::from("horse_ebook"),
    content: String::from("of course,sa you probably..."),
    reply:false,
    retweet:false,
    };
    println!("1 new tweet:{}",tweet.summary());
    }

    demo is the name of the [package] item in the Cargo.toml file

    Constraints for implementing a trait

    The prerequisite for implementing a trait on a type is:

    • The type itselforthis trait is defined in the local crate

    Cannot implement an external trait for an external type

    This restriction is part of a property of the program calledcoherencecoherence), or more specifically theorphan ruleorphan rule), named for the absence of a parent type. This rule ensures that code written by others won’t break your code, and vice versa. Without this rule, two crates could implement the same trait for the same type, and Rust would have no way to know which implementation to use.

    In short:If neither the type nor the trait is yoursthen you are not allowed to add an impl for them

    Default implementations of functions in a trait

    Filename: src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    pub trait Summary {
    fn summarize(&self) -> String {
    String::from("(Read more...)")
    }
    }
    pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
    }

    impl Summary for NewsArticle {
    //fn summarize(&self) -> String {
    // format!("{}, by {} ({})", self.headline, self.author, self.location)
    //}
    }

    pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
    }

    impl Summary for Tweet {
    //Overriding a default implementation
    fn summarize(&self) -> String {
    format!("{}: {}", self.username, self.content)
    }
    }

    Default implementations are allowed to call other methods in the same traiteven if those methods don’t have default implementations

    1
    2
    3
    4
    5
    6
    7
    pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
    format!("(Read more from {}...)", self.summarize_author())
    }
    }

    Note: It is not possible to call the default implementation from an overriding implementation of the same method

    Traits as parameters

    impl trait syntax:

    Applies to simple cases; it’s syntactic sugar for trait bounds.

    1
    2
    3
    pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
    }

    trait bound syntax:

    Applies to complex cases.

    1
    2
    3
    pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
    }

    Comparison

    1
    pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

    This works when item1 and item2 are allowed to be different types (as long as both implement Summary). But what if you want to force them to be the same type? That’s only possible with trait bounds:

    1
    pub fn notify<T: Summary>(item1: &T, item2: &T) {}

    Specifying multiple trait bounds with +

    1
    2
    3
    pub fn notify(item: &(impl Summary + Display)) {}

    pub fn notify<T: Summary + Display>(item: &T) {}

    Simplifying trait bounds with where

    fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

    Using where clauses

    1
    2
    3
    4
    fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
    U: Clone + Debug
    {}

    Returning types that implement a trait

    You can also use impl Trait syntax in return positions to return a type that implements a trait:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn returns_summarizable() -> impl Summary {
    Tweet {
    username: String::from("horse_ebooks"),
    content: String::from(
    "of course, as you probably already know, people",
    ),
    reply: false,
    retweet: false,
    }
    }

    However, this only works forreturning a single type. For example, this code specifies the return type as impl Summary, but returning either NewsArticle or Tweet won’t work:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
    NewsArticle {
    headline: String::from(
    "Penguins win the Stanley Cup Championship!",
    ),
    location: String::from("Pittsburgh, PA, USA"),
    author: String::from("Iceburgh"),
    content: String::from(
    "The Pittsburgh Penguins once again are the best \
    hockey team in the NHL.",
    ),
    }
    } else {
    Tweet {
    username: String::from("horse_ebooks"),
    content: String::from(
    "of course, as you probably already know, people",
    ),
    reply: false,
    retweet: false,
    }
    }
    }

    Here, it attempts to return either NewsArticle or Tweet. This won’t compile due to limitations in how impl Trait works.

    You can use dyn trait objects

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    struct Sheep {}
    struct Cow {}

    trait Animal {
    fn noise(&self) -> String;
    }

    impl Animal for Sheep {
    fn noise(&self) -> String {
    "baaaaah!".to_string()
    }
    }

    impl Animal for Cow {
    fn noise(&self) -> String {
    "moooooo!".to_string()
    }
    }

    // Returns a type that implements the Animal trait, but we cannot know at compile time which specific type is returned
    // Fix the error here; you can use fake randomness or trait objects
    fn random_animal(random_number: f64) -> Box<dyn Animal> {
    if random_number < 0.5 {
    Box::new(Sheep {})
    } else {
    Box::new(Cow {})
    }
    }

    fn main() {
    let random_number = 0.234;
    let animal = random_animal(random_number);
    println!("You've randomly chosen an animal, and it says {}", animal.noise());
    }

    Trait objects, using trait objects in arrays

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    trait Bird {
    fn quack(&self);
    }

    struct Duck;
    impl Duck {
    fn fly(&self) {
    println!("Look, the duck is flying")
    }
    }
    struct Swan;
    impl Swan {
    fn fly(&self) {
    println!("Look, the duck.. oh sorry, the swan is flying")
    }
    }

    impl Bird for Duck {
    fn quack(&self) {
    println!("{}", "duck duck");
    }
    }

    impl Bird for Swan {
    fn quack(&self) {
    println!("{}", "swan swan");
    }
    }

    fn main() {
    // Fill in the blank
    let birds :[Box<dyn Bird>;2]=[Box::new(Duck{}),Box::new(Swan{})];

    for bird in birds {
    bird.quack();
    // When duck and swan become bird, they both forget how to soar in the sky, only remembering how to quack...
    // Therefore, the following code will cause an error
    // bird.fly();
    }
    }

    &dyn and Box

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    trait Draw {
    fn draw(&self) -> String;
    }

    impl Draw for u8 {
    fn draw(&self) -> String {
    format!("u8: {}", *self)
    }
    }

    impl Draw for f64 {
    fn draw(&self) -> String {
    format!("f64: {}", *self)
    }
    }

    fn main() {
    let x = 1.1f64;
    let y = 8u8;

    // draw x
    draw_with_box(Box::new(x));

    // draw y
    draw_with_ref(&y);
    }

    fn draw_with_box(x: Box<dyn Draw>) {
    x.draw();
    }

    fn draw_with_ref(x: &dyn Draw) {
    x.draw();
    }

    Static dispatch and dynamic dispatch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    trait Foo {
    fn method(&self) -> String;
    }

    impl Foo for u8 {
    fn method(&self) -> String { format!("u8: {}", *self) }
    }

    impl Foo for String {
    fn method(&self) -> String { format!("string: {}", *self) }
    }

    // implement below with generics
    fn static_dispatch<T: Foo>(x: T) {
    x.method();
    }

    // implement below with trait objects
    fn dynamic_dispatch(x: &dyn Foo) {
    x.method();
    }

    fn main() {
    let x = 5u8;
    let y = "Hello".to_string();

    static_dispatch(x);
    dynamic_dispatch(&y);

    println!("Success!")
    }

    Use trait bounds to fix the largest function

    File name: src/main.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
    if item > largest {
    largest = item;
    }
    }

    largest
    }

    fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
    }

    If you don’t want to restrict the largest function to only types that implement the Copy trait, you can specify Clone instead of Copy in the trait bounds for T. Then clone each value of the slice so that the largest function owns them. Using the clone function means that for types like String that own heap data, it will potentially allocate more heap space, and heap allocation can be quite slow when dealing with large amounts of data.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn largest<T: PartialOrd + Clone>(list: &[T]) -> T {
    let mut largest = list[0].clone();

    for item in list {
    if item > &largest {
    largest = item.clone();
    }
    }

    largest
    }

    fn main() {
    let str_list = vec![String::from("hello"),String::from("world")];
    let result = largest(&str_list)
    println!("The largest word is {}", result);
    }

    Or have largest directly return a reference

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn largest<T: PartialOrd + Clone>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
    if item > &largest {
    largest = item;
    }
    }

    largest
    }

    fn main() {
    let str_list = vec![String::from("hello"),String::from("world")];
    let result = largest(&str_list)
    println!("The largest word is {}", result);
    }

    Conditionally implement methods using trait bounds

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    use std::fmt::Display;

    struct Pair<T> {
    x: T,
    y: T,
    }

    impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
    Self { x, y }
    }
    }

    impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
    if self.x >= self.y {
    println!("The largest member is x = {}", self.x);
    } else {
    println!("The largest member is y = {}", self.y);
    }
    }
    }

    Implementing a trait on all types that satisfy a trait bound is calledBlanket implementations

    For example:

    1
    2
    3
    impl<T: Display> ToString for T {
    // --snip--
    }

    Because the standard library has these blanket implementations, we can call the to_string method defined by ToString on any type that implements the Display trait. For example, we can convert an integer to its corresponding String value because integers implement Display:

    let s = 3.to_string();

    Derive macro implementations

    We can use the #[derive] attribute to derive some traits, for which the compiler will automatically provide default implementations. This is very convenient for everyday code development. For example, the commonly used Debug trait is obtained directly through derivation without needing to implement it manually.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    // `Centimeters`, a tuple struct that can be compared for size
    #[derive(PartialEq, PartialOrd)]
    struct Centimeters(f64);

    // `Inches`, a tuple struct that can be printed
    #[derive(Debug)]
    struct Inches(i32);

    impl Inches {
    fn to_centimeters(&self) -> Centimeters {
    let &Inches(inches) = self;

    Centimeters(inches as f64 * 2.54)
    }
    }

    // Add some attributes to make the code work
    // Do not modify other code!
    #[derive(Debug,PartialEq,PartialOrd)]
    struct Seconds(i32);

    fn main() {
    let _one_second = Seconds(1);

    println!("One second looks like: {:?}", _one_second);
    let _this_is_true = _one_second == _one_second;
    let _this_is_true = _one_second > _one_second;

    let foot = Inches(12);

    println!("One foot equals {:?}", foot);

    let meter = Centimeters(100.0);

    let cmp =
    if foot.to_centimeters() < meter {
    "smaller"
    } else {
    "bigger"
    };

    println!("One foot is {} than one meter.", cmp);
    }

    Lifetimes

    • Every reference in Rust has its own lifetime
    • Lifetimes: the scope for which a reference is valid
    • In most cases: lifetimes are implicit and can be inferred
    • When the lifetimes of references might be related in different ways: manually annotate lifetimes

    The goal of lifetimes is:Avoiding Dangling References

    Attempting to use a reference to a value that has gone out of scope will fail

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    let r;

    {
    let x = 5;
    r = &x;
    }

    println!("r: {}", r);
    }

    Borrow Checker

    The Rust compiler has aborrow checkerborrow checker), which compares scopes to ensure all borrows are valid.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    let r; // ---------+-- 'a
    // |
    { // |
    let x = 5; // -+-- 'b |
    r = &x; // | |
    } // -+ |
    // |
    println!("r: {}", r); // |
    }

    Here, the lifetime of r is marked as 'a and the lifetime of x is marked as 'b. As you can see, the inner 'b block is much smaller than the outer lifetime 'a. At compile time, Rust compares the sizes of these two lifetimes and finds that r has lifetime 'a, but it references an object with lifetime 'b. The program is rejected because lifetime 'b is smaller than lifetime 'a: the referenced object lives for a shorter time than its referencer.

    Generic Lifetimes in Functions

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
    x
    } else {
    y
    }
    }
    fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
    }

    Execute

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    error[E0106]: missing lifetime specifier
    --> src\main.rs:1:33
    |
    1 | fn longest(x: &str, y: &str) -> &str {
    | ---- ---- ^ expected named lifetime parameter
    |
    = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
    help: consider introducing a named lifetime parameter
    |
    1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    | ++++ ++ ++ ++

    For more information about this error, try `rustc --explain E0106`.
    error: could not compile `demo` due to previous error

    Annotating Lifetimes

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
    x
    } else {
    y
    }
    }
    fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
    }

    Indicates that the lifetime of the return value is the overlap of the lifetimes of the two references passed as parameters.

    Lifetime Annotation Syntax

    • Lifetime annotations do not change the length of the lifetimes of references

    • When generic lifetime parameters are specified, the function can accept references with any lifetime

    • Lifetime annotations:Describes the relationship between the lifetimes of multiple references, but does not affect the lifetimes.

    • Lifetime parameter name: starts with ', usually all lowercase and very short; many people use 'a.

    • Position of lifetime annotations

    After the & symbol of a reference, use a space to separate the annotation from the reference type

    1
    2
    3
    &i32        // Reference
    &'a i32 // Reference with an explicit lifetime
    &'a mut i32 // Mutable reference with an explicit lifetime

    Generic lifetime parameters are declared in:Inside the <> between the function name and the parameter list

    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

    1
    2
    3
    4
    5
    6
    7
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
    x
    } else {
    y
    }
    }

    Its actual meaning is that the lifetime of the reference returned by the longest function is consistent with the smaller of the lifetimes of the references passed into the function.

    Remember that when specifying lifetime parameters in a function signature, we are not changing the lifetimes of any passed-in or returned values, but rather indicating that any values that do not satisfy this constraint will be rejected by the borrow checker. Note that the longest function does not need to know exactly how long x and y will live, but only that there is some scope that can be substituted for 'a that will satisfy this signature.

    When concrete references are passed to longest, the specific lifetime substituted for 'a is the overlapping part of the scopes of x and ythe overlapping part. In other words, the concrete lifetime of the generic lifetime 'a is equivalent to the smaller of the lifetimes of x and ythe smaller one. Because we annotated the returned reference with the same lifetime parameter 'a, the returned reference is guaranteed to remain valid until the end of the shorter lifetime between x and y.

    Using

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let string1 = String::from("long string is long");

    {
    let string2 = String::from("xyz");
    let result = longest(string1.as_str(), string2.as_str());
    println!("The longest string is {}", result);
    }
    }

    In this example, string1 is valid until the end of the outer scope, string2 is valid in the inner scope, and result references a value that is valid until the end of the inner scope. The borrow checker approves this code; it compiles and runs, printing “The longest string is long string is long”.

    Modify

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
    let string2 = String::from("xyz");
    result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
    }

    Program runs with error

    The error indicates that to ensure result is valid in println!, string2 needs to be valid until the end of the outer scope. Rust knows this because the parameters and return value of the (longest) function all use the same lifetime parameter 'a.

    Deep Understanding of Lifetimes

    • The correct way to specify lifetime parameters depends on the specific functionality of the function implementation

    If the implementation of the longest function is modified to always return the first parameter instead of the longest string slice, there is no need to specify a lifetime for parameter y. The following code will compile:

    Filename: src/main.rs

    1
    2
    3
    fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
    }
    • When returning a reference from a function, the lifetime parameter of the return value needs to match the lifetime parameter of one of the parameters.

    If the returned referencedoes notpoint to any parameter, then the only possibility is that it points to a value created inside the function, which will be a dangling reference because it will go out of scope when the function ends.

    1
    2
    3
    4
    fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
    }

    Compilation error

    In summary, lifetime syntax is used toassociate the lifetimes of multiple function parameters with that of its return value. Once they form a certain association, Rust has enough information to allow memory-safe operations and prevent actions that would create dangling pointers or violate memory safety.

    Lifetime annotations in struct definitions

    A struct can include:

    • Self-owned types
    • References: need to add lifetime annotations on each reference

    Filename: src/main.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct ImportantExcerpt<'a> {
    part: &'a str,
    }

    fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
    part: first_sentence,
    };
    }

    Indicates that the struct’s lifetime is the same as that of its member part.

    Lifetime elision

    Lifetime Elision

    The patterns encoded in Rust’s reference analysis are calledlifetime elision ruleslifetime elision rules

    These are not rules that programmers need to follow; they are a set of specific scenarios where the compiler considers that if the code matches these scenarios, explicit lifetime annotations are unnecessary.

    The lifetimes of function or method parameters are calledinput lifetimeinput lifetimes), and the lifetime of the return value is called theoutput lifetimeoutput lifetimes)。

    The compiler usesthree rulesto determine when references do not need explicit annotations. The first rule applies to input lifetimes, and the latter two rules apply to output lifetimes. If the compiler checks these three rules and there are still references whose lifetimes cannot be determined, the compiler will stop and generate an error. These rulesapply tofndefinitions, andimplblocks

    • Rule 1Each parameter that is a reference type has its own lifetime
    • Rule 2If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
    • Rule 3If there are multiple input lifetime parameters, but one of them is &self or &mut self (because it is a method), then the lifetime of self is assigned to all output lifetime parameters

    Example:

    Suppose we ourselves are the compiler.

    1
    fn first_word(s: &str) -> &str {

    Then the compiler applies the first rule, which isEach reference parameter has its own lifetime

    1
    fn first_word<'a>(s: &'a str) -> &str {

    For the second rule, it applies because there is exactly one input lifetime parameter. The second rule states that the lifetime of the input parameter will be assigned to the output lifetime parameter, so the signature now looks like this:

    1
    fn first_word<'a>(s: &'a str) -> &'a str {

    Now all references in this function signature have lifetimes, so the compiler can continue its analysis without the programmer needing to annotate the lifetimes in this function signature.

    1
    fn longest(x:&str,y:&str)->&str{

    Apply the first rule

    1
    fn longest<'a,'b>(x:&'a str,y:&'b str)->&str{

    The second rule does not apply, the third rule does not apply, so the compiler reports an error

    Lifetime annotations in method definitions

    When implementing methods for a struct with lifetimes, the syntax is still similar to that of generic type parameters.

    Where to declare lifetime parameters depends on:

    • Whether the lifetime parameters are related to struct fields or method parameters and return values

    Lifetime names for struct fields:

    • Declared after impl
    • Used after the struct name
    • These lifetimes are part of the struct type

    In method signatures within impl blocks:

    • References must be bound to the lifetime of struct field references, or references can be independent
    • Lifetime elision rules often make lifetime annotations unnecessary in methods
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    struct ImportantExcerpt<'a>{
    part: &'a str,
    }

    impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
    3
    }
    //Here is an example that applies the third lifetime elision rule
    fn announce_and_return_part(&self, announcement: &str) -> &str {
    println!("Attention please: {}", announcement);
    self.part
    }
    }

    fn main(){
    let novel = String::from("Call me Ishmael. Some year ago...");
    let first_sentence = novel.split('.').next().expect("Could not found a '.'");
    let i = ImportantExcerpt{
    part: first_sentence,
    };
    }

    announce_and_return_part has two input lifetimes here, so Rust applies the first lifetime elision rule and gives &self and announcement their own lifetimes. Then, because one of the parameters is &self, the return type is assigned the lifetime of &self, so all lifetimes are resolved.

    Static lifetime

    'static, whose lifetimecanlive for the entire duration of the program.

    All string literals have'staticlifetime

    let s: &'static str = “I have a static lifetime.”;

    The text of this string is stored directly in the program’s binary file, which is always available. Therefore, all string literals are 'static.

    Combining generic type parameters, trait bounds, and lifetimes

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    use std::fmt::Display;

    fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
    ) -> &'a str
    where
    T: Display,
    {
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
    x
    } else {
    y
    }
    }

    Writing automated tests

    Test functions in Rust are used to verify that non-test code is behaving as expected. A test function body typically performs the following three actions:

    1. Set up any needed data or state (Arrange)
    2. Run the code that needs to be tested (Act)
    3. Assert that the results are what we expect (Assert)

    Anatomy of a Test Function

    Writing Test Functions

    A test in Rust is a function annotated with the test attribute. Attributes are metadata about pieces of Rust code.

    To turn a function into a test function, add #[test] before the fn line**#[test]**

    Running Tests

    • Run all test functions using the cargo test command

    Rust builds a test runner executable that runs functions annotated with the test attribute and reports whether they succeed

    • When creating a library project with cargo, a test module with one test function is generated

    You can add any number of test modules or functions

    1
    cargo new 项目名 --lib

    src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    pub fn add(left: usize, right: usize) -> usize {
    left + right
    }

    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    fn it_works() {
    let result = add(2, 2);
    assert_eq!(result, 4);
    }
    }

    Run

    1
    cargo run test

    Test Failure

    • A test function panicking indicates failure
    • Each test runs in a new thread
    • When the main thread sees a test thread die, that test is marked as failed

    Assert

    Use the assert! macro to check test results

    The assert! macro, from the standard library, is used to determine whether a condition is true

    • true, test passes
    • false, calls panic!, test fails
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #[derive(Debug)]
    struct Rectangle {
    width: u32,
    height: u32,
    }

    impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
    self.width > other.width && self.height > other.height
    }
    }

    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
    let larger = Rectangle {
    width: 8,
    height: 7,
    };
    let smaller = Rectangle {
    width: 5,
    height: 1,
    };

    assert!(larger.can_hold(&smaller));
    }
    }

    Using assert_eq! and assert_ne! macros to test equality

    • Both come from the standard library
    • Determine whether two arguments are equal or not equal
    • In fact, they use the assert! of the == and != operators
    • On assertion failure, it automatically prints the values of the two arguments

    Print parameters using debug format: requires parameters to implement PartialEq and Debug traits (all basic types and most standard library types implement them)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    pub fn add_two(a: i32) -> i32 {
    a + 2
    }

    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
    assert_eq!(4, add_two(2));
    }
    }

    Custom error messages

    You can pass an optional failure message argument to the assert!, assert_eq!, and assert_ne! macros, which will print the custom failure message along with the test failure.

    • The first argument of the assert! macro is required, with the custom message as the second argument
    • assert_eq! and assert_ne! require the first two arguments, with the custom message as the third argument
    • The custom message argument is passed to the format! macro, allowing the use of {} placeholders
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    pub fn greeting(name: &str) -> String {
    format!("Hello {}!", name)
    }

    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(result.contains("Carol"));
    }
    }

    Custom error messages

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #[test]
    fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(
    result.contains("Carol"),
    "Greeting did not contain name, value was `{}`",
    result
    );
    }

    Verifying error handling scenarios

    Can verify whether code panics under specific circumstances

    should_panicAttribute:

    • Function panics: test passes
    • Function does not panic: test fails
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    pub struct Guess {
    value: i32,
    }

    impl Guess {
    pub fn new(value: i32) -> Guess {
    if value < 1 || value > 100 {
    panic!("Guess value must be between 1 and 100, got {}.", value);
    }

    Guess { value }
    }
    }

    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
    Guess::new(200);
    }
    }

    Make should_panic more precise

    An optional expected parameter can be added to the should_panic attribute. The test harness will ensure that the error message contains the provided text

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // --snip--
    impl Guess {
    pub fn new(value: i32) -> Guess {
    if value < 1 {
    panic!(
    "Guess value must be greater than or equal to 1, got {}.",
    value
    );
    } else if value > 100 {
    panic!(
    "Guess value must be less than or equal to 100, got {}.",
    value
    );
    }

    Guess { value }
    }
    }

    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
    Guess::new(200);
    }
    }

    Using Result<T, E> in tests

    Instead of panicking, you can write tests that return Result<T, E>

    • Returning Ok: test passes
    • Returning Err: test fails
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #[cfg(test)]
    mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
    if 2 + 2 == 4 {
    Ok(())
    } else {
    Err(String::from("two plus two does not equal four"))
    }
    }
    }

    Note:You cannot use the #[should_panic] annotation on tests that use Result<T, E>

    Because when they fail, they return an Err instead of panicking

    Controlling test execution

    Changing the behavior of cargo test: adding command-line arguments

    Default behavior:

    • Running in parallel
    • All tests
    • Capturing (not displaying) all standard output to make it easier to read output related to test results

    Command line arguments:

    • Arguments for cargo test: immediately after cargo test

    cargo test --help

    • Arguments for the test executable: placed after –

    cargo test – --help

    Running tests in parallel

    By default, tests run in parallel using multiple threads

    Ensure that tests:

    • Do not depend on each other
    • Do not depend on any shared state (environment, working directory, environment variables, etc.)

    The --test-threads argument

    If you do not want tests to run in parallel, or want more precise control over the number of threads, you can pass the --test-threads argument and the desired number of threads to the test binary. For example:

    1
    cargo test -- --test-threads=1

    Here, the test threads are set to 1, telling the program not to use any parallelism. This will take more time than running in parallel, but when there is shared state, tests will not potentially interfere with each other.

    Showing function output

    By default, if a test passes, Rust’s test library captures everything printed to standard output

    For example, println!:

    • If the test succeeds, we will not see the output of println! in the terminal: You will only see the line indicating that the test passed.
    • Ifthe test fails, you will see all standard output and other error messages

    If you also want to see the values printed in passing tests, you can add --show-output at the end to tell Rust to display the output of successful tests.

    1
    cargo test -- --show-output

    Running a Subset of Tests by Name

    If you run tests without passing any arguments, all tests will run in parallel:

    Running a Single Test

    You can pass the name of any test to cargo test to run only that test:

    1
    cargo test one_hundred

    Filtering to Run Multiple Tests

    We can specify part of a test name, and any test whose name matches will be run. For example, because the first two tests have names containing ‘add’, you can run them with cargo test add:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ cargo test add
    Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.61s
    Running unittests (target/debug/deps/adder-92948b65e88960b4)

    running 2 tests
    test tests::add_three_and_two ... ok
    test tests::add_two_and_two ... ok

    test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

    This runs all tests with ‘add’ in their name and filters out the test named one_hundred.

    Ignoring Some Tests

    Sometimes specific tests are very time-consuming to execute, so you may want to exclude them during most cargo test runs.

    You can use the ignore attribute to mark time-consuming tests and exclude them, as shown below:

    Filename: src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #[test]
    fn it_works() {
    assert_eq!(2 + 2, 4);
    }

    #[test]
    #[ignore]
    fn expensive_test() {
    // Code that needs to run for an hour
    }

    For tests we want to exclude, we add the #[ignore] line after #[test]. Now if we run the tests, we’ll see that it_works runs, while expensive_test does not run:

    If we only want to run the ignored tests, we can use cargo test – --ignored

    1
    $ cargo test -- --ignored

    Test Organization

    The Rust community tends to think about tests in terms of two main categories:

    Unit testsunit tests) andIntegration testsintegration tests

    • Unit tests tend to be smaller and more focused, testing one module at a time in isolation, or testing private interfaces.
    • Integration tests are entirely external to your library. They use your code in the same way as other external code, only testing public interfaces, and each test may test multiple modules.

    Unit Tests

    Annotating the test module with #[cfg(test)]

    • Only compile and run code when running cargo test
    • Running cargo build will not
    • Integration tests are in a different directory and do not need the #[cfg(test)] annotation

    cfg: configuration

    Tells Rust that the following item is only included under a specified configuration option

    Testing private functions

    There has been debate within the testing community about whether private functions should be tested directly, and in other languages it is difficult or even impossible to test private functions. However, regardless of which testing ideology you adhere to,Rust’s privacy rules do allow you to test private functions

    Filename: src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    pub fn add_two(a: i32) -> i32 {
    internal_adder(a, 2)
    }

    fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
    }

    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    fn internal() {
    assert_eq!(4, internal_adder(2, 2));
    }
    }

    Example 11-12: Testing a private function

    Note the internal_The adder function is not marked as pub. Also, tests is just another module. As mentioned in the section ‘Paths for Referring to an Item in the Module Tree’, items in child modules can use items from their parent modules. In the test, we bring all items from the parent module of the test module into scope with use super:😗, and then the test calls internal_adder。

    Integration tests

    In Rust, integration tests are entirely external to the test library

    They use the library file just like any other code that uses the library, meaning they can only call the public API of the library. The purpose of integration tests is to test whether multiple parts of the library work together correctly. Some code units that work correctly individually may have issues when integrated together, soIntegration test coverageIt is also very important.

    tests directory

    • Creating integration tests: the tests directory
    • Each test file in the tests directory is a separate crate

    Create an integration test. Keep the code in the adder example_src/lib.rs_code. Create a_tests_directory, create a new file_tests/integration_test.rs_, and enter the code from the example.

    1
    2
    3
    4
    5
    6
    use adder;

    #[test]
    fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
    }
    • There is no need to_tests/integration_annotate any code in test.rs_ with #[cfg(test)].The tests folder is a special folder in Cargo, Cargo only compiles files in this directory when running cargo test.
    • Need to import the library being tested

    Run a specific integration test

    Run a particular integration test

    1
    $ cargo test 函数名

    Run all tests in a specific test file

    1
    $ cargo test --test 文件名

    Submodules in integration tests

    As integration tests grow, you may want to add more files to the tests directory to better organize them, for example, grouping tests by their functionality. As we mentioned earlier, each_tests_file in the directory is compiled as a separate crate.

    Treating each integration test file as its own crate helps create separate scopes, which provide an environment more similar to how end users would use the crate.

    For example, if we can create atests/common.rsfile and create a function named setup, we want this function to be callable by test functions in multiple test files:

    Filename: tests/common.rs

    1
    2
    3
    pub fn setup() {
    // setup code specific to your library's tests would go here
    }

    If you run the tests again, you will see a new corresponding_common.rs_section in the test results for the file, even though this file does not contain any test functions and the setup function is not called anywhere:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    $ cargo test
    Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.89s
    Running unittests (target/debug/deps/adder-92948b65e88960b4)

    running 1 test
    test tests::internal ... ok

    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

    Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

    running 0 tests

    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

    Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

    running 1 test
    test it_adds_two ... ok

    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

    Doc-tests adder

    running 0 tests

    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

    We do not want common to appear in the test results showing ‘running 0 tests’. We just want it to be callable from other integration test files.

    To prevent common from appearing in the test output, we willcreatetests/common/mod.rsinstead of creatingtests/common.rs. This is a Rust naming convention; naming it this wayTell Rust not to treatcommonas an integration test file. Move the setup function code to_tests/common/mod.rs_and delete_tests/common.rs_file, this part will no longer appear in the test output.Subdirectories within the tests directory are not compiled as separate crates nor do they appear as separate test result sections in the test output.

    Once you have_tests/common/mod.rs_, you can use it as a module in any integration test file. Here is an_tests/integration_example of calling the setup function in test.rs__adds_two test example:

    Filename: tests/integration_test.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    use adder;

    mod common;

    #[test]
    fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
    }//Then in the test function you can call`common::setup()`.

    Integration tests for binary crates

    • If the project is a binary crate that only contains src/main.rs and no src/lib.rs:

      • Cannot create integration tests in the tests directory

      • Cannot import functions from main.rs into scope

    • Only library crates can expose functions for use by other crates

    • A binary crate means it runs independently

    IO Project: Building a Command Line Program

    Accepting Command Line Arguments

    1
    2
    3
    4
    5
    6
    use std::env;

    fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
    }

    Note:

    std::env::args will panic if any of its arguments contain invalid Unicode characters.

    If you need to accept arguments containing invalid Unicode characters, usestd::env::args_osinstead. This functionreturns OsString valuesinstead of String values. Here, std::env::args is used for simplicity, because OsString values differ per platform and are more complex to handle than String values.

    Separation of Concerns for Binary Projects

    The problem of the main function being responsible for multiple tasks is common in many binary projects. Therefore, the Rust community has developed a set of guidelines for separating concerns in binary programs when the main function starts to grow large. These steps are as follows:

    • Split the program into main.rs and lib.rs and put the program’s logic into lib.rs
    • When the command-line parsing logic is small, it can be kept in_main.rs_.
    • When the command-line parsing starts to become complex, similarly extract it from_main.rs_into_lib.rs_.

    After these processes, the responsibilities remaining in the main function should be limited to:

    • Calling the command-line parsing logic with parameter values
    • Setting any other configuration
    • Calling_lib.rs_the run function in
    • If run returns an error, handle that error

    The whole point of this pattern is separation of concerns:_main.rs_handles program execution, while_lib.rs_handles all the real task logic. Because the main function cannot be directly tested, this structure allows us to test them by moving all program logic into_lib.rs_functions, enabling us to test them. Only keep in_main.rs_The code in it will be small enough to read and verify its correctness.

    TDD (Test-Driven Development)

    Follow the Test-Driven Development (TDD) pattern to incrementally add the search logic to minigrep. This is a software development technique that follows these steps:

    1. Write a failing test and run it to ensure it fails for the reason you expect.
    2. Write or modify enough code to make the new test pass.
    3. Refactor the code you just added or modified, and ensure the tests still pass.
    4. Repeat from step 1!

    This is just one of many ways to write software, but TDD helps drive code design. Writing tests before the code that makes them pass helps maintain high test coverage during development.

    Writing the minigrep code

    src/lib.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    use std::env;
    use std::error::Error;
    use std::fs;

    pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    //Reading a file
    let contents = fs::read_to_string(&config.filename)?;
    // println!("With text:\n{}", contents);
    let results = if config.case_sensitive {
    search(&config.query, &contents)
    } else {
    search_case_insensitive(&config.query, &contents)
    };
    for line in results {
    println!("{}", line);
    }
    Ok(())
    }

    pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
    }

    impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
    if args.len() < 3 {
    return Err("not enough arguments");
    }
    let query = args[1].clone();
    let filename = args[2].clone();
    // println!("Search for {}", query);
    // println!("In file {}", filename);
    let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
    Ok(Config {
    query,
    filename,
    case_sensitive,
    })
    }
    }

    pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut result = Vec::new();
    for line in contents.lines() {
    if line.contains(query) {
    result.push(line);
    }
    }

    result
    }
    pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut result = Vec::new();
    let query = query.to_lowercase();
    for line in contents.lines() {
    if line.to_lowercase().contains(&query) {
    result.push(line);
    }
    }

    result
    }

    #[cfg(test)]
    mod tests {
    use super::*;
    #[test]
    fn case_sensitive() {
    let query = "duct";
    let contents = "\
    Rust:
    safe,fast,productive.
    Pick three.";
    assert_eq!(vec!["safe,fast,productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
    let query = "duct";
    let contents = "\
    Rust:
    safe,fast,productive.
    Pick three.";
    assert_eq!(
    vec!["safe,fast,productive."],
    search_case_insensitive(query, contents)
    );
    }
    }

    src/main.rs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    use std::env;
    use std::process;
    use minigrep::Config;

    fn main() {
    let args: Vec<String> = env::args().collect();
    // println!("{:?}",args);
    let config = Config::new(&args).unwrap_or_else(|err| {
    eprintln!("Problem parsing arguments:{}", err);
    process::exit(1);
    });
    if let Err(e) = minigrep::run(config){
    eprintln!("Application error: {}",e);
    process::exit(1);
    };
    }