1. Functional Language Features: Iterators and Closures
    1. Closures
      1. Definition of a closure
      2. Type inference of closures
      3. Generic parameter closure
        1. How to make a struct hold a closure
        2. Limitations of the Cacher Implementation
      4. Closures capture their environment
        1. How closures capture values from their environment
        2. The move keyword
        3. Best practices
    2. Iterators
      1. Iterator trait
        1. Several Iterator Methods
      2. Methods that consume the iterator
      3. Methods that produce other iterators
      4. Using Closures to Capture the Environment
      5. Creating Custom Iterators
      6. Improving the I/O Project
        1. Using Iterators and Removing clone
        2. Using the iterator returned by env::args directly
      7. Performance Comparison: Loops vs. Iterators
  2. Cargo and crates.io
    1. Customizing Builds with Release Profiles
      1. Custom profiles
    2. Documentation comments
      1. Generate documentation
      2. Generate documentation and browse
      3. Common sections
      4. Documentation comments as tests
      5. Add documentation comments to the item containing the comments
    3. Use pub use to re-export a convenient public API
    4. Publishing a Crate
    5. Publish a new version of an existing crate
    6. Use cargo yank to remove a version from Crates.io
    7. Cargo Workspaces
      1. Create a workspace
    8. Installing Binary Crates from CRATES.IO
      1. cargo install
    9. Extending cargo with custom commands
  3. Smart pointers
    1. Other differences between references and smart pointers
    2. Using BoxPoints to data on the heap
    3. Using Box to enable recursive types
      1. Cons List
      2. Calculating the Size of Non-Recursive Types
      3. Using BoxGiving recursive types a known size
    4. Dref Trait
    5. Dereference operator
    6. Treating Boxas a reference
    7. Defining our own smart pointer
    8. Treating a type like a reference by implementing the Deref trait
    9. Implicit Deref Coercion for functions and methods
    10. Dereferencing and Mutability
    11. Drop Trait
    12. Using std::mem::drop to drop a value early
    13. RcReference-counted smart pointer
    14. Cloning Rcwill increase the reference count
    15. RefCelland interior mutability
      1. interior mutability
      2. RefCellCompared to Boxthe difference
      3. Comparison of borrowing rules checked at different stages
      4. Choosing Box,Rc,RefCellbased on
      5. Interior mutability: mutably borrowing an immutable value
      6. Using RefCellTracking borrows at runtime
      7. Combining Rc and RefCell to have multiple owners of mutable data
    16. Other types that enable interior mutability
    17. Circular references causing memory leaks
      1. Avoid reference cycles: turn Rcinto Weak
      2. Strong VS Weak
        1. Creating a tree data structure: Node with child nodes
        2. Add a reference from child to parent
  4. Fearless Concurrency
    1. Using Threads to Run Code Simultaneously
    2. Creating a new thread with spawn
    3. Waiting for all threads to finish using join Handle
    4. Using move closures
    5. Using Message Passing to Transfer Data Between Threads
      1. Channel
      2. Create a Channel
      3. Channels and Ownership Transfer
      4. Sending multiple values and observing the receiver’s waiting
      5. Creating Multiple Producers by Cloning the Sender
    6. Shared-State Concurrency
      1. A mutex allows only one thread to access data at a time
      2. Mutex API
      3. Sharing a Mutex Between Threads
      4. Multiple threads and multiple ownership
      5. Atomic Reference Counting Arc
      6. Similarity between RefCell/Rc and Mutex/Arc
    7. Extensible Concurrency with the Sync and Send Traits
      1. Allowing Ownership Transfer Between Threads with Send
      2. Sync allows multi-threaded access
      3. Manually implementing Send and Sync is unsafe
  5. Object-Oriented Features of Rust
    1. Characteristics of Object-Oriented Languages
    2. Trait objects for values of different types
    3. Defining a Trait for Common Behavior
    4. Trait objects perform dynamic dispatch
    5. Trait objects require type safety
    6. Implementation of an Object-Oriented Design Pattern
  6. Patterns and Pattern Matching
    1. arm of match
    2. conditional if let expression
    3. while let conditional loop
    4. pattern matching in for loops
    5. Pattern matching in let statements
    6. Function parameters
    7. Refutability: whether a pattern can fail to match
    8. Match literal values
    9. Match named variables
    10. Match a mutable reference
    11. Multiple patterns
    12. Use …= to match a range of values
    13. Destructuring to decompose values
      1. Destructuring assignment
      2. Destructuring tuples
      3. Destructuring structs
      4. Destructuring enums
      5. Destructuring nested structs and enums
      6. Destructuring structs and tuples
    14. Ignoring Values in Patterns
      1. _ Ignoring the Entire Value
      2. Ignoring Parts of a Value with Nested _
      3. Ignoring Unused Variables by Starting Their Names with _
      4. … (Ignoring the Remaining Parts of a Value)
    15. Using Match Guards to Provide Extra Conditions
    16. @ bindings
  7. unsafe Rust
    1. unsafe superpowers
      1. Dereference a raw pointer
      2. Calling unsafe functions or methods
      3. Creating a safe abstraction for unsafe code.
      4. Using extern functions to call external code.
    2. Calling Rust Functions from Other Languages
    3. Accessing or Modifying a Mutable Static Variable
    4. Implementing an unsafe trait
    5. Accessing fields of a union
    6. When to use unsafe code
  8. Advanced traits
    1. Using associated types in trait definitions to specify placeholder types
    2. Difference between associated types and generics
    3. Default generic parameters and operator overloading
    4. Fully Qualified Syntax
    5. Using supertraits to require a trait to include functionality from other traits
    6. Using the newtype pattern to implement external traits on external types
  9. Advanced Types
    1. Using the Newtype Pattern for Type Safety and Abstraction
    2. Creating Type Synonyms with Type Aliases
    3. The Never Type
    4. Dynamic Sizes and the Sized Trait
    5. Rust’s general approach to dynamically sized types
    6. Another dynamically sized type: trait
    7. Sized trait
    8. ?Sized trait bound
  10. Advanced functions and closures
    1. Function pointers
    2. Difference between function pointers and closures
    3. Returning closures
  11. Macros
  12. Asynchronous programming
  13. Formatted Output
    1. Positional Parameters
    2. Named Parameters
    3. String Alignment
    4. Precision
    5. Binary, Octal, Hexadecimal
    6. Capturing Values from the Environment
    7. Exponent, Pointer Address, Escape
  14. Other
    1. error handling:
      1. unreachable!()
    2. misconception corollaries
      1. if T: 'static then T must be valid for the entire program
Cover image for rust language advanced

rust language advanced


Timeline

Timeline

2025-10-22

init

This article introduces advanced features of the Rust language, focusing on the implementation and application of functional language features such as closures and iterators. It elaborates on the definition of closures, type inference, and patterns for implementing memoization and lazy evaluation through structs, and analyzes the mechanism by which closures capture environment variables based on the three traits Fn, FnMut, and FnOnce, as well as the role of the move keyword.

Functional Language Features: Iterators and Closures

Closures

Rust’sclosuresclosures) are anonymous functions that can be saved in a variable or passed as arguments to other functions

  • Anonymous functions
  • Saved as variables, passed as arguments
  • You can create a closure in one place and then call it in another context to perform computation
  • Can capture values from the scope in which they are defined

Filename: src/main.rs

A function to replace an assumed computation that takes about two seconds

1
2
3
4
5
6
7
8
use std::thread;
use std::time::Duration;

fn simulated_expensive_calculation(intensity: u32) -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
intensity
}

Filename: src/main.rs

The business logic of the program, which takes input and calls simulated_expensive_calculation function to print out the workout plan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn generate_workout(intensity: u32, random_number: u32) {
if intensity < 25 {
println!(
"Today, do {} pushups!",
simulated_expensive_calculation(intensity)
);
println!(
"Next, do {} situps!",
simulated_expensive_calculation(intensity)
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
simulated_expensive_calculation(intensity)
);
}
}
}

File name: src/main.rs

The main function contains simulated user input and simulated random number input for the generate_workout function

1
2
3
4
5
6
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;

generate_workout(simulated_user_specified_value, simulated_random_number);
}

Definition of a closure

1
2
3
4
5
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
  • The closure definition is the part after the = in the assignment to expensive_closure. The closure definition starts with a pair of vertical bars (|), within which the closure’s parameters are specified;
  • Ifthere is more than one parameter, they can be separated by commas, for example |param1, param2|
  • After the parameters come the curly braces that hold the closure body —if the closure body is only one line, the curly braces can be omitted. After the curly braces, the closure ends, and a semicolon is needed for the let statement. Because the last line of the closure body has no semicolon (just like a function body), the return value of the last line of the closure body (num) serves as the return value when the closure is called.

Note:

This let statement means that expensive_closure contains the definition of an anonymous functiondefinition, not the invocation of the anonymous functionReturn value

Refactoring code

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
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};

if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}

Type inference of closures

  • Closures do not require annotation of parameter and return value types
  • Closures are usually short and work in a narrow context; the compiler can often infer the types
  • Types can be added manually
1
2
3
4
5
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};

Comparison with functions

1
2
3
4
fn  add_one_v1   (x: u32) -> u32 { x + 1 }//Function
let add_one_v2 = |x: u32| -> u32 { x + 1 };//Closure
let add_one_v3 = |x| { x + 1 };//Closure
let add_one_v4 = |x| x + 1 ;//Closure

src/main.rs

1
2
3
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);

When this closure executes the second line of code, the compiler can determine that the closure’s type is String, and an error will occur when executing the third line

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
   Compiling rust_programming v0.1.0 (/home/zhaohang/repository/rust_programming)
error[E0308]: mismatched types
--> src/main.rs:4:29
|
4 | let n = example_closure(5);
| --------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:3:29
|
3 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
help: try using a conversion method
|
4 | let n = example_closure(5.to_string());
| ++++++++++++

For more information about this error, try `rustc --explain E0308`.

Generic parameter closure

In the above code, the slow computation closure is still called more times than necessary. One way to solve this problem is to save the result into a variable for reuse wherever multiple results of the slow computation closure are needed throughout the code, so that the variable can be used instead of calling the closure again. However, this would result in many places where the result variable is repeatedly saved.

Fortunately, there is another available solution. You cancreate a struct that stores a closure and the result of calling the closure.This struct will only execute the closure when the result is needed and will cache the result value, so the rest of the code does not have to be responsible for saving the result and can reuse the value. You may have seen this pattern called*memoization* or***lazy evaluation****(lazy evaluation)*。

How to make a struct hold a closure

  • The definition of a struct needs to know the types of all fields, meaning it needs to specify the type of the closure.
  • Each closure instance has its own unique anonymous type, even if two closures have exactly the same signature.
  • So you need to use: generics and trait bounds

Fn Trait

  • provided by the standard library
  • All closures implement at least one of the following traits:
    • Fn
    • FnMut
    • FnOnce

Note:Functions also implement all three of theseFntrait. If you don’t need to capture values from the environment, you can use a function that implements the Fn trait instead of a closure.

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
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
calculation: T,
value: Option<u32>,
}
impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}

fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
}
}
}
}

The struct Cacher has a field calculation of generic type T.

The trait bound on T specifies that T is a closure using Fn.

Any closure we want to store in the calculation field of a Cacher instance must have a u32 parameter (specified by the content in parentheses after Fn) and must return a u32 (specified by the content after ->).

The value field is of type Option. Before executing the closure, value will be None. If code using Cacher requests the result of the closure, the closure is executed and the result is stored in the Some variant of the value field. Then,if the code requests the result of the closure again, the closure is not executed again; instead, the result stored in the Some variant is returned.

Refactoring the Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});

if intensity < 25 {
println!("Today, do {} pushups!", expensive_result.value(intensity));
println!("Next, do {} situps!", expensive_result.value(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result.value(intensity)
);
}
}
}

Limitations of the Cacher Implementation

  1. The first problem is that the Cacher instance assumes that for any arg parameter value of the value method, it will always return the same value.

Solution

You can use a HashMap instead of a single value:

  • key: the arg parameter

  • value: the result of executing the closure

  1. The second problem is that it can only accept a single u32 parameter and a u32 return value.

Solution:

Introduce two or more generic parameters.

Closures capture their environment

  • They can capture their environment and access variables from the scope where they are defined, whereas ordinary functions cannot
1
2
3
4
5
6
7
8
9
fn main() {
let x = 4;

let equal_to_x = |z| z == x;

let y = 4;

assert!(equal_to_x(y));
}
  • ClosureIncurs memory overhead
    In Rust, a closure is a compiler-generated anonymous struct whose fields are the captured variables.
1
2
3
4
5
6
7
8
9
10
11
12
let x = 10;
let c = || println!("{}", x);

// Internally, the compiler does something like:
struct Closure {
x: i32, // Captured variables
}
impl Fn() for Closure {
fn call(&self) {
println!("{}", self.x);
}
}

Capturing variables requires storage space

  • If it captures avalue, the closure stores that value in its own struct.
  • If it captures areference, the closure struct stores a pointer (the reference itself also takes up space).

Overhead size

  • Small variables (e.g., i32, bool): Almost no extra overhead; they can be stored directly in the closure’s struct.
  • Large variables (e.g., String, Vec, HashMap)
    • Ifmovecaptured, the closure will copy or move the entire object (heap memory may be bound to the closure).
    • If only captured by reference, the closure stores only a pointer, but the reference’s lifetime must be valid.

How closures capture values from their environment

is the same as the three ways functions receive parameters:

  • Taking ownership:FnOnce

    • FnOnce consumes the variables captured from the surrounding scope; the scope around the closure is called itsenvironmentenvironment. To consume the captured variables, the closure must take ownership and move them into the closure when it is defined. The ‘Once’ part of its name indicates that the closure cannot take ownership of the same variable multiple times, so it can only be called once.
  • Mutable borrowing:FnMut

    • FnMut takes mutable borrows of values so it can change its environment
  • Immutable borrowing:Fn

    • Fn takes immutable borrows from its environment

When creating a closure, Rust infers which trait to use based on how the closure uses values from its environment:

  • All closures implement FnOnce
  • Closures that don’t move captured variables implement FnMut
  • Closures that don’t need mutable access to captured variables implement Fn

There is actually a hierarchy: everything that implements Fn also implements FnMut, and everything that implements FnMut also implements FnOnce

The move keyword

Before the parameter listUsing the move keyword forces the closure to take ownership of the values it uses from its environment

  • Whenpassing a closure to a new thread to move data so it belongs to the new threadthis technique is most useful

Example

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = vec![1, 2, 3];

let equal_to_x = move |z| z == x;

println!("can't use x here: {:?}", x); // error

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

assert!(equal_to_x(y));
}

x is moved into the closure because the closure is defined with the move keyword. The closure then takes ownership of x, and main is no longer allowed to use x in the println! statement. Removing the println! fixes the issue.

Best practices

When specifying one of the Fn trait bounds, start with Fn; based on the closure body’s situation, if FnOnce or FnMut is needed, the compiler will tell you.

Iterators

The iterator pattern allows you to perform some processing on a sequence of items.Iteratorsiterator) is responsible for the logic of iterating over each item in a sequence and determining when the sequence ends. When using iterators, we don’t need to reimplement this logic.

In Rust,iterators are lazy, meaning they have no effect until you call methods that consume the iterator.

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

let v1_iter = v1.iter();

Iterator trait

  • All iterators implement this trait
  • defined in the standard library

The definition of this trait looks like this:

1
2
3
4
5
6
7
pub trait Iterator {
type Item;

fn next(&mut self) -> Option<Self::Item>;

// The default implementation of the method is omitted here
}

type Item and Self::Item, which define the trait’sassociated typeassociated type)。

This code shows that implementing the Iterator trait requires bothDefine aItemtype, and this Item type is used as thenextreturn type of the method. In other words, the Item type will be the type of elements returned by the iterator.

The Iterator trait only requires implementing one method: next

next:

  • which returns one item of the iteration each time
  • the result is wrapped in Some
  • when the iteration is over, it returns None

you can call the next method directly on the iterator

1
2
3
4
5
6
7
8
9
10
11
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];

let mut v1_iter = v1.iter();

assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
  • v1_iter needs to be mutable: calling the next method on the iterator changes the state inside the iterator that tracks the sequence position. In other words, the codeconsumes(consumes) or uses the iterator. Each call to next consumes one item from the iterator.
  • When using a for loop, there is no need to make v1_iter mutable because the for loop takes ownership of v1_takes ownership of iter and makes v1_iter mutable behind the scenes.

Several Iterator Methods

  • iterMethod: oncreating an iterator over immutable references(immutable references to elements)
  • into_iterMethod: creates aniterator that takes ownership
  • iter_mutMethod:iterating over mutable references

Methods that consume the iterator

  • In the standard library, the Iterator trait has some methods with default implementations
  • some of these methods call the next method

one of the reasons why the next method must be implemented when implementing the Iterator trait

  • those that call next are called "consuming adaptors

because calling them consumes the iterator

An example of a consuming adapter is the sum method. This method takes ownership of the iterator and repeatedly calls next to traverse the iterator, thereby consuming it. As it iterates through each item, it adds each item to a running total and returns the total when iteration is complete.

Filename: src/lib.rs

1
2
3
4
5
6
7
8
9
10
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

let total: i32 = v1_iter.sum();

assert_eq!(total, 6);
}

Methods that produce other iterators

Another category of methods defined in the Iterator trait are callediterator adaptorsiterator adaptors),

  • They allow you to change the current iterator into a different type of iterator.
  • Multiple iterator adaptors can be chained together.
  • However, because all iterators are lazy, you must call a consuming adapter method to get results from iterator adaptor calls.

Filename: src/main.rs

1
2
3
let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);

The map method uses a closure to call each element and produce a new iterator. The closure here creates a new iterator where each element in the vector is incremented by 1.

However, this code produces a warning:

= note: iterators are lazy and do nothing unless consumed

The code actually does nothing; the specified closure is never called. The warning reminds us why:Iterator adaptors are lazy, and here we need to consume the iterator.

Filename: src/main.rs

1
2
3
4
5
let v1: Vec<i32> = vec![1, 2, 3];

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

The underscore in the second line of code actually lets the compiler infer its type.

The collect method is aconsuming adapter, which collects the results into a collection type.

Because map takes a closure, you can specify any operation you want to perform on each element being iterated. This is an excellent example of how to use closures to customize behavior while reusing the iteration behavior provided by the Iterator trait.

Using Closures to Capture the Environment

The filter method

  • The filter method on an iterator takes a closure that takes each item from the iterator and returns a boolean.
  • If the closure returns true, the value will be included in the new iterator provided by filter.
  • If the closure returns false, the value will not be included in the resulting iterator.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
//The closure captures the`shoe_size`variable from the environment and uses its value to compare with the size of each shoe.
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];

let in_my_size = shoes_in_size(shoes, 10);

assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}

shoes_in_The my_size function takes ownership of a vector of shoes and a shoe size as parameters. It returns a vector containing only shoes of the specified size.

shoes_in_my_Inside the size function body, it calls into_iter to create an iterator that takes ownership of the vector. Then it calls filter to adapt this iterator into a new iterator that only contains elements for which the closure returns true.

The closure captures the shoe_size variable from the environment and uses its value to compare with the size of each shoe, retaining only shoes of the specified size. Finally, it calls collect to gather the values returned by the iterator adapter into a vector and returns it.

Creating Custom Iterators

The only method required in the Iterator trait definition is the next method. Once it is defined, you can use all other methods provided by the Iterator trait with default implementations to create custom iterators!

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
struct Counter {
count: u32,
}

impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}

impl Iterator for Counter {
type Item = u32;
//Here, the iterator's associated type Item is set to u32, meaning the iterator will return a collection of u32 values.

fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
//If the count value is less than 6, next returns the current value wrapped in Some,
//but if the count is greater than or equal to 6, the iterator returns None.
}
}

#[test]
fn calling_next_directly() {
let mut counter = Counter::new();

assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
assert_eq!(counter.next(), Some(3));
assert_eq!(counter.next(), Some(4));
assert_eq!(counter.next(), Some(5));
assert_eq!(counter.next(), None);
}

By implementing the Iterator trait by defining the next method, we can now use any methods from the Iterator trait that have default implementations defined in the standard library, because they all rely on the functionality of the next method.

For example, for some reason we want to take the values produced by a Counter instance and pair them with values from another Counter instanceskipping the first valueproduced after that, and pair them,multiply each pair of values together,and only keep the results that are divisible by three.Then add all the retained results together, which can be done as shown in the test in Listing 13-23:

Filename: src/lib.rs

1
2
3
4
5
6
7
8
9
#[test]
fn using_other_iterator_trait_methods() {
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
assert_eq!(18, sum);
}

Listing 13-23: Using various methods on a custom Counter iterator, noting that Counter itself is an Iterator

Note that zip produces only four pairs of values; the theoretical fifth pair (5, None) is never produced because zip returns None when either input iterator returns None.

All these method calls are possible because we specified how the next method works, and the standard library provides default implementations for other methods that call next.

Improving the I/O Project

Using Iterators and Removing clone

Filename: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();

let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

Ok(Config {
query,
filename,
case_sensitive,
})
}
}

Using the iterator returned by env::args directly

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
fn main() {
//let args: Vec<String> = env::args().collect();

let config = Config::new(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});

// --snip--
}

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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(mut args: std::env::Args) -> Result<Config, &'static str> {
// if args.len() < 3 {
// return Err("not enough arguments");
// }
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let filename = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file name"),
};
// 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

contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
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
contents
.lines()
.filter(|line| line.to_lowercase().contains(query.to_lowercase().as_str()))
.collect()
}

#[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
use std::env;
use std::process;
use minigrep::Config;

fn main() {
// println!("{:?}",args);
let config = Config::new(env::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);
};
}

Performance Comparison: Loops vs. Iterators

Iterators are Rust’szero-cost abstractionszero-cost abstractions), meaning that abstractions do not introduce runtime overhead, which aligns with what Bjarne Stroustrup (the designer and implementer of C++) defined in “Foundations of C++” (2012) aszero-overheadzero-overhead) is identical:

In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.

  • Bjarne Stroustrup “Foundations of C++”

Overall, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.

  • Bjarne Stroustrup “Foundations of C++”

Cargo and crates.io

Customizing Builds with Release Profiles

release profile

  • are predefined
  • customizable
  • Each profile configuration is independent of the others

Cargo’s two main profiles

  • dev profile: used for development cargo build
  • release profile: used for release cargo build --release

Custom profiles

Add [profile.xxxx] sections in Cargo.toml to override subsets of default configurations

File name: Cargo.toml

1
2
3
4
5
[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

The opt-level setting controls how much optimization Rust applies to your code. This configuration value ranges from 0 to 3. Higher optimization levels require more compilation time, so if you are developing and compiling frequently, you may prefer faster compilation at the cost of some code performance. This is why the default opt-level for dev is 0.

Documentation comments

  • Generate HTML documentation
  • Display documentation comments for public APIs: How to use the API
  • Use ///
  • Supports Markdown
  • Place before the item being described

Generate documentation

Run the rustdoc tool

1
cargo doc

Place the generated documentation under target/doc

Generate documentation and browse

1
cargo doc --open

Common sections

#Examples

Other common sections

1
2
3
Panics: 函数可能发生panic的场景
Errors: 如果函数返回Result,描述可能的错误种类,以及可导致错误的条件
Safety: 如果函数处于unsafe调用,就应该解释函数unsafe的原因,以及调用者确保的使用前提

Documentation comments as tests

Run cargo test: treat example code in documentation comments as tests

Filename: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}

Try running cargo test on the documentation for the add_one function as in the example; you should see a section like this in the test results:

1
2
3
4
5
6
Doc-tests my_crate

running 1 test
test src/lib.rs - add_one (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s

Now try changing the function or example to make the assert_eq! in the example panic. Run cargo test again, and we will see that the documentation test catches that the example is no longer in sync with the code!

Add documentation comments to the item containing the comments

  • Symbol: //!
  • This type of comment typically describes crates and modules

Crate root (by convention src/lib.rs)

Within a module, document the crate or module as a whole

Example:

Filename: src/lib.rs

1
2
3
4
5
6
7
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.

/// Adds one to the number given.
// --snip--

Use pub use to re-export a convenient public API

The file structure you use during development may not be convenient for users. Your structure might be a hierarchical one with multiple levels, but this is not convenient for users. This is because people who want to use types defined deep in the hierarchy may have difficulty finding those types. They might also get annoyed having to use use my_crate::some_module::another_module::UsefulType; instead of use my_crate::UsefulType; to use the type.

Usepub useRe-export items to make the public structure different from the private structure

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
//! # Art
//!
//! A library for modeling artistic concepts.

pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}

/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}

pub mod utils {
use crate::kinds::*;

/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
SecondaryColor::Green
}
}

Filename: src/main.rs

1
2
3
4
5
6
7
8
use art::kinds::PrimaryColor;
use art::utils::mix;

fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}

To remove the internal organization of the crate from the public API, we can take the art crate from the example and add pub use statements to re-export items to the top-level structure, as shown in Listing 14-5:

Filename: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//! # Art
//!
//! A library for modeling artistic concepts.

pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;

pub mod kinds {
// --snip--
}

pub mod utils {
// --snip--
}

Use

Filename: src/main.rs

1
2
3
4
5
6
use art::mix;
use art::PrimaryColor;

fn main() {
// --snip--
}

Publishing a Crate

With a unique name, version number, author information added by cargo new when creating a new project, description, and chosen license, the_Cargo.toml_file of a project ready to publish might look like this:

Filename: Cargo.toml

1
2
3
4
5
6
7
8
9
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
author = "even629"

[dependencies]

Cargo’s documentation describes other metadata that can be specified, which can help your crate be more easily discovered and used!

Publishing:

1
$ cargo publish

Once a crate is published, it is permanent: the version cannot be overwritten, and the code cannot be deleted

  • Purpose: projects that depend on that version can continue to work correctly

Publish a new version of an existing crate

Modify the version and republish

Use cargo yank to remove a version from Crates.io

  • Cannot delete previous versions of a crate

Yanking a version prevents new projects from starting to depend on that version, but all existing projects with that dependency can still download and depend on it. Essentially, yanking means that all projects with_Cargo.lock_dependencies will not be broken, while any newly created_Cargo.lock_will not be able to use the yanked version.

To yank a crate, run cargo yank and specify the version you want to yank:

1
$ cargo yank --vers 1.0.1

You can also undo a yank and allow projects to start depending on a version again by adding --undo to the command:

1
$ cargo yank --vers 1.0.1 --undo

Yankingdoes notdelete any code. For example, the yank feature is not intended to delete accidentally uploaded secrets. If this happens, reset those secrets immediately.

Cargo Workspaces

  • Cargo workspace: helps manage multiple interrelated crates that need to be developed together
  • A Cargo workspace is a set of packages that share the same Cargo.lock and output directory

Create a workspace

To run a binary crate in the top-level_add_directory, you can use the -p argument and the package name to run cargo run specifying the package we want to use in the workspace:

1
2
3
4
$ cargo run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!

This runs the code in_adder/src/main.rs_, which depends on the add_one crate

Installing Binary Crates from CRATES.IO

  • Command: cargo install
  • Source: https://crates.io
  • Limitation: Only crates with a binary target can be installed

Binary target: is a runnable program

  • Generated by crates that have src/main.rs or other files designated as binaries

Usually: The README contains a description of the crate:

  • Has a library target
  • Has a library target
  • Both

cargo install

Binaries installed by cargo install are stored in the bin folder of the root directory

Extending cargo with custom commands

  • Cargo is designed to be extensible with subcommands
  • Example: if a binary in $PATH is named cargo-something, you can run it like a subcommand:
1
$ cargo something
  • Custom commands like this can be listed with: cargo --list
  • Advantage: you can install extensions with cargo install and run them like built-in tools

Smart pointers

  • Pointerpointer) is a general concept for a variable that contains a memory address.

This address refers to, or “points at,” some other data.

  • The most common pointer in Rust is thereferencereference)。

References are indicated by the & symbol and borrow the value they point to. They have no special capabilities other than referring to data, and they alsohave no overhead, so it is the most widely used.

  • Smart pointerssmart pointers) are a class of data structures that behave like pointers but also have additional metadata and capabilities.

Other differences between references and smart pointers

  • References: only borrow data
  • Smart pointers: often own the data they point to

Examples of smart pointers:

  • String and Vec
  • both own a memory region and allow users to operate on it
  • also have metadata (such as capacity, etc.)
  • provide additional functionality or guarantees (String guarantees its data is valid UTF-8 encoding)

Implementation of smart pointers

  • **Smart pointers are usually implemented using structs,**and implement the Deref and Drop traits

    • The Deref trait allows instances of the smart pointer struct to be used like references

    • The Drop trait lets you customize what happens when a smart pointer instance goes out of scope.

Using BoxPoints to data on the heap

  • BoxIs the simplest smart pointer:

    • Allows you to store data on the heap (rather than the stack)
    • The stack holds a pointer to the heap data
    • No performance overhead
    • No other extra features
  • BoxImplements the Deref trait and the Drop trait

Often used in scenarios like:

  • When you have a type whose size is unknown at compile time and you want to use a value of that type in a context that requires an exact size
  • When you have a large amount of data and want to transfer ownership while ensuring the data isn’t copied
  • When you want to own a value and only care that it implements a specific trait, rather than its concrete type
1
2
3
4
fn main() {
let b = Box::new(5);
println!("b = {}",b);
}

Using Box to enable recursive types

  • At compile time, Rust needs to know how much space a type takes up
  • The size of recursive types cannot be determined at compile time.
  • But the size of the Box type is fixed.
  • Using Box in recursive types solves the above problem.
  • Cons List in Functional Languages

Cons List

_cons list_is a data structure originating from the Lisp programming language and its dialects. In Lisp, the cons function (short for “construct function”) takes two arguments to construct a new list, typically a single value and another list.

The concept of the cons function relates to more common functional programming terminology; “cons_x_onto_y_” usually means constructing a new container by placing_x_'s elements at the beginning of the new container, followed by the elements of container_y_.

Each item in a cons list contains two elements: the value of the current item and the next item. The last item contains a value called Nil and has no next item. A cons list is produced by recursively calling the cons function. The canonical name for the base case of recursion is Nil, which signals the end of the list.

The canonical name for the base case of recursion is Nil, which signals the end of the list. Note that this is different from the concept of “null” or “nil”, which represent invalid or missing values.

Cons List is not a common collection in Rust.

1
2
3
4
5
6
7
8
9
use crate::List::{Cons,Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}

enum List {
Cons(i32,List),
Nil,
}

Runtime Error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Compiling my_box v0.1.0 (C:\Users\cauchy\Desktop\rust\my_box)
error[E0072]: recursive type `List` has infinite size
--> src\main.rs:6:1
|
6 | enum List {
| ^^^^^^^^^
7 | Cons(i32,List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
7 | Cons(i32,Box<List>),
| ++++ +

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

Calculating the Size of Non-Recursive Types

Message enum:

1
2
3
4
5
6
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

When Rust needs to know how much space to allocate for a Message value, it can examine each variant and find that

  • Message::Quit requires no space,
  • Message::Move needs enough space to store two i32 values, and so on.
  • Because an enum only uses one of its variants at a time, the space needed for a Message value is equal to the space needed to store its largest variant.

In contrast, what happens when the Rust compiler checks a recursive type like the List example above? The compiler tries to calculate how much memory is needed to store a List enum and starts checking the Cons variant. The space needed for Cons equals the size of an i32 plus the size of a List. To calculate how much memory List needs, it checks its variants, starting with Cons. The Cons variant stores an i32 value and a List value, and this calculation goes on infinitely.

Using BoxGiving recursive types a known size

  • Because Boxis a pointer, we always know how much space it needs

The size of a pointer does not change based on the amount of data it points to.

  • Box
  1. Only provides indirection and heap memory allocation
  2. No other extra features
  3. No performance overhead
  4. Suitable for scenarios requiring indirection, such as a Cons List
  5. Implemented the Deref trait and Drop trait

Dref Trait

  • Implementing the Deref trait allows us to**customize the behavior of the dereference operator ***
  • By implementing Deref, smart pointers canbe treated like references

Dereference operator

Filename: src/main.rs

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

assert_eq!(5, x);
assert_eq!(5, *y);
}

Treating Boxas a reference

Filename: src/main.rs

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = Box::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

Defining our own smart pointer

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct MyBox<T>(T);

impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}

fn main() {
let x = 5;
let y = MyBox::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

The compilation error we get is:

1
2
3
4
5
6
7
8
9
10
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^

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

MyBoxThe type cannot be dereferenced because we have not yet implemented this functionality on the type. To enable the dereference functionality of the * operator, we need to implement the Deref trait.

Treating a type like a reference by implementing the Deref trait

  • The Deref trait in the standard library requires us to implement a deref method:

    • This method borrows self

    • returns a reference to the internal data

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

When we in the example code

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = MyBox::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

enter *y, Rust actually runs the following code under the hood:

*(y.deref())

Implicit Deref Coercion for functions and methods

  • Deref Coercion is a convenience feature provided forfunctions and methodsa convenience feature provided
  • Assuming T implements the Deref trait: Deref Coercion can convert a reference to T into a reference to the type resulting from Deref on T
  • When passing a reference of some type to a function or method, but its type does not match the defined parameter type:
  1. Deref Coercion will automatically occur
  2. The compiler will make a series of calls to deref to convert it into the required parameter type
  3. It is done at compile time, with no additional performance cost

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
//&m &MyBox<String>
//deref &String
//&String &str
hello(&m);
}
  1. Here, we call the hello function with &m, which is a MyBoxvalue reference
  2. Because in the example, on MyBoxwe implemented the Deref trait, Rust can turn &MyBoxinto &String via deref calls.
  3. The standard library provides a Deref implementation on String that returns a string slice, which can be seen in the Deref API documentation. Rust calls deref again to turn &String into &str, which matches the definition of the hello function.

If Rust did not implement deref coercion, to use &MyBoxtype value to call hello, we would have to write the following code

Filename: src/main.rs

1
2
3
4
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}

Dereferencing and Mutability

  • You can use the DerefMut trait to overload the * operator for mutable references
  • Rust performs deref coercion when the type and trait meet the following three conditions:
  1. When T: Deref<Target=U>, allows &T to be converted to &U
  2. When T: DerefMut<Target=U>, allows &mut T to be converted to &mut U
  3. When T: Deref<Target=U>, allows &mut T to be converted to &U
SituationConditionConversion
1T: Deref<Target=U>&TAutomatic change&U
2T: DerefMut<Target=U>&mut TAutomatic change&mut U
3T: Deref<Target=U>&mut TMutable referenceDowngradeto immutable reference&U

Example:

1
2
3
4
5
6
7
8
fn greet(name: &str) {
println!("Hello {name}");
}

let s = String::from("Rust");
greet(&s);
// Normally, &String cannot be passed to &str,
// but because String implements Deref<Target=str>, it automatically becomes &str

Drop Trait

  • Implementing the Drop trait allows us to customizethe action that occurs when a value is about to go out of scope

    1. For example: releasing files, network resources, etc.

    2. Any type can implement the Drop trait

  • The Drop trait only requires you to implement the drop method

    1. Parameter: a mutable reference to self
  • The Drop trait is in the prelude

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct CustomSmartPointer {
data: String,
}

impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}

fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}

When running this program, the following output appears:

1
2
3
4
5
6
7
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/drop-example`
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

Using std::mem::drop to drop a value early

  • It is difficult to directly disable automatic drop functionality, and it is unnecessary

    1. The purpose of the Drop trait is to perform automatic cleanup logic
  • Rust does not allow manually calling the drop method of the Drop trait

  • Butyou can call the standard library’s std::mem::drop function (in the prelude) to drop a value early

Filename: src/main.rs

1
2
3
4
5
6
7
8
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}

Running this code will print the following:

1
2
3
4
5
6
7
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

We also don’t need to worry about accidentally cleaning up values still in use, which would cause a compiler error: the ownership system ensures references are always valid, and also ensures that drop is only called once when the value is no longer used.

RcReference-counted smart pointer

  • Sometimes a value has multiple owners

  • To support multiple ownership: Rt

    1. reference counting

    2. Tracks references to the value

    3. 0 references: the value can be cleaned up

  • Data that needs to be allocated on the heap, whichis read (read-only) by multiple parts of the program, butat compile time it cannot be determined which part will finish using the data last

  • NoteRccan only be used in single-threaded scenarios

We want to create two lists that share ownership of a third list, and the concept will look as shown in the figure:

Filename: src/main.rs

Cannot use two BoxThe list attempts to share ownership of the third list

1
2
3
4
5
6
7
8
9
10
11
12
enum List {
Cons(i32, Box<List>),
Nil,
}

use crate::List::{Cons, Nil};

fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}

Error:

1
2
3
4
5
6
7
8
9
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move

We modify the definition of List to use Rcinstead of Box, as shown in the listing. Now each Cons variant contains a value and an Rc pointing to a List. When creating b, instead of taking ownership of a, it clones the Rc contained in a, which increases the reference count from 1 to 2 and allows a and b to share the Rc's data ownership. Creating c also clones a, increasing the reference count from 2 to 3. Each call to Rc::clone, the Rc's data reference count increases, and the data will not be cleaned up until there are zero references.

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
enum List {
Cons(i32, Rc<List>),
Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}

Data structure relationship:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        a (Rc)


Cons(5)


Cons(10)


Nil

b ---> Cons(3) ───┐


a (共享)

c ---> Cons(4) ───┘

You can also call a.clone() instead of Rc::clone(&a), but here the Rust convention is to use Rc::clone.

  • The implementation of Rc::clone does not perform a deep copy of all data like most types’ clone implementations do.
  • Rc::clone only increments the reference count, which doesn’t take much time. Deep copying can take a long time

Cloning Rcwill increase the reference count

Filename: src/main.rs

Rc::strong_count gets the reference count

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

This code will print:

1
2
3
4
5
6
7
8
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

We can see that the Rc in ahas an initial reference count of 1, and each time clone is called, the count increases by 1. When c goes out of scope, the count decreases by 1. There is no need to call a function to decrease the count like calling Rc::clone to increase it; the implementation of the Drop trait automatically decreases the reference count when the Rcvalue goes out of scope.

What we cannot see from this example is that at the end of main, when b and then a go out of scope, the count here will be 0, and the Rcis fully cleaned up. Using Rcallows a value to have multiple owners, and the reference count ensures that the value remains valid as long as any owner still exists.

  • RcThroughimmutable references, Rcallows read-only sharing of data between multiple parts of the program.
  • If Rcalso allowed multiple mutable references, it would violate one of the borrowing rules discussed in Chapter 4: multiple mutable borrows to the same location can cause data races and inconsistencies.

RefCelland interior mutability

interior mutability

  • interior mutability is one of Rust’s design patterns
  • it allows you to mutate data even when there are immutable references to it

the data structure uses unsafe code to bypass Rust’s normal mutability and borrowing rules

  • Unlike Rc, RefCellthe type represents sole ownership of the data it holds

Recall the borrowing rules:

  1. At any given time, you can have either one mutable reference or any number of immutable references
  2. References must always be valid

RefCellCompared to Boxthe difference

BoxRefCell
compile-timeenforces the borrowing rules on codeonly atruntimecheck borrowing rules
otherwise an error occursotherwise triggers panic

Comparison of borrowing rules checked at different stages

Compile timeRuntime
expose problems earlyproblem exposure delayed, even to production environment
no runtime overheadsome performance loss due to reference counting
best choice for most scenariosimplement certain specific memory-safe scenarios (modifying own data in immutable environment)
is Rust’s default behavior
  • Similar to Rc, can only be used insingle-threadedscenarios

Choosing Box,Rc,RefCellbased on

BoxRcRefCell
Owner of the same dataOneMultipleOne
Mutability, borrow checkingMutable, immutable borrows (compile-time check)Immutable borrows (compile-time check)Mutable, immutable borrows (runtime check)

Interior mutability: mutably borrowing an immutable value

A corollary of the borrowing rules is that when you have an immutable value, you cannot borrow it mutably. For example, the following code will not compile:

1
2
3
4
fn main() {
let x = 5;
let y = &mut x;
}

If you try to compile it, you will get the following error:

1
2
3
4
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13

Here is a scenario we want to test:

We are writing a library that tracks the difference between a value and a maximum, and sends messages based on how close the current value is to the maximum. For example, this library could be used to track the number of API calls a user is allowed.

The library only provides the functionality of tracking the difference from the maximum and determining what messages to send in which situations. The program that uses this library is expected to provide the mechanism for actually sending messages: the program can choose to log a message, send an email, send a text message, etc. The library itself doesn’t need to know these details; it just needs to implement the Messenger trait it provides. Listing 15-20 shows the library code:

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
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
pub trait Messenger {
fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}

pub fn set_value(&mut self, value: usize) {
self.value = value;

let percentage_of_max = self.value as f64 / self.max as f64;

if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;

struct MockMessenger {
sent_messages: Vec<String>,
}

impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}

impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}

#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

limit_tracker.set_value(80);

assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}

An important part of this code is the Messenger trait, which has a method send that takes animmutable referenceto self and a text message. This trait is the interface that mock objects need to implement so that the mock can be used just like a real object. Another important part is that we need to test the behavior of LimitTracker’s set_value method. We can change the value passed to the value parameter, but set_value does not return any value that we can assert on. That is, if we create a LimitTracker with a value that implements the Messenger trait and a specific max, when we pass different value values, the message sender should be told to send the appropriate messages.

The mock object we need is one where calling send does not actually send an email or message, but only records that the message was notified to be sent. We can create a new mock object instance, use it to create a LimitTracker, call LimitTracker’s set_value method, and then check whether the mock object has the messages we expect. Listing 15-21 shows an attempt at such a mock object implementation, but the borrow checker does not allow it:

However, this test has a problem:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- help: consider changing that to be a mutable reference: `&mut self`
...
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` due to previous error
warning: build failed, waiting for other jobs to finish...
error: build failed

We cannot modify MockMessenger to record messages becausethe send method takes an immutable reference to selfWe also cannot follow the error message’s suggestion to use &mut self instead, because then the signature of send would not match the signature defined in the Messenger trait (you can try this change and see what error message appears).

This is exactly where interior mutability comes into play! We will store sent_messages via RefCell, and then send will be able to modify sent_messages and store the messages.

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
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;

struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}

impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}

impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}

#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--

assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}

Using RefCellTracking borrows at runtime

  • Two methods (safe interface)

    1. The borrow method: returns the smart pointer Ref, which implements Deref

    2. The borrow_mut method: returns RefMut, which implements Deref

  • RefCellIt records how many active Refand RefMutsmart pointers

    1. Each call to borrow: immutable borrow count +1

    2. Any RefWhen its value goes out of scope and is dropped: immutable borrow count -1

    3. Each call to borrow_mut: mutable borrow count +1

    4. Any RefMutWhen its value goes out of scope and is dropped: mutable borrow count -1

  • Rust uses this count to maintain the borrowing rules: at any given time, you can have either multiple immutable borrows or one mutable borrow

Combining Rc and RefCell to have multiple owners of mutable data

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
24
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
let value = Rc::new(RefCell::new(5));

let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

*value.borrow_mut() += 10;

println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}

When we print a, b, and c, we can see that they all have the modified value 15 instead of 5:

1
2
3
4
5
6
7
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

Other types that enable interior mutability

  • Cell: accessing data through copying
  • Mutex: used to implement the interior mutability pattern in cross-thread scenarios

Circular references causing memory leaks

Rust’s memory safety guarantees make it difficult to accidentally create memory that is never cleaned up (known asMemory Leakmemory leak)), but it is not impossible. Unlike rejecting data races at compile time, Rust does not guarantee complete avoidance of memory leaks, meaning memory leaks are considered memory-safe in Rust. This can be seen through Rcand RefCell: the possibility of creating reference cycles exists. This causes memory leaks because the reference count of each item never reaches 0, and its value is never dropped.

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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

// List is a linked list, each node is Cons(value, next) or Nil
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
//
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}

fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());

let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());

if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}

println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));

// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}

If you keep the last println! line commented and run the code, you will get the following output:

1
2
3
4
5
6
7
8
9
10
11
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

If you uncomment the last println! and run the program, Rust will try to print the cycle of a pointing to b pointing to a until stack overflow. This is because:

In Rust,#[derive(Debug)]for enums (like linked lists) generates a recursive printing logic: Note:nextalso callsDebug→ then prints itsnext→ recursive call, and this recursive call has no termination condition, thus eventually causing stack overflow

1
2
3
4
5
6
7
8
9
10
11
impl Debug for List {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Cons(v, next) => {
write!(f, "Cons({:?}, {:?})", v, next)
}
Nil => write!(f, "Nil")
}
}
}

Solution to Prevent Memory Leaks

  • Rely on the developer to ensure, not on Rust

  • Restructure data: some references express ownership, some do not

    1. In a reference cycle, some parts have ownership relationships, others do not

    2. Only ownership relationships affect value cleanup

Avoid reference cycles: turn Rcinto Weak

  • Rc::clone increments the strongcount of an Rc_instance, and an Rcinstance is only cleaned up when its strong_count reaches 0

  • RcAn instance can create a Weak Reference to its value by calling Rc::downgrade

    1. The return type is Weak(smart pointer)

    2. Calling Rc::downgrade increments weak_count by 1

  • RcUse weak_count to track how many Weak

  • A weak_count of 0 does not affect RcCleanup of the instance

Strong VS Weak

  • Strong Reference is about how to share RcOwnership of the instance
  • Weak Reference does not express the above meaning
  • Using Weak Reference does not create circular references:

When the number of Strong References is 0, the Weak Reference is automatically broken

  • Before using Weak, ensure that the value it points to still exists:

On WeakCall the upgrade method on the instance, returning Option<Rc>

Creating a tree data structure: Node with child nodes

File name: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});

let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}

Here we clone the Rc in leafand store it in branch, meaning the Node in leaf now has two owners: leaf and branch. You can get leaf from branch via branch.children, but not from leaf to branch. Leaf has no reference to branch and does not know they are related. We want leaf to know that branch is its parent. We will do this later

Add a reference from child to parent

To make child nodes aware of their parent, we need to add a parent field to the Node struct definition. The question is what type parent should be. We know it cannot contain Rc, because then leaf.parent would point to branch and branch.children would contain a pointer to leaf, creating a reference cycle that would cause their strong_count to never reach 0.

Now let’s think about this relationship in a different way:

  • A parent node should own its children
  • If a parent node is dropped, its children should also be dropped
  • Howevera child node should not own its parent
  • If a child node is dropped, its parent should still exist

This is exactly a case for weak references!

So parent uses Weaktype instead of Rc, specifically RefCell<Weak>. Now the Node struct definition looks like this:

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
24
25
26
27
28
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});

*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

Creating a leaf node is similar to how a leaf node was created in Listing 15-27, except the parent field is different: leaf starts without a parent, so we create a new empty Weak reference instance.

At this point, when trying to use the upgrade method to get a reference to leaf’s parent node, we get a None value, as shown in the first println! output:

1
leaf parent = None

When creating the branch node, it also creates a new Weakreference, because branch does not have a parent. leaf is still a child of branch. Once we have a Node instance in branch, we can modify leaf to have a Weakreference pointing to its parent. Here, we use the borrow_mut method of the RefCell<Weak> in leaf’s parent field, and then use the Rc::downgrade function to create a Weakreference pointing to branch from the Rcvalue in branch.

When printing leaf’s parent again, this time we get a Some value containing branch: now leaf can access its parent! When printing leaf, we also avoid the cycle that would eventually cause a stack overflow as in Listing 15-26: Weakreferences are printed as (Weak):

1
2
3
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

The absence of infinite output indicates that this code does not create a reference cycle. This can also be seen from observing the results of Rc::strong_count and Rc::weak_count calls.

Visualizing changes in strong_count and weak_countLet’s observe Rcthe strong_count and weak_count values change. This shows what happens when branch is created and then dropped when it leaves scope. These changes are shown in the example:

Example: Creating a branch in an inner scope and checking its strong and weak reference counts

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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
// leaf strong = 1, weak = 0
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);

{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});

*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
// branch strong = 1, weak = 1
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
// leaf strong = 2, weak = 0
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
// leaf parent = None
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

// leaf strong = 1, weak = 0
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}

Once leaf is created, its Rchas a strong count of 1 and a weak count of 0. In the inner scope, branch is created and associated with leaf, at which point the Rc in branchhas a strong count of 1 and a weak count of 1 (because leaf.parent points to branch via Weak). Here, leaf’s strong count is 2, because branch’s branch.children now stores a copy of leaf’s Rccopy, but the weak count remains 0.

When the inner scope ends, branch goes out of scope, and the Rcstrong count decreases to 0, so its Node is dropped. The weak count of 1 from leaf.parent is irrelevant to whether the Node is dropped, so no memory leak occurs!

If you try to access leaf’s parent after the inner scope ends, you get None again. At the end of the program, the Rc in leafhas a strong count of 1 and a weak count of 0, because leaf is now the only reference to the Rc.

All this logic for managing counts and values is built into Rcand Weakand their Drop trait implementations. By specifying the relationship from child to parent as a Weakreference, it is possible to have bidirectional references between parent and child nodes without causing reference cycles and memory leaks.

Fearless Concurrency

  • Concurrent programmingConcurrent programming), meaning different parts of a program execute independently,
  • Parallel programmingparallel programming) means different parts of a program execute simultaneously

Using Threads to Run Code Simultaneously

In most modern operating systems, the code of an executed program runs in aprocessprocess), and the operating system manages multiple processes. Inside a program, there can also be multiple independent parts running simultaneously. The ability to run these independent parts is calledthreadsthreads)。

Splitting computations in a program into multiple threads can improve performance because the program can perform multiple tasks simultaneously, but this also adds complexity. Since threads run concurrently, the order of execution of code in different threads cannot be guaranteed in advance. This can lead to issues such as:

  • Race conditions, where multiple threads access data or resources in an inconsistent order
  • Deadlocks, where two threads wait for each other to stop using the resources they own, preventing them from continuing
  • Bugs that only occur under specific conditions and are difficult to reproduce and fix reliably

Programming languages have several different approaches to implementing threads.

  • Many operating systems provide APIs for creating new threads. This model, where a programming language calls the OS API to create threads, is sometimes called_1:1_, where one OS thread corresponds to one language thread.The Rust standard library only provides a 1:1 threading implementation; it requires a small runtime (meaning Rust does not need an additional thread scheduler or complex mechanisms; it only needs to track thread handles, call OS APIs when creating and destroying threads, with no extra user-space scheduling).
  • Some crates implement other threading models with different trade-offs, namely language-implemented threads (green threads): the M:N model. This requires a larger runtime

Creating a new thread with spawn

To create a new thread, you need to call thethread::spawnfunction and pass a closure containing the code you want to run in the new thread

Filename: src/main.rs

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

fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});

for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}

When the main thread ends, the new thread also ends, regardless of whether it has finished executing.

Waiting for all threads to finish using join Handle

  • The return type of thread::spawn is JoinHandle.
  • JoinHandle is an owned value.
  • When the join method is called on it, it blocks the execution of the currently running thread until the threads represented by the handle terminate.

Filename: src/main.rs

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

fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});

for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}

handle.join().unwrap();
}

Calling join on the handle blocks the current thread until the thread represented by the handle finishes.BlockingBlocking) a thread means preventing that thread from performing work or exiting. Because we placed the join call after the main thread’s for loop,

Using move closures

  • Move closures are often used with the thread::spawn function, allowing you to use data from other threads.
  • When creating a thread, transfer ownership of a value from one thread to another.

Example: Attempting to use a vector created in the main thread from another thread

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
use std::thread;

fn main() {
let v = vec![1, 2, 3];

let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});

handle.join().unwrap();
}

The closure uses v, so it captures v and makes it part of the closure’s environment. Because thread::spawn runs this closure in a new thread, v can be accessed in the new thread. However, when compiling this example, you get the following error:

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
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {:?}", v);
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++

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

Rust willinferHow to capture v, becauseprintln! only needs a reference to v, the closure tries to borrow v. However, there is a problem:Rust doesn’t know how long the new thread will run, so it cannot know whether the reference to v will always be valid

Listing 16-4 shows a scenario where a reference to v is likely no longer valid:

Filename: src/main.rs

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

fn main() {
let v = vec![1, 2, 3];

let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});

drop(v); // oh no!

handle.join().unwrap();
}

By adding the move keyword before the closure, we force the closure to take ownership of the values it uses, rather than letting Rust infer that it should borrow the values. The following shows the modification to the code, which compiles and runs as we expect:

Example: Using the move keyword to force taking ownership of the values it uses

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
use std::thread;

fn main() {
let v = vec![1, 2, 3];

let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});

handle.join().unwrap();
}

Using Message Passing to Transfer Data Between Threads

An increasingly popular approach to ensuring safe concurrency ismessage passingmessage passing), where threads or actors communicate by sending messages containing data. This idea comes from the slogan in [Go programming language documentation]: “Do not communicate by sharing memory; instead, share memory by communicating.”

  • Threads (or Actors) communicate by sending messages (data) to each other
  • Rust: Channel (provided by the standard library)

Channel

  • Channel contains:transmitterreceiver
  • Call the transmitter’s method to send data
  • The receiver will check and receive the arriving data
  • If either the transmitter or the receiver is dropped, the Channel is ‘closed’

Create a Channel

  • Usempsc::channelfunction to create a Channel
  1. mpsc stands formultiple producer,single consumer(multiple producers, single consumer)
  2. Returns a tuple: the elements are the transmitter and receiver respectively
1
pub fn channel<T>() -> (Sender<T>, Receiver<T>)

Let’s move the transmitter to a new thread and send a string, so the new thread can communicate with the main thread

Example: move tx to a new thread and send ‘hi’

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

fn main() {
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
// Block the main thread's execution until a value is received from the channel
let received = rx.recv().unwrap();
println!("Got: {}", received);
}

Here, thread::spawn is used again to create a new thread, and move is used to transfer tx into the closure so that the new thread owns tx. The new thread needs to own the sending end of the channel in order to send messages through it.

The sending end of the channel has a send method that takes the value to be placed into the channel. The send method returns a Result<T, E> type, soif the receiving end has already been dropped, there is no destination for the value, so the send operation will return an error. In this example, calling unwrap on an error causes a panic. However, for a real program, it needs to be handled properly

The recv method of the receiving end

  • The receiving end of the channel has two useful methods: recv and try_recv.
  • Here, we use recv, which is the abbreviation for_receive_receive. This method willblock the main thread’s execution until a value is received from the channel. Once a value is sent, recv returns it in a Result<T, E>. When the sending end of the channel is closed, recv returns an error indicating that no more values will arrive.
  • try_recv does not block, instead it immediately returns a Result<T, E>: an Ok value contains available information, while an Err value indicates that there is no message at the moment. If a thread has other work to do while waiting for messages, using try_recv is useful: you can write a loop that frequently calls try_recv, processing messages when available, and otherwise doing other work for a while before checking again.

Channels and Ownership Transfer

Now let’s do an experiment to see how channels and ownership work together to avoid problems: we’ll try sending the val value through a channel in a new threadand thenuse it again. Try compiling the code in the following example and see why this is not allowed:

Example: Attempting to use the val reference after it has been sent through the channel

Filename: src/main.rs

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

fn main() {
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {}", val);
});

let received = rx.recv().unwrap();
println!("Got: {}", received);
}

Here we try to print val after sending it through the channel via tx.send. Allowing this would be a bad idea:

Once the value is sent to another thread, that thread might modify or discard it before we use it again. Possible modifications to the value by other threads could cause errors or unexpected results due to inconsistent or nonexistent data

However, when trying to compile the code in Example 16-9, Rust gives an error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:31
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {}", val);
| ^^^ value borrowed here after move

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

Our concurrency mistake causes a compile-time error. The send function takes ownership of its parameter and moves the value so that it belongs to the receiver. This prevents accidentally using the value again after sending; the ownership system checks that everything is in order.

Sending multiple values and observing the receiver’s waiting

Example: Sending multiple messages and pausing for a while after each send

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
24
25
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];

for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

for received in rx {
println!("Got: {}", received);
}
}

This time, in the new thread, we have a vector of strings that we want to send to the main thread. We iterate over them, sending each string individually and pausing for one second by calling the thread::sleep function with a Duration value.

In the main thread, we no longer explicitly call the recv function: instead, we treat rx as an iterator. For each received value, we print it out. When the channel is closed, the iterator will also end.

When running the code in Example 16-10, you will see the following output, with each line pausing for one second:

1
2
3
4
Got: hi
Got: from
Got: the
Got: thread

Since there is no pause or wait code in the for loop of the main thread, it can be said that the main thread is waiting to receive values from the newly created thread.

Creating Multiple Producers by Cloning the Sender

Example: Sending Multiple Messages from Multiple Producers

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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// --snip--

let (tx, rx) = mpsc::channel();

let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];

for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];

for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

for received in rx {
println!("Got: {}", received);
}

// --snip--

This time, before creating the new thread, we called the clone method on the sending end of the channel. This gives us asender handle that can be passed to the first newly created thread. We will pass the original channel sender to the second newly created thread. This results in two threads, each sending different messages to the receiving end of the channel.

If you run this code, youmightsee output like this:

1
2
3
4
5
6
7
8
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you

Although you might see these values appear in a different order; this depends on your system. This is what makes concurrency both interesting and difficult. If you experiment with thread::sleep, providing different values in different threads, you will find their execution becomes even more non-deterministic, producing different output each time.

Shared-State Concurrency

  • In a way, channels in any programming language are similar to single ownership, because once a value is sent into a channel, you can no longer use that value.
  • Shared memory is similar to multiple ownership: multiple threads can access the same memory location simultaneously

A mutex allows only one thread to access data at a time

mutexmutex) is_mutual exclusion_an abbreviation, meaning that at any given time, it only allows one thread to access certain data. To access the data in the mutex, a thread must first acquire the mutex’slocklock) to indicate its desire to access the data. A lock is a data structure that is part of the mutex, which records who has exclusive access to the data. Therefore, we describe the mutex as protecting its data through a locking systemprotectingguarding) its data.

Mutexes are notoriously difficult to use because you have to remember:

  1. Attempt to acquire the lock before using the data.
  2. After processing the data protected by the mutex, you must unlock the data so that other threads can acquire the lock.

In Rust, thanks to the type system and ownership, we won’t make mistakes with locking and unlocking.

Mutex API

As an example of how to use a mutex, let’s start by using a mutex in a single-threaded context, as shown in the example:

Example: Exploring the Mutex API in a single-threaded context for simplicityAPI

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
use std::sync::Mutex;

fn main() {
let m = Mutex::new(5);

{
let mut num = m.lock().unwrap();
*num = 6;
}

println!("m = {:?}", m);
}

Like many types, we use the associated function new to create a Mutex. Use the lock method to acquire the lock and access the data in the mutex. This call will block the current thread until we have the lock.

Once the lock is acquired, the return value (here, num)can be treated as a mutable reference to its internal data. The type system ensures that we acquire the lock before using the value in m: Mutexis not an i32, so wemustacquire the lock to use the i32 value. We won’t forget to do this because otherwise the type system won’t allow access to the inner i32 value.

Mutexis a smart pointer. More precisely,the lock call returns a smart pointer called MutexGuardThis smart pointer implements Deref to point to its internal data; it also provides a Drop implementation that automatically releases the lock when the MutexGuard goes out of scope, which happens at the end of the inner scope in Listing 16-12. As a result, we don’t risk forgetting to release the lock and blocking the mutex from being used by other threads, becausethe lock release happens automatically

Sharing a Mutex Between Threads

Now let’s try using a Mutexto share a value across multiple threads. We’ll spawn ten threads, and each thread will increment the same counter value by one, so the counter goes from 0 to 10. The example in the listing will have a compilation error, and we’ll use those errors to learn how to use a Mutex, and how Rust helps us use it correctly.

Listing: A program that spawns 10 threads, each of which increments a counter via a Mutexto increment the counter’s value

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
use std::sync::Mutex;
use std::thread;

fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];

for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();

*num += 1;
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());
}

Here we create a counter variable to hold a Mutex, similar to Listing 16-12. Then we iterate over a range to create 10 threads. We use thread::spawn and give all threads the same closure: each one will call the lock method to acquire the lock on the Mutex, then add one to the value in the mutex. When a thread finishes executing, num goes out of scope and releases the lock so another thread can acquire it.

In the main thread, we collect all the join handles and call their join methods to ensure all threads finish. At that point, the main thread will acquire the lock and print the result of the program.

Compilation fails:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: use of moved value: `counter`
--> src/main.rs:9:36
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
9 | let handle = thread::spawn(move || {
| ^^^^^^^ value moved into closure here, in previous iteration of loop
10 | let mut num = counter.lock().unwrap();
| ------- use occurs due to use in closure

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

The error message indicates that the counter value was moved in the previous iteration of the loop. So Rust tells uscannot movecounterownership of the lock into multiple threads

Multiple threads and multiple ownership

By using the smart pointer Rcto create reference-counted values, multiple owners can be supported.

Example: Attempting to use Rcto allow multiple threads to own a Mutex

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
24
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();

*num += 1;
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());
}

Compile again and… a different error appears!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:22
|
11 | let handle = thread::spawn(move || {
| ______________________^^^^^^^^^^^^^_-
| | |
| | `Rc<Mutex<i32>>` cannot be sent between threads safely
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________- within this `[closure@src/main.rs:11:36: 15:10]`
|
= help: within `[closure@src/main.rs:11:36: 15:10]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
= note: required because it appears within the type `[closure@src/main.rs:11:36: 15:10]`
note: required by a bound in `spawn`

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

The first error line indicates Rc<Mutex> cannot be sent between threads safely. The compiler also tells us the reasonthe trait Sendis not implemented forRc<Mutex>. The next section will discuss Send: one of the traits that ensures the types used are suitable for concurrent environments.

Unfortunately,Rccannot be safely shared between threads. When Rcmanages reference counting, it must increment the count on every clone call and decrement it when each clone is dropped.Rcdoes not use any concurrency primitives to ensure that count-changing operations are not interrupted by other threads. Errors in counting can lead to subtle bugs, such as memory leaks or dropping a value before it is no longer in use. What we need is something exactly like Rc, and changes the reference count in a thread-safe manner.

Atomic Reference Counting Arc

Arcis exactlya type similar to Rcthat can be safely used in concurrent environments. The letter ‘a’ stands foratomicityatomic), so this is anatomic reference countingatomically reference counted) type.

Why aren’t all primitive types atomic? Why don’t all types in the standard library use Arc by defaultimplementation?

The reason is thatthread safety comes with a performance penalty, and we want to pay that cost only when necessary. If you’re only operating on values within a single thread, the guarantees provided by atomicity are unnecessary, and the code can run faster.

Example: Using ArcWrapping a MutexEnables sharing ownership across multiple threads

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
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();

*num += 1;
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());
}

This prints:

1
Result: 10

Similarity between RefCell/Rc and Mutex/Arc

  • Because counter is immutable, but we can get a mutable reference to its inner value; this means Mutexprovides interior mutability, like the Cell family of types. Just as we use RefCellto mutate the contents of an Rc, similarly we can use Mutexto mutate the contents of an Arc.
  • Rust cannot prevent all logical errors with Mutex. Recall that using Rccarries the risk of creating reference cycles, where two Rcvalues reference each other, causing memory leaks. Similarly, Mutexalso has the risk of causingdeadlockdeadlock. This occurs when an operation needs to lock two resources and two threads each hold one lock, causing them to wait for each other indefinitely.

Extensible Concurrency with the Sync and Send Traits

One interesting aspect of Rust’s concurrency model is that the language itself knows verylittleabout concurrency. Almost everything we’ve discussed so far is part of the standard library, not the language itself. Since the language doesn’t need to provide concurrency-related infrastructure, concurrency solutions are not limited by the standard library or the language: we can write our own or use concurrency features written by others.

However, there are two concurrency concepts that areembedded in the language:the Sync and Send traits from std::marker.

Allowing Ownership Transfer Between Threads with Send

  • The Send marker trait indicates that ownership of values of types implementing Send can be transferred between threads.
  • Almost all Rust types are Send,
  • but there are some exceptions, includingRc: This cannot be Send,

because if you clone an Rcand try to transfer ownership of the clone to another thread, both threads might update the reference count simultaneously. For this reason, Rcis implemented for single-threaded scenarios, where there is no need to pay the performance cost of having a thread-safe reference count.

  • Rust’s type system and trait bounds ensure that you can never accidentally send an unsafe Rcacross threads. When you try to do so, you get the error: the traitSendis not implemented forRc<Mutex<i32>>. However, when usingArc, which is marked asSend,there is no problem.
  • Any type composed entirely of Send types is automatically marked as Send.Almost all primitive types are Send, except for raw pointers.

Sync allows multi-threaded access

  • The Sync marker trait indicates that a type implementing Sync cansafely have references to its value across multiple threads
  • In other words,for any type T, T is Sync if &T (an immutable reference to T) is Send., which means its reference can be safely sent to another thread.
  • Similar to Send, primitive types are Sync, and types composed entirely of Sync types are also Sync.
  • The smart pointer Rcis also not Sync, for the same reason it is not Send. RefCelland Cellfamily of types are not Sync. RefCell's runtime borrow checking is also not thread-safe.
  • Mutexis Sync, as discussed in the section “Sharing a Mutex Between Threads”, it can be used to share access across multiple threads.

Manually implementing Send and Sync is unsafe

  • It is usually not necessary to manually implement the Send and Sync traits, because types composed of Send and Sync types are automatically Send and Sync.
  • Because they are marker traits, they don’t even require implementing any methods. They are only used to enforce concurrency-related invariants.
  • Manually implementing these marker traits involves writing unsafe Rust code,

The important thing now is to be careful when creating new concurrent types composed of parts that are not Send and Sync, to ensure their safety guarantees are maintained.“The Rustonomicon”There is more information about these guarantees and how to maintain them in the documentation.

Object-Oriented Features of Rust

Characteristics of Object-Oriented Languages

  • Objects Contain Data and Behavior

Under this definition, Rust is object-oriented: structs and enums contain data, and impl blocks provide methods on structs and enums. Although structs and enums with methods are notcalledobjects, they provide the same functionality as objects,

  • Encapsulation Hides Implementation Details

Encapsulationencapsulation) idea: the implementation details of an object are not accessible to code using that object. Therefore, the only way to interact with an object is through its public API; code using the object cannot reach into the object’s internals and directly change data or behavior. Encapsulation enables changing and refactoring an object’s internals without needing to change the code that uses the object.

In Rust, the pub keyword can be used to decide that modules, types, functions, and methods are public, while by default everything else is private.

Example:

For example, we can define a struct AveragedCollection that holds a vector of i32 values. The struct can also have a field that stores the average of all values in the vector. This way, anyone who wants to know the average of the vector in the struct can obtain it at any time without having to compute it themselves. In other words, AveragedCollection caches the average result for us. The following example shows the definition of the AveragedCollection struct:

Example: The AveragedCollection struct maintains a list of integers and the average of all elements in the collection.

Filename: src/lib.rs

1
2
3
4
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}

Note,**The struct itself is marked as pub so that other code can use this struct, but the fields within the struct remain private.**This is very important because we want to ensure that when a variable is added to or removed from the list, the average is also updated simultaneously. This can be achieved by implementing add, remove, and average methods on the struct, as shown in the example:

Example: Implementing the public methods add, remove, and average on the AveragedCollection struct

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
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}

pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}

pub fn average(&self) -> f64 {
self.average
}

fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}

The public methods add, remove, and average are the only ways to modify an instance of AveragedCollection. When using the add method to add an element to the list or the remove method to delete one, the implementations of these methods also call the private update_average method to update the average field.

list and average are private, so there is no other way for external code to directly add or remove elements from the list; otherwise, changes to the list could cause the average field to become out of sync. The average method returns the value of the average field, allowing external code to only read the average but not modify it.

Because we have encapsulated the implementation details of AveragedCollection, we can easily change aspects such as the data structure in the future. For example, we could use a HashSetinstead of a Vecas the type for the list field. As long as the signatures of the public functions add, remove, and average remain unchanged, code using AveragedCollection does not need to change. Conversely, if list were made public, this would not necessarily be the case: HashSetand Vecuse different methods to add or remove items, so if external code were to directly modify the list, it might have to make changes.

If encapsulation is considered a necessary aspect for a language to be object-oriented, then Rust meets this requirement. Using pub or not in different parts of the code can encapsulate implementation details.

  • Inheritance, as a Type System and Code Sharing

Inheritance(_Inheritance_is a mechanism provided by many programming languages, where an object can be defined to inherit from another object’s definition, allowing it to obtain the parent object’s data and behavior without redefining them.

**If a language must have inheritance to be called object-oriented, then Rust is not object-oriented.**It is not possible to define a struct that inherits the fields and methods of a parent struct. However, if you are accustomed to using inheritance in your programming toolbox, Rust offers other solutions depending on why you originally considered inheritance.

There are two main reasons for choosing inheritance.

  1. The first is for code reuse: once a specific behavior is implemented for one type, inheritance allows that implementation to be reused for a different type. In contrast, Rust code can share implementations using default trait methods,
  2. The second reason for using inheritance relates to the type system: it allows a subtype to be used where a parent type is expected. This is also calledpolymorphismpolymorphism), which means that if multiple objects share certain properties, they can be used interchangeably.

In recent years, inheritance has fallen out of favor as a language design solution in many languages because it often carries the risk of sharing more code than necessary.**Subclasses should not always share all the characteristics of their parent class, but inheritance always does so. This can make program design less flexible and introduce meaningless method calls on subclasses,**or the possibility of errors because a method is not actually applicable to the subclass. Some languages also only allow a subclass to inherit from one parent class, further limiting design flexibility.

Trait objects for values of different types

The limitation of a vector being able to store only elements of the same type. In our earlier example, we provided an alternative by defining a SpreadsheetCell enum to store integer, floating-point, and text members. This means you can store different types of data in each cell while still having a vector representing a row of cells. This is perfectly feasible when the set of types you expect to use interchangeably is fixed at compile time.

However, sometimes we want library users to be able to extend the set of valid types in specific situations.

To demonstrate how to achieve this, here we will create an example of a graphical user interface (GUI) tool that draws items to the screen by iterating over a list and calling the draw method on each item — a common technique for GUI tools. We will create a library crate called gui that contains the structure of a GUI library. This GUI library includes some types for developers to use, such as Button or TextField. On top of this, users of gui want to create custom types that can be drawn on the screen: for example, one programmer might add Image, another might add SelectBox.

This example will not implement a full-featured GUI library, but it will show how the various parts fit together. When writing the library, we cannot know and define all the types that other programmers might want to create. What we do know is that gui needs to keep track of a collection of values of different types and needs to be able to call the draw method on each of them. We don’t need to know exactly what happens when draw is called, as long as the value has that method available for us to call.

In languages with inheritance, you can define a class named Component that has a draw method on it. Other classes, such as Button, Image, and SelectBox, would derive from Component and thus inherit the draw method. Each of them can override the draw method to define their own behavior, but the framework treats all these types as instances of Component and calls draw on them.

However, Rust does not have inheritance, so we need to find another way.

Defining a Trait for Common Behavior

To implement the behavior expected by the GUI, let’s define a Draw trait that contains a method named draw. Then we can define a vector that holdstrait objects. A trait object points to an instance of a type that implements our specified trait, along with a table used to look up the trait methods of that type at runtime. We create a trait object by specifying some kind of pointer, such as a & reference or a Boxsmart pointer, along with the dyn keyword, and specifying the relevant trait (the [“Dynamically Sized Types and the Sized Trait”] section explains why trait objects must use pointers). We can use trait objects in place of generics or concrete types. Anywhere a trait object is used, Rust’s type system will ensure at compile time that any value used in that context implements the trait of the trait object. This way, we don’t need to know all possible types at compile time.

Rust deliberately does not call structs and enums “objects” to distinguish them from objects in other languages. In a struct or enum, the data in the struct fields and the behavior in impl blocks are separate, unlike other languages where data and behavior are combined into a concept called an object.

Trait objects combine data and behavior, so in that sensethey aremore similar to objects in other languages. However, trait objects differ from traditional objects because you cannot add data to a trait object. Trait objects are not as general as objects in other languages: their specific purpose is to allow abstraction over common behavior.

The following example shows how to define a trait named Draw with a draw method:

Filename: src/lib.rs

1
2
3
pub trait Draw {
fn draw(&self);
}

Next, we define a struct Screen that holds a vector named components. The type of this vector is Box, which is a trait object: it stands in for any type inside a Box that implements the Draw trait.

Filename: src/lib.rs

1
2
3
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}

Example: A definition of a Screen struct with a field components that contains a vector of trait objects implementing the Draw trait

On the Screen struct, we will define a run method that calls the draw method on each component in its components, as shown in the example:

Filename: src/lib.rs

1
2
3
4
5
6
7
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}

This is different from defining a struct that uses generic type parameters with trait bounds.A generic type parameter can only be substituted with one concrete type at a time, whereastrait objects allow for multiple concrete types at runtime. For example, the Screen struct can be defined to use generics and trait bounds, as shown in the example:

Example: An alternative implementation of the Screen struct, where the run method uses generics and trait bounds

Filename: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}

impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}

Thisrestrictsthe elements stored in the Vec in a Screen instance to be of the same type, meaning it must have a list of components that are all Button types or all TextField types. If you only needa homogeneous (same type) collection, it tends to use generics and trait bounds, because their definitions are monomorphized with concrete types at compile time.

1
2
3
4
5
6
7
8
9
10
11
12
13
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}

On the other hand, by using trait objects, a Screen instance can hold a Vec of smart pointers that can contain both Button and TextField.

Implementing the Trait

Now let’s add some types that implement the Draw trait. We’ll provide the Button type. Actually implementing a GUI library is beyond the scope, so the draw method body won’t have any meaningful implementation. To imagine what this implementation looks like, a Button struct might have width, height, and label fields, as shown in the example:

Filename: src/lib.rs

Example: A Button struct implementing the Draw trait

1
2
3
4
5
6
7
8
9
10
11
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}

impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}

The width, height, and label fields on Button will differ from other components, such as TextField which might have width, height, label, and placeholder fields. Each type we want to draw on the screen will use different code to implement the draw method of the Draw trait to define how to draw that specific type, like the Button type here (which doesn’t include any actual GUI code, as that’s beyond the scope of this chapter). Besides implementing the Draw trait, Button might also have another impl block containing methods for how button clicks respond. Such methods are not applicable to types like TextField.

If some library user decides to implement a struct SelectBox with width, height, and options fields, and also implements the Draw trait for it, as shown in the example:

Example: Implementing the Draw trait on the SelectBox struct in another crate using gui

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
use gui::Draw;

struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}

impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}

The library user can now create a Screen instance in their main function. At this point, they can add components by putting SelectBox and Button into Boxto turn them into trait objects. Then they can call Screen’s run method, which will call the draw method of each component. The following example shows this implementation:

Example: Using trait objects to store values of different types that implement the same trait

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
24
use gui::{Button, Screen};

fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};

screen.run();
}

When writing the library, we don’t know who might add the SelectBox type at some point, but the Screen implementation can operate on and draw this new type because SelectBox implements the Draw trait, meaning it implements the draw method.

This concept — caring only about the information a value reflects rather than its specific type — is similar to what is calledduck typingduck typing) in dynamically typed languages: if it walks like a duck and quacks like a duck, then it must be a duck! In the example’s implementation of run on Screen, run does not need to know the specific types of each component.It does not check whether a component is an instance ofButtonorSelectBoxinstance ofBy specifyingBoxascomponentsthe type of values in the vector, we define Screen to require values on which the draw method can be called.

The advantage of using trait objects and Rust’s type system to perform duck-typing-like operations is that there is no need to check at runtime whether a value implements a particular method or worry about errors if a value doesn’t implement a method when called. Rust will not compile code if the value does not implement the trait required by the trait object.

For example, the example shows what happens when creating a Screen that uses String as its component:

Example: Attempting to use a type that does not implement the trait for the trait object

Filename: src/main.rs

1
2
3
4
5
6
7
8
9
use gui::Screen;

fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};

screen.run();
}

We get this error because String does not implement the rust_gui::Draw trait:

1
2
3
4
5
6
7
8
9
10
11
12
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= note: required for the cast to the object type `dyn Draw`

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

This tells us that either we are passing a type to Screen that we didn’t intend to and should provide a different type, or we should implement Draw on String so that Screen can call draw on it.

Trait objects perform dynamic dispatch

  • The monomorphization process performed by the compiler when using trait bounds on generics: the compiler generates non-generic implementations of functions and methods for each concrete type that replaces the generic type parameter. The code produced by monomorphization is executed withstatic dispatchstatic dispatch)。
  • Static dispatch occurs when the compiler knows at compile time which method is being called.
  • Dynamic dispatch (_dynamic dispatch_The compiler cannot know at compile time which method is being called.
  • In the case of dynamic dispatch, the code generated by the compiler determines which method is called only at runtime.

When using trait objects, Rust must use dynamic dispatch. The compiler cannot know all the types that might be used with the trait object code, so it also doesn’t know which method implementation of which type to call. For this reason, Rust uses pointers in the trait object at runtime to know which method needs to be called.Dynamic dispatch also prevents the compiler from selectively inlining method code, which correspondinglydisables some optimizations. Although we do gain additional flexibility in writing examples and the code that supports them, there are still trade-offs to consider.

Trait objects require type safety

Only object-safe traits can be implemented as trait objects (dyn).

There are some complex rules for making a trait object-safe, but in practice, only two rules are relevant.

A trait is object-safe if all methods defined in it satisfy the following rules:

  • The return type is not Self.
  • There are no generic type parameters.

When we use trait objects, we are actually doingdynamic dispatch. Rust finds the corresponding method implementation at runtime through avtablemethod table. To achieve this, the methods of a trait must be able tofully determine the method signature(including return type information, etc.) at compile time, and cannot rely on unknown type information.

However, some trait methods depend onSelfor generics, making it impossible for the compiler to guarantee that dynamic dispatch is safe.

An example of a non-object-safe trait is the standard library’sClonetrait. The declaration of the clone method in the Clone trait is as follows:

1
2
3
pub trait Clone {
fn clone(&self) -> Self;
}

The String type implements the Clone trait. When we call the clone method on an instance of String, we get an instance of the String type. Similarly, if we call the clone method on a Vecinstance, we get a Vectype instance. The signature of the clone method needs to know which type is the Self type, because Self is its return type.

When we try to compile code that violates the object safety rules of trait objects, we receive a compiler hint. For example, if we want to implement a Screen struct that holds a type implementing the Clone trait instead of the Draw trait, as shown below

1
2
3
pub struct Screen {
pub components: Vec<Box<dyn Clone>>,
}

We will receive the following error:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cargo build
Compiling gui v0.1.0 (file:///projects/gui)
error[E0038]: the trait `Clone` cannot be made into an object
--> src/lib.rs:2:29
|
2 | pub components: Vec<Box<dyn Clone>>,
| ^^^^^^^^^ `Clone` cannot be made into an object
|
= note: the trait cannot be made into an object because it requires `Self: Sized`
= note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>

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

This error means we cannot use this trait for 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
// Make the code work using at least two methods
// Do not add or remove any lines of code
trait MyTrait {
fn f(&self) -> Self;
}

impl MyTrait for u32 {
fn f(&self) -> Self { 42 }
}

impl MyTrait for String {
fn f(&self) -> Self { self.clone() }
}

fn my_function(x: Box<dyn MyTrait>) {
x.f()
}

fn main() {
my_function(Box::new(13_u32));
my_function(Box::new(String::from("abc")));

println!("Success!")
}

First modification method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
trait MyTrait {
fn f(&self) -> Self;
}

impl MyTrait for u32 {
fn f(&self) -> u32 { 42 }
}

impl MyTrait for String {
fn f(&self) -> String { self.clone() }
}

fn my_function(x: impl MyTrait) -> impl MyTrait {
x.f()
}

fn main() {
my_function(13_u32);
my_function(String::from("abc"));
}

Second modification method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
trait MyTrait {
fn f(&self) -> Box<dyn MyTrait>;
}

impl MyTrait for u32 {
fn f(&self) -> Box<dyn MyTrait> { Box::new(42) }
}

impl MyTrait for String {
fn f(&self) -> Box<dyn MyTrait> { Box::new(self.clone()) }
}

fn my_function(x: Box<dyn MyTrait>) -> Box<dyn MyTrait> {
x.f()
}

fn main() {
my_function(Box::new(13_u32));
my_function(Box::new(String::from("abc")));
}

Implementation of an Object-Oriented Design Pattern

  • State Patternstate pattern) is an object-oriented design pattern. The key to this pattern is that a value has some internal state, represented as a series ofstate objects, and the value’s behavior changes with its internal state
  • State object sharing functionality: In Rust, structs and traits are used instead of objects and inheritance. Each state object is responsible for its own behavior and when it should transition to another state. The value holding a state object is unaware of the behaviors of different states or when state transitions occur.
  • Using the state pattern means that when the business requirements of the program change, there is no need to modify the code that holds the state or uses the value. We only need to update the code within a state object to change its rules, or add more state 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
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
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, _post: &'a Post) -> &'a str {
""
}
}

pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}

impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}

pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}

pub fn content(&self) -> &str {
// Delegate to the state object to decide whether to return content
self.state.as_ref().unwrap().content(self)
}

pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review());
}
}

pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve());
}
}
}

// ===== State Implementation =====

struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self // ignore
}
}

struct PendingReview {}
impl State for PendingReview {
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
fn request_review(self: Box<Self>) -> Box<dyn State> {
self // ignore
}
}

struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self // ignore
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self // ignore
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}

// ===== Testing =====

fn main() {
let mut post = Post::new();

post.add_text("Rust makes systems programming safe!");

assert_eq!("", post.content()); // Still in draft, cannot view content

post.request_review();
assert_eq!("", post.content()); // Pending review, still cannot view content

post.approve();
assert_eq!("Rust makes systems programming safe!", post.content()); // Published!

println!("Published content: {}", post.content());
}

Patterns and Pattern Matching

Patterns:

  • Patterns are a special syntax in Rust for matching against the structure of complex and simple types
  • Using patterns in conjunction with match expressions and other constructs allows for better control of the program’s control flow
  • Patterns consist of (some combination of) the following elements:
    • Literals
    • Destructured arrays, enums, structs, and tuples
    • variable
    • wildcard
    • placeholder

arm of match

  • match VALUE

  • requirement of expressions: exhaustive (covering all possibilities)

  • a special pattern: _ (underscore): it matches nothing, does not bind to a variable, often used for the last arm of match, or to ignore certain values

conditional if let expression

  • the if let expression is mainly a concise way to equivalently replace a match with only one arm

  • if let can optionally have an else, including:

    • else if
    • else if let
  • but, if let does not check for exhaustiveness:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8,_> = "34".parse();

if let Some(color) = favorite_color{
print!("Using your favorite color,{},as the background",color);
} else if is_tuesday{
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}

while let conditional loop

  • as long as the pattern continues to match the condition, it allows the while loop to keep running
1
2
3
4
5
6
7
8
9
10
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
println!("{}",top);
}
}

pattern matching in for loops

  • the for loop is the most common loop in Rust
  • in a for loop, the pattern is the value immediately following the for keyword
1
2
3
4
5
6
fn main() {
let v = vec!['a','b','c'];
for (index,value) in v.iter().enumerate(){
println!("{} is at index {}",value,index);
}
}

iter().enumerate() returns a tuple

Pattern matching in let statements

  • let statements are also patterns
  • let PARTTERN = EXPRESSION
1
2
let a = 5;
let (x,y,z) = (1,2,3);

Function parameters

  • Function parameters can also be patterns
1
2
3
4
5
6
7
fn print_coordinates(&(x,y): &(i32,i32)) {
println!("Current location: ({},{})",x,y);
}
fn main() {
let point = (3,5);
print_coordinates(&point);
}

Refutability: whether a pattern can fail to match

  • Two forms of patterns: refutable and irrefutable
  • Patterns that match any possible value: irrefutable, e.g., let x = 4;
  • Patterns that may fail to match for some possible values: refutable, e.g., if let Some(x) = a_value
  • Function parameters, let statements, and for loops only accept irrefutable patterns
  • if let and while let accept both refutable and irrefutable patterns
1
2
3
4
fn main() {
let a: Option<i32> = Some(5);
let Some(x) = a;
}

Output

1
2
3
4
5
6
7
8
error[E0005]: refutable pattern in local binding: `None` not covered
--> src\main.rs:3:9
|
3 | let Some(x) = a;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html

Cannot match because the pattern does not cover the None case

Modification

1
2
3
4
5
6
fn main() {
let a: Option<i32> = Some(5);
if let Some(x) = a{

};
}

In match, all arms except the last are refutable; the last arm is irrefutable because it must match all remaining cases

Match literal values

1
2
3
4
5
6
7
8
9
fn main(){
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}

Match named variables

  • Named variables are irrefutable patterns that can match any value
1
2
3
4
5
6
7
8
9
10
fn main(){
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched,y={:?}",y),
_ => println!("Default Case,x={:?}",x),
}
println!("at the end: x={:?},y={:?}",x,y);
}

In the second arm of this matchy is a new variable, existing in the scope of that arm

Match a mutable reference

When using the pattern &mut V to match a mutable reference, you need to be extra careful because the matched Vis a value, andnot a mutable reference

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

match r {
// The type of value is &mut String
value => value.push_str(" world!")
}
}

Multiple patterns

  • In a match expression, using the | syntax (meaning OR) allows matching multiple patterns
1
2
3
4
5
6
7
8
fn main(){
let x= 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}

Use …= to match a range of values

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main(){
let x= 5;
match x{
1..=5 => println!("one through five"),
_ => println!("something else"),
}
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}

Both numbers and characters are acceptable

Destructuring to decompose values

  • Patterns can be used to destructure structs, enums, and tuples to reference different parts of these type values

Destructuring assignment

1
2
3
4
5
6
fn main() {
let (x, y);
(x,..) = (3, 4);
[.., y] = [1, 2];
assert_eq!([x,y],[3,2]);
}

Destructuring tuples

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

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

Destructuring structs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[derive(Debug)]
struct Point{
x: i32,
y: i32,
}
fn main(){
let p = Point{x:0,y:7};
let Point {x:a,y:b}=p;
assert_eq!(0,a);
assert_eq!(7,b);

//Shorthand form
let Point{x,y} = p;
assert_eq!(0,x);
assert_eq!(7,y);

match p {
Point {x,y:0}=> println!("On the x axis at {}",x),//Require y to be 0
Point {x:0,y} => println!("On the y axis at {}",y),//Require x to be 0
Point {x,y} => println!("On neither axis:({},{})",x,y),
}
}

Destructuring enums

Note: The following code will trigger copy or move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Message{
Quit,
Move{x:i32,y:i32},
Write(String),
ChangeColor(i32,i32,i32),
}
fn main(){
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure");
}
Message::Move { x, y }=>{
println!("Move in the x direction {} and in the y direction {}",x,y);
}
Message::Write(text)=>{
println!("Text message:{}",text);
}
Message::ChangeColor(r, g, b)=>{
println!("rgb is ({},{},{})",r,g,b);
}
}
}

Run the following code:

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
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::Write(String::from("Hello World\n"));
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure");
}
Message::Move { x, y } => {
println!("Move in the x direction {} and in the y direction {}", x, y);
}
Message::Write(text) => {
println!("Text message:{}", text);
}
Message::ChangeColor(r, g, b) => {
println!("rgb is ({},{},{})", r, g, b);
}
}
println!("{:?}", msg);
}

Error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
error[E0382]: borrow of partially moved value: `msg`
--> src/main.rs:24:22
|
17 | Message::Write(text) => {
| ---- value partially moved here
...
24 | println!("{:?}", msg);
| ^^^ value borrowed here after partial move
|
= note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
= 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)
help: borrow this binding in the pattern to avoid moving the value
|
17 | Message::Write(ref text) => {
| +++

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

Can match references

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match &msg {
Message::Quit => {
println!("The Quit variant has no data to destructure");
}
Message::Move { x, y } => {
println!("Move in the x direction {} and in the y direction {}", x, y);
}
Message::Write(text) => {
println!("Text message:{}", text);
}
Message::ChangeColor(r, g, b) => {
println!("rgb is ({},{},{})", r, g, b);
}
}
}

Destructuring nested structs and enums

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Color{
Rgb(i32,i32,i32),
Hsv(i32,i32,i32),
}
enum Message{
Quit,
Move{x:i32,y:i32},
Write(String),
ChangeColor(Color),
}
fn main(){
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b))=>{
println!("Change the color to red {},green {},and blue {}",r,g,b);
}
Message::ChangeColor(Color::Hsv(h, s, v))=>{
println!("Change the color to hue {},saturation {},and value {}",h,s,v);
}
_ => (),
}
}

Destructuring structs and tuples

1
2
3
4
5
6
7
struct Point{
x: i32,
y: i32,
}
fn main(){
let ((feet,inches),Point{x,y}) = ((3,10),Point{x:3,y:-10});
}

Ignoring Values in Patterns

  • There are several ways to ignore an entire value or parts of a value in a pattern:

_ Ignoring the Entire Value

1
2
3
4
5
6
fn foo(_:i32,y:i32){
println!("y is {}",y);
}
fn main(){
foo(3, 4);
}

Ignoring Parts of a Value with Nested _

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main(){
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value,new_setting_value) {
(Some(_),Some(_))=>{
println!("Can't overwrite an existing customized value");
}
_ =>{
setting_value = new_setting_value;
}
}
println!("setting is {:?}",setting_value);

let numbers = (3,4,8,16,32);
match numbers {
(first,_,third,_,fifth)=>{
println!("Some numbers:{},{},{}",first,third,fifth);
}
}
}

Ignoring Unused Variables by Starting Their Names with _

1
2
3
4
5
6
7
8

fn main(){
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}",s);
}

In Pattern Matching_s is a new variable, and pattern matching moves ownership of s to_s, accessing s afterward will cause an error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
error[E0382]: borrow of partially moved value: `s`
--> src/main.rs:6:22
|
3 | if let Some(_s) = s {
| -- value partially moved here
...
6 | println!("{:?}", s);
| ^ value borrowed here after partial move
|
= note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
= 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)
help: borrow this binding in the pattern to avoid moving the value
|
3 | if let Some(ref _s) = s {
| +++

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

Using _

1
2
3
4
5
6
7
fn main(){
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{:?}",s);
}

_ does not bind, so no ownership is moved

… (Ignoring the Remaining Parts of a Value)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Point{
x: i32,
y: i32,
z: i32,
}
fn main(){
let origin = Point{x:0,y:0,z:0};
match origin {
Point {x,..} => println!("x is {}",x),
}

let numbers = (2,4,8,16,32);
match numbers {
(first,..,last)=>{
println!("Some numbers: {},{}",first,last)
}
}
}

A comma must be added,

1
2
3
4
5
6
7
8
9
10
fn main() {
let numbers = (2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048);

match numbers {
(first,..,last) => {
assert_eq!(first, 2);
assert_eq!(last, 2048);
}
}
}

Using Match Guards to Provide Extra Conditions

  • A match guard is an additional if condition after the match arm pattern; the pattern must also satisfy this condition to match
  • Match guards are suitable for more complex scenarios
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
fn main(){
let num = Some(4);
match num {
Some(x) if x < 5 => println!("less than five:{}",x),
Some(x) => println!("{}",x),
None => (),
}
}
fn main(){
let x = Some(5);
let y = 10;

match x {
Some(50) => println!("Got 50"),
Some(n) if n==y => println!("Matched,n = {:?}",n),//Here, if n==y is not a pattern and does not introduce a new variable
_ =>println!("Default case,x ={:?}",x),
}
println!("at the end:x={:?},y={:?}",x,y);
}
fn main(){
let x= 4;
let y = false;
match x {
4 | 5 | 6 if y=> println!("yes"),
_ => println!("no"),
}
}

@ bindings

  • The @ symbol allows us to create a variable that holds a value while testing whether that value matches a pattern

It is essentially like an equals sign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Message{
Hello {id:i32},
}
fn main(){
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_variable @ 3..=7, }=>{
println!("Found an id in range:{}",id_variable);
}
Message::Hello { id: 10..=12 }=>{
println!("Found an id in another range");
}
Message::Hello { id }=>{
println!("Found some other id:{}",id);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Point {
x: i32,
y: i32,
}

fn main() {
// fill in the blank to let p match the second arm
let p = Point { x: 2, y: 20 }; // x can be [0, 5], y can be 10 20 or 30

match p {
Point { x, y: 0 } => println!("On the x axis at {}", x),
// second arm
Point { x: 0..=5, y: y@ (10 | 20 | 30) } => println!("On the y axis at {}", y),
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
}
}

Use cases:

The following code will cause an error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Message {
Hello { id: i32 },
}

fn main() {
let msg = Message::Hello { id: 5 };

match msg {
Message::Hello {
id: 3..=7,
} => println!("id 值的范围在 [3, 7] 之间: {}", id),//Error cannot find value `id` in this scope
Message::Hello { id: newid@10 | 11 | 12 } => {//Error variable `newid` is not bound in all patterns pattern doesn't bind `newid`
println!("id 值的范围在 [10, 12] 之间: {}", newid)
}
Message::Hello { id } => println!("Found some other id: {}", id),
}
}

Fix the error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Message {
Hello { id: i32 },
}

fn main() {
let msg = Message::Hello { id: 5 };

match msg {
Message::Hello {
id: id @3..=7,
} => println!("id 值的范围在 [3, 7] 之间: {}", id),
Message::Hello { id: newid@(10 | 11 | 12) } => {
println!("id 值的范围在 [10, 12] 之间: {}", newid)
}
Message::Hello { id } => println!("Found some other id: {}", id),
}
}

unsafe Rust

  • There is a second language hidden within, whichdoes not enforce memory safety guarantees: unsafe Rust

It is the same as normal Rust, but provides additional superpowers

  • Reasons for the existence of Unsafe Rust:
  1. Static analysis is conservative; using unsafe Rust is like telling the compiler: I know what I’m doing and I accept the associated risks
  2. Computer hardware itself is inherently unsafe, and Rust needs to be capable of low-level systems programming

unsafe superpowers

  • Use the unsafe keyword to switch to unsafe Rust, opening a block that contains unsafe code
  • Four actions performed in unsafe Rust (unsafe superpowers):
  1. Dereference a raw pointer
  2. Call an unsafe function or method
  3. Access or modify a mutable static variable
  4. Implement an unsafe trait
  • Note:

unsafe doesnot turn off the borrow checker or disable other safety checks

Any memory safety-related errors must stay within the unsafe block

Isolate unsafe code as much as possible, preferably encapsulating it in a safe abstraction that provides a safe API

Dereference a raw pointer

  • Raw pointers

*Mutable: mut T

Immutable: const T, meaningAfter dereferencing a pointer, you cannot directly assign to it*

Note: The * here is not a dereference operator; it is part of the type name

  • Unlike references, raw pointers:
  1. Allow ignoring borrowing rules by having both mutable and immutable pointers or multiple mutable pointers to the same location
  2. Cannot guarantee pointing to valid memory
  3. Are allowed to be null
  4. Do not implement any automatic cleanup
  • Forgo guaranteed safety in exchange for better performance or the ability to interface with other languages or hardware
1
2
3
4
5
6
7
fn main(){
let mut num=5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
let address = 0x012345usize;
let r = address as *const i32;
}

Raw pointers can be created in safe code blocks, but cannot be dereferenced

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main(){
let mut num=5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe{
println!("r1:{}",*r1);
println!("r2:{}",*r2);
}

let address = 0x012345usize;
let r = address as *const i32;
unsafe{
println!("r:{}",*r);
}
}

Why use raw pointers?

  • Interfacing with C language
  • Building safe abstractions that the borrow checker cannot understand

Calling unsafe functions or methods

  • Unsafe functions or methods: prefixed with the unsafe keyword in their definition
    • Before calling, some conditions must be manually satisfied (mainly by reading the documentation), because Rust cannot verify these conditions.
    • The call must be made within an unsafe block.
1
2
3
4
5
6
unsafe fn dangerous(){}
fn main(){
unsafe{
dangerous();
}
}

Creating a safe abstraction for unsafe code.

  • A function containing unsafe code does not mean the entire function needs to be marked as unsafe.
  • Wrapping unsafe code in a safe function is a common abstraction.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::vec;

fn split_at_mut(slice:&mut[i32],mid:usize)->(&mut [i32],&mut[i32]){
let len = slice.len();
assert!(mid<=len);
(&mut slice[..mid],&mut slice[mid..])
}
fn main(){
let mut v= vec![1,2,3,4,5,6];
let r = &mut v[..];
let (a,b) = r.split_at_mut(3);
assert_eq!(a,&mut [1,2,3]);
assert_eq!(b,&mut [4,5,6]);
}

Error reporting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
--> src\main.rs:6:29
|
3 | fn split_at_mut(slice:&mut[i32],mid:usize)->(&mut [i32],&mut[i32]){
| - let's call the lifetime of this reference `'1`
...
6 | (&mut slice[..mid],&mut slice[mid..])
| ------------------------^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*slice` is borrowed for `'1`

For more information about this error, try `rustc --explain E0499`.

Using unsafe code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::slice;
fn split_at_mut(slice: &mut [i32],mid: usize)->(&mut [i32],&mut [i32]){
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid<=len);
unsafe{
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len-mid)
)
}
}
fn main(){
let mut v= vec![1,2,3,4,5,6];
let r = &mut v[..];
let (a,b) = r.split_at_mut(3);
assert_eq!(a,&mut [1,2,3]);
assert_eq!(b,&mut [4,5,6]);
}

Using extern functions to call external code.

  • The extern keyword: simplifies the process of creating and using Foreign Function Interfaces (FFI).
  • Foreign Function Interface (FFI): It allows one programming language to define functions that can be called by other programming languages.
  • Functions declared in an extern block are always unsafe in Rust code. Because other languages do not enforce Rust’s rules and Rust cannot check them, ensuring their safety is the programmer’s responsibility.
1
2
3
4
5
6
7
8
extern "C"{//"C" specifies the Application Binary Interface (ABI) used by the external function.
fn abs(input: i32) ->i32;//The signature of the external function to be called.
}
fn main(){
unsafe{
println!("Absolute value of -3 according to C:{}",abs(-3));
}
}
  • Application Binary Interface (ABI): Defines how functions are called at the assembly level.
  • The “C” ABI is the most common ABI, and it follows the C language’s ABI.

Calling Rust Functions from Other Languages

  • You can use extern to create interfaces through which other languages can call Rust functions
  • Add theextern keyword before fn and specify the ABI
  • Also need toadd the #[no_mangle] annotation to prevent Rust from changing its name during compilation
1
2
3
4
5
6
7
8
#[no_mangle]
pub extern "C" fn call_from_c(){
println!("Just called a Rust function from C!");//After compilation and linking, it can be accessed by C language; using extern does not require unsafe.
}

fn main(){

}

Accessing or Modifying a Mutable Static Variable

  • Rust supports global variables, but the ownership mechanism may cause certain issues, such as data races
  • In Rust, global variables are called static variables
1
2
3
4
5
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
println!("name is: {}", HELLO_WORLD);
}
  • Static variables are similar to constants.
  • Static variable names typically use SCREAMING_SNAKE_CASE notation.
  • Static variables can only store references with a 'static lifetime, meaning the Rust compiler can infer their lifetime without explicit annotation.
  • AccessImmutable static variablesare safe.

Difference between static variables and constants:

  • The value in a static variablehas a fixed memory address. Using this value always accesses the same address. Constants, on the other hand, allowcopying their data
  • Static variables can be mutable.Accessing and modifying mutable static variables is unsafe.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}

fn main() {
add_to_count(3);

unsafe {
println!("COUNTER: {}", COUNTER);
}
}

Any code that reads or writes COUNTER must be within an unsafe block. This code compiles and prints COUNTER: 3 as expected because it is single-threaded.Having multiple threads access COUNTER could lead to data races

Having globally accessible mutable data makes it difficult to guarantee the absence of data races, which is why Rust considers mutable static variables unsafe. Whenever possible, prefer using smart pointers so that the compiler can detect whether data access between different threads is safe.

Implementing an unsafe trait

  • A trait is unsafe when at least one of its methods contains invariants that the compiler cannot verify.
  • You can declare a trait as unsafe by adding the unsafe keyword before the trait, and the implementation of the trait must also be marked as unsafe
1
2
3
4
5
6
7
8
9
unsafe trait Foo {
// methods go here
}

unsafe impl Foo for i32 {
// method implementations go here
}

fn main() {}

The Sync and Send marker traits are automatically implemented by the compiler for types composed entirely of Send and Sync types. If you implement a type that contains some types that are not Send or Sync, such as raw pointers, and you want to mark this type as Send or Sync, you must use unsafe. Rust cannot verify that our type guarantees safe cross-thread sending or multi-thread access, so we need to check it ourselves and indicate it through unsafe.

Accessing fields of a union

The last operation that applies only to unsafe is accessingunionfields. A union is similar to a struct, but only one declared field can be used at a time in an instance. Unions are primarily used to interact with unions in C code. Accessing fields of a union is unsafe becauseRust cannot guarantee the type of data currently stored in the union instance. You can refer to (https://doc.rust-lang.org/reference/items/unions.html) for more information about unions.

When to use unsafe code

Using unsafe to perform one of these five operations (superpowers) is fine, and doesn’t even require deep thought, but making unsafe code correct is not easy because the compiler cannot help guarantee memory safety. When there is a reason to use unsafe code, it is acceptable to do so, and using explicit unsafe annotations makes it easier to trace the source of problems when errors occur.

Advanced traits

Using associated types in trait definitions to specify placeholder types

  • An associated type is a type placeholder in a trait that can be used in the method signatures of the trait:

You can define a trait that includes certain types without needing to know what those types are before implementation

1
2
3
4
5
6
7
pub trait Iterator {
type Item;
fn next(&mut self)->Option<Self::Item>;
}
fn main(){
println!("Hello World");
}

Difference between associated types and generics

GenericsAssociated Type
Annotate the type each time a trait is implementedNo need to annotate the type
A trait can be implemented multiple times for a type (with different generic parameters)Cannot implement a trait multiple times for a single 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
26
27
28
29
30
31
32
33
34
35
36
37
38
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
pub trait Iterator2<T>{
fn next(&mut self) -> Option<T>;
}
struct Counter{

}
impl Iterator for Counter{//Can only be implemented once
type Item=u32;
fn next(&mut self) -> Option<Self::Item> {
None
}
}
// impl Iterator for Counter { // can only be implemented once, implementing for String a second time causes an error
// type Item=String;
// fn next(&mut self) -> Option<Self::Item> {
// None
// }
// }

impl Iterator2<String> for Counter {
fn next(&mut self) -> Option<String> {
None
}
}

impl Iterator2<u32> for Counter {//Can be implemented multiple times for different types
fn next(&mut self) -> Option<u32> {
None
}
}

fn main(){

}

Associated types are mainly used to improve code readability, for example the following code:

1
2
3
4
pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable {
type Address: AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash;
fn is_null(&self) -> bool;
}

Compared to AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash, using Address can greatly reduce the boilerplate code required for other types when implementing this trait.

Example: Using associated types:

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
struct Container(i32, i32);

// Re-implement the following trait using associated types
// trait Contains {
// type A;
// type B;

trait Contains<A, B> {
fn contains(&self, _: &A, _: &B) -> bool;
fn first(&self) -> i32;
fn last(&self) -> i32;
}

impl Contains<i32, i32> for Container {
fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
(&self.0 == number_1) && (&self.1 == number_2)
}
// Grab the first number.
fn first(&self) -> i32 { self.0 }

// Grab the last number.
fn last(&self) -> i32 { self.1 }
}

fn difference<A, B, C: Contains<A, B>>(container: &C) -> i32 {
container.last() - container.first()
}

fn main() {
let number_1 = 3;
let number_2 = 10;

let container = Container(number_1, number_2);

println!("Does container contain {} and {}: {}",
&number_1, &number_2,
container.contains(&number_1, &number_2));
println!("First number: {}", container.first());
println!("Last number: {}", container.last());

println!("The difference is: {}", difference(&container));
}

Implement

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
struct Container(i32, i32);

// A trait which checks if 2 items are stored inside of container.
// Also retrieves first or last value.
trait Contains {
// Define generic types here which methods will be able to utilize.
type A;
type B;

fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
fn first(&self) -> i32;
fn last(&self) -> i32;
}

impl Contains for Container {
// Specify what types `A` and `B` are. If the `input` type
// is `Container(i32, i32)`, the `output` types are determined
// as `i32` and `i32`.
type A = i32;
type B = i32;

// `&Self::A` and `&Self::B` are also valid here.
fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
(&self.0 == number_1) && (&self.1 == number_2)
}
// Grab the first number.
fn first(&self) -> i32 { self.0 }

// Grab the last number.
fn last(&self) -> i32 { self.1 }
}

fn difference<C: Contains>(container: &C) -> i32 {
container.last() - container.first()
}

fn main() {
let number_1 = 3;
let number_2 = 10;

let container = Container(number_1, number_2);

println!("Does container contain {} and {}: {}",
&number_1, &number_2,
container.contains(&number_1, &number_2));
println!("First number: {}", container.first());
println!("Last number: {}", container.last());

println!("The difference is: {}", difference(&container));
}

Default generic parameters and operator overloading

  • You can specify a default concrete type for a generic when using generic parameters.
  • Syntax: <PlaceholderType=ConcreteType>
  • This is commonly used in operator overloading.
  • Rust does not allow creating your own operators or overloading arbitrary operators.
  • Butyou can overload some corresponding operators by implementing the traits listed in std::ops.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::ops::Add;

#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}

Here, the default generic parameter self of add is used.

1
2
3
4
5
6
7
8
9
10
11
12
use std::ops::Add;
struct Millimeters(u32);
struct Meter(u32);
impl Add<Meter> for Millimeters {
type Output = Millimeters;
fn add(self, rhs: Meter) -> Self::Output {
Millimeters(self.0+(other.0*1000))
}
}
fn main(){

}

Here, the generic parameter is specified.

Main use cases for default generic parameters

  • Extending a type without breaking existing code.
  • Allowing customization in specific scenarios that most users do not need.

Fully Qualified Syntax

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
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;

impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self){
println!("*waving arms furiously*");
}
}
fn main(){
let person = Human;
person.fly();//Calling the method of the type itself.
Pilot::fly(&person);//Calling the method from the Pilot trait.
Wizard::fly(&person);//Calling the method from the Wizard trait.
}

Parameterless form

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
trait Animal {
fn baby_name()->String;
}
struct Dog;

impl Dog {
fn baby_name()->String{
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name()->String {
String::from("puppy")
}
}

fn main(){
println!("A baby dog is called a{}",Dog::baby_name());
println!("A baby dog is called a{}",Animal::baby_name());//Error
}

Here, baby_name has no parameters, so the compiler doesn’t know which Dog is calling it

  • Fully qualified syntax:::function(receiver_if_method,netx_arg,…) ;
  • Can be used anywhere you call a function or method
  • Allows omitting parts that can be inferred from other context
  • This syntax is only needed when Rust cannot distinguish which specific implementation you intend to call
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
trait Animal {
fn baby_name()->String;
}
struct Dog;

impl Dog {
fn baby_name()->String{
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name()->String {
String::from("puppy")
}
}

fn main(){
println!("A baby dog is called a{}",Dog::baby_name());
println!("A baby dog is called a{}",<Dog as Animal>::baby_name());
}

Using supertraits to require a trait to include functionality from other traits

  • Need to use functionality from other traits within a trait
    • Require that the depended-upon trait is also implemented
    • The indirectly depended-upon trait is the supertrait of the current 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
use std::fmt::{self, write};
trait OutlinePrint: fmt::Display{
fn outline_print(&self){
let output = self.to_string();
let len = output.len();
println!("{}","*".repeat(len+4));
println!("*{}*"," ".repeat(len+2));
println!("* {} *",output);
println!("*{}*"," ".repeat(len+2));
println!("{}","*".repeat(len+4));
}
}
struct Point{
x:i32,
y:i32,
}
impl OutlinePrint for Point {
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({},{})",self.x,self.y)
}
}

fn main(){

}

Using the newtype pattern to implement external traits on external types

  • Orphan rule: You can only implement a trait for a type if either the trait or the type is defined in the local crate
  • This rule can be bypassed using the newtype pattern
    • Create a new type using a tuple struct

(Example)

1
2
3
4
5
6
7
8
9
10
11
12
use std::fmt;
struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f,"[{}]",self.0.join(", "))
}
}
fn main(){
let w = Wrapper(vec![String::from("hello"),String::from("world")]);
println!("w={}",w);
}

Advanced Types

Using the Newtype Pattern for Type Safety and Abstraction

  • The newtype pattern can:
    • Statically ensure that values are not confused and indicate the units of a value
    • Provide abstraction for certain details of a type
    • Hide internal implementation details through lightweight wrapping

Creating Type Synonyms with Type Aliases

  • Rust provides type aliases: — to create another name (synonym) for an existing type — not a separate type — uses the type keyword
  • Main use: reduce code repetition
  • Similar to C’s typedef
1
2
3
4
5
6
7
8
9
fn takes_long_type(f: Box<dyn Fn()+Send+'static>){
//snip
}
fn returns_long_type()->Box<dyn Fn()+Send+'static>{
Box::new(|| println!("hi"))
}
fn main(){
let f:Box<dyn Fn()+Send+'static> = Box::new(|| println!("hi"));
}

Using Type Aliases

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Thunk = Box<dyn Fn()+Send+'static>;

fn takes_long_type(f: Thunk){
//snip
}
fn returns_long_type()->Thunk{
Box::new(|| println!("hi"))
}
fn main(){
let f:Thunk = Box::new(|| println!("hi"));
}
use std::io::Error;
use std::fmt;
pub trait Write {
fn write(&mut self,buf: &[u8])->Result<usize,Error>;
fn flush(&mut self)->Result<(),Error>;
fn write_all(&mut self,buf: &[u8])->Result<(),Error>;
fn write_fmt(&mut self,fmt: fmt::Arguments)->Result<(),Error>;
}
fn main(){

}

Using Type Aliases

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::io::Error;
use std::fmt;
// type Result<T>= Result<T, std::io::Error>; the standard library defines this
type Result<T> = std::io::Result<T>;
pub trait Write {
fn write(&mut self,buf: &[u8])->Result<usize>;
fn flush(&mut self)->Result<()>;
fn write_all(&mut self,buf: &[u8])->Result<()>;
fn write_fmt(&mut self,fmt: fmt::Arguments)->Result<()>;
}
fn main(){

}

The Never Type

  • There is a special type called !:

    • It has no values, known in jargon as the empty type.
    • We tend to call it the never type, as it serves as the return type for functions that never return.
  • Functions that never return are also called diverging functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn bar() -> !{
//return () returns the unit type, but it is impossible to create a function that returns the ! type.
}
fn main(){

}
fn main(){
let guess = "";
loop{
let guess:u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
}
}

A match expression requires all arms to return the same type, and continue returns the never type, which can be safely coerced to the type corresponding to num.

1
2
3
4
5
6
7
8
impl<T> Option<T>{
pub fn unwrap(self) -> T{
match self{
Some(val) => val,
None=>panic!("called `Option::unwrap()` on a `None` value"),
}
}
}

panic! returns the never type.

Dynamic Sizes and the Sized Trait

  • Rust needs to determine at compile time how much space to allocate for a value of a specific type.

  • The concept of Dynamically Sized Types (DST):

    • Writing code that uses values whose size can only be determined at runtime.
  • str is a dynamically sized type.(Note: not &str): the length of the string can only be determined at runtime.
    The following code does not work:

1
2
let s1:str = "Hello therel";
let s2:str = "How is it going";

Because they are all the same type and should require the same space, but here the common space is not determined at declaration.

Solution: Use the &str string slice type

Rust’s general approach to dynamically sized types

  • Carrying some extra metadata to store the size of dynamic information
    • When using a dynamically sized type, its value is always placed behind some kind of pointer

Another dynamically sized type: trait

  • Everytrait is a dynamically sized type, and can be referenced by its name
  • To use a trait as a trait object, it must be placed behind some kind of pointer
    • For example, &dyn Trait or Box (Rc) behind

Sized trait

  • To handle dynamically sized types, Rust provides a Sized trait to determine whether a type’s size is known at compile time—Types whose size can be computed at compile time automatically implement this trait
  • Rust also implicitly adds a Sized constraint to every generic function
1
2
3
4
5
6
7
8
fn generic<T>(t:T){

}
fn generic<T: Sized>(t:T){
}
fn main(){

}
  • By default, generic functions can only be used with types whose size is known at compile time; this restriction can be lifted with special syntax

?Sized trait bound

1
2
3
fn generic<T: ?Sized>(t:&T){

}
  • T may or may not be Sized
  • This syntax can only be used on Sized, not on other traits

Advanced functions and closures

Function pointers

  • Functions can be passed to other functions
  • Functions are coerced to the fn type when passed
  • The fn type isfunction pointer
1
2
3
4
5
6
7
8
9
10
fn add_one(x:i32)->i32{
x+1
}
fn do_twice(f: fn(i32)->i32,arg:i32)->i32{
f(arg) + f(arg)
}
fn main(){
let answer = do_twice(add_one, 5);
println!("The answer is:{}",answer);
}

Difference between function pointers and closures

  • fn is a type, not a trait

    • You can directly specify fn as the parameter type without declaring a generic parameter constrained by the Fn trait
  • Function pointers implement all three closure traits (Fn, FnMut, FnOnce):

    • You can always pass a function pointer as an argument to a parameter that accepts a closure
    • Therefore, it is preferable to write functions with generics that use closure traits: they can accept both closures and regular functions
  • In some scenarios, only want to accept fn but not closures

    • Interacting with external code that does not support closures: C functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main(){
let list_of_numbers = vec![1,2,3];
let list_of_strings:Vec<String> = list_of_numbers
.iter()
.map(|i| i.to_string())
.collect();

let list_of_numbers = vec![1,2,3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(ToString::to_string)
.collect();
}
fn main(){
enum Status{
Value(u32),
Stop,
}
let v = Status::Value(3);
let list_of_statuses:Vec<Status> = (0u32..20).map(Status::Value).collect();
}

Returning closures

  • Closures are expressed using traits, and a closure cannot be returned directly from a function; a concrete type that implements the trait can be returned instead
1
2
3
4
5
6
7
8
9
10
11
// fn returns_closure() -> Fn(i32) -> i32 { // return type size is not fixed
// |x| x+1
// }

fn returns_closure() -> Box<dyn Fn(i32)->i32>{
Box::new(|x| x+1)
}

fn main(){

}

Macros

Reference:

Official documentation:

The Rust Programming Language

The Little Book of Rust Macros

Asynchronous programming

Reference:

Asynchronous Programming in Rust

The Rust Programming Language Bible

Formatted Output

Positional Parameters

1
2
3
4
5
6
fn main() {
println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob");// => Alice, this is Bob. Bob, this is Alice
assert_eq!(format!("{1}{0}", 1, 2), "21");
assert_eq!(format!("{1}{}{0}{}", 1, 2), "2112");
println!("Success!")
}

Named Parameters

1
2
3
4
5
6
7
8
9
10
11
fn main() {
println!("{argument}", argument = "test"); // => "test"

assert_eq!(format!("{name}{}", 1, name = 2), "21");
assert_eq!(format!("{a} {c} {b}",a = "a", b = 'b', c = 3 ), "a 3 b");

// named argument must be placed after other arguments
println!("{abc} {0}", 2, abc = "def");

println!("Success!")
}

String Alignment

By default, strings are padded with spaces

1
2
3
4
5
6
7
8
9
10
fn main() {
// the following two are padding with 5 spaces
println!("Hello {:5}!", "x"); // => "Hello x !"
println!("Hello {:1$}!", "x", 5); // => "Hello x !"

assert_eq!(format!("Hello {1:0$}!", 5, "x"), "Hello x !");
assert_eq!(format!("Hello {:width$}!", "x", width = 5), "Hello x !");

println!("Success!")
}

Left-aligned, right-aligned, padded with specified characters

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
// left align
println!("Hello {:<5}!", "x"); // => Hello x !
// right align
assert_eq!(format!("Hello {:>5}!", "x"), "Hello x!");
// center align
assert_eq!(format!("Hello {:^5}!", "x"), "Hello x !");

// left align, pad with '&'
assert_eq!(format!("Hello {:&<5}!", "x"), "Hello x&&&&!");

println!("Success!")
}

We can also use 0 to pad numbers

1
2
3
4
5
6
7
8
9
10
fn main() {
println!("Hello {:5}!", 5); // => Hello 5!
println!("Hello {:+}!", 5); // => Hello +5!
println!("Hello {:05}!", 5); // => Hello 00005!
println!("Hello {:05}!", -5); // => Hello -0005!

assert!(format!("{number:0>width$}", number=1, width=6) == "000001");

println!("Success!")
}

Precision

Floating-point Precision

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

println!("{:.1$}", v, 4); // same as {:.4} => 3.1416

assert_eq!(format!("{:.2}", v), "3.14");
assert_eq!(format!("{:+.2}", v), "+3.14");
assert_eq!(format!("{:.0}", v), "3");

println!("Success!")
}

String Length

1
2
3
4
5
6
7
8
9
fn main() {
let s = "Hello, world!";

println!("{0:.5}", s); // => Hello

assert_eq!(format!("Hello {1:.0$}!", 3, "abcdefg"), "Hello abc!");

println!("Success!")
}

Binary, Octal, Hexadecimal

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
assert_eq!(format!("{:#b}", 27), "0b11011");
assert_eq!(format!("{:#o}", 27), "0o33");
assert_eq!(format!("{:#x}", 27), "0x1b");
assert_eq!(format!("{:#X}", 27), "0x1B");

println!("{:x}!", 27); // hex with no prefix => 1b

println!("{:#010b}", 27); // pad binary with 0, width = 10, => 0b00011011

println!("Success!")
}

Capturing Values from the Environment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn get_person() -> String {
String::from("sunface")
}

fn get_format() -> (usize, usize) {
(4, 1)
}


fn main() {
let person = get_person();
println!("Hello, {person}!");

let (width, precision) = get_format();
let scores = [("sunface", 99.12), ("jack", 60.34)];
/* Make it print:
sunface: 99.1
jack: 60.3
*/
for (name, score) in scores {
println!("{name}: {score:width$.precision$}");
}
}

Exponent, Pointer Address, Escape

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
// index
println!("{:2e}", 1000000000); // => 1e9
println!("{:2E}", 1000000000); // => 1E9

// pointer address
let v= vec![1, 2, 3];
println!("{:p}", v.as_ptr()); // => 0x600002324050

// escape
println!("Hello {{}}"); // => Hello {}
}

Other

error handling:

unreachable!()

This is a standard macro to mark paths that the program should not enter. If the program enters these paths, it will panic and return the error message “‘internal error: entered unreachable code’”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let level = 22;
let stage = match level {
1...5 => "beginner",
6...10 => "intermediate",
11...20 => "expert",
_ => unreachable!(),
};

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


// -------------- Compile time error --------------
thread 'main' panicked at 'internal error: entered unreachable code', src/main.rs:7:20

We can also set a custom error message for this.

1
2
3
4
5
6
7
8
9
10
// --- with a custom message ---
_ => unreachable!("Custom message"),
// -------------- Compile time error --------------
thread 'main' panicked at 'internal error: entered unreachable code: Custom message', src/main.rs:7:20


// --- with debug data ---
_ => unreachable!("level is {}", level),
// -------------- Compile time error --------------
thread 'main' panicked at 'internal error: entered unreachable code: level is 22', src/main.rs:7:14

misconception corollaries

ifT: 'staticthenTmust be valid for the entire program

Misconception Corollaries

  • T: 'staticshould be read as Thas a'staticlifetime”
  • &'static TandT: 'staticare the same thing
  • ifT: 'staticthenTmust be immutable
  • ifT: 'staticthenTcan only be created at compile time

Most Rust beginners get introduced to the'staticlifetime for the first time in a code example that looks something like this:

1
2
3
fn main() {
let str_literal: &'static str = "str literal";
}

They get told that"str literal"is hardcoded into the compiled binary and is loaded into read-only memory at run-time so it’s immutable and valid for the entire program and that’s what makes it'static. These concepts are further reinforced by the rules surrounding definingstaticvariables using thestatickeyword.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Note: This example is purely for illustrative purposes.
// Never use `static mut`. It's a footgun. There are
// safe patterns for global mutable singletons in Rust but
// those are outside the scope of this article.

static BYTES: [u8; 3] = [1, 2, 3];
static mut MUT_BYTES: [u8; 3] = [1, 2, 3];

fn main() {
MUT_BYTES[0] = 99; // ❌ - mutating static is unsafe

unsafe {
MUT_BYTES[0] = 99;
assert_eq!(99, MUT_BYTES[0]);
}
}

Regardingstaticvariables

  • they can only be created at compile-time
  • they should be immutable, mutating them is unsafe
  • they’re valid for the entire program

The'staticlifetime was probably named after the default lifetime ofstaticvariables, right? So it makes sense that the'staticlifetime has to follow all the same rules, right?

Well yes, but a type with a'staticlifetime is different from a type bounded by a'staticlifetime. The latter can be dynamically allocated at run-time, can be safely and freely mutated, can be dropped, and can live for arbitrary durations.

It’s important at this point to distinguish&'static TfromT: 'static.

&'static Tis an immutable reference to someTthat can be safely held indefinitely long, including up until the end of the program. This is only possible ifTitself is immutable and does not move after the reference was created.Tdoes not need to be created at compile-time. It’s possible to generate random dynamically allocated data at run-time and return'staticreferences to it at the cost of leaking memory, e.g.

1
2
3
4
5
6
7
use rand;

// generate random 'static str refs at run-time
fn rand_str_generator() -> &'static str {
let rand_string = rand::random::<u64>().to_string();
Box::leak(rand_string.into_boxed_str())
}

T: 'staticis someTthat can be safely held indefinitely long, including up until the end of the program.T: 'staticincludes all&'static Thowever it also includes all owned types, likeString,Vec, etc. The owner of some data is guaranteed that data will never get invalidated as long as the owner holds onto it, therefore the owner can safely hold onto the data indefinitely long, including up until the end of the program.T: 'staticshould be read as Tis bounded by a'staticlifetime” not Thas a'staticlifetime”. A program to help illustrate these concepts:

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 rand;

fn drop_static<T: 'static>(t: T) {
std::mem::drop(t);
}

fn main() {
let mut strings: Vec<String> = Vec::new();
for _ in 0..10 {
if rand::random() {
// all the strings are randomly generated
// and dynamically allocated at run-time
let string = rand::random::<u64>().to_string();
strings.push(string);
}
}

// strings are owned types so they're bounded by 'static
for mut string in strings {
// all the strings are mutable
string.push_str("a mutation");
// all the strings are droppable
drop_static(string); // ✅
}

// all the strings have been invalidated before the end of the program
println!("I am the end of the program");
}

Key Takeaways

  • T: 'staticshould be read as Tis bounded by a'staticlifetime”

  • ifT: 'staticthenTcan be a borrowed type with a'staticlifetime or an owned type

  • since

    1
    T: 'static

    includes owned types that means

    1
    T
    • can be dynamically allocated at run-time
    • does not have to be valid for the entire program
    • can be safely and freely mutated
    • can be dynamically dropped at run-time
    • can have lifetimes of different durations