Timeline
Timeline
2025-10-22
init
This article introduces advanced functional features of the Rust language, focusing on closures and iterators. Closures are anonymous functions that can be stored in variables or passed as arguments, and can capture values from their environment. The article explains closure type inference, comparison with functions, and how to build caching structs (such as Cacher) using generics and the Fn trait to achieve lazy evaluation. It also details the three ways closures capture the environment: FnOnce (takes ownership), FnMut (mutable borrow), and Fn (immutable borrow), and introduces the move keyword to force ownership transfer. In the iterator section, it points out the lazy nature of iterators, meaning no effect occurs until a consuming method is called.
Functional language features: iterators and closures
closures
Rust’s closures(_closures_are anonymous functions that can be stored in a variable or passed as arguments to other functions
- Anonymous functions
- Stored in variables, passed as arguments
- You can create a closure in one place and then call it in another context to perform the computation.
- can capture values from the scope in which they are defined.
File name: src/main.rs
A function that stands in for a hypothetical calculation and takes about two seconds to execute.
12345678 | use std::thread;use std::time::Duration;fn simulated_expensive_calculation(intensity: u32) -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); intensity} |
File name: src/main.rs
The program’s business logic, which takes the input and calls the simulated_expensive_calculation function to print out the workout plan.
123456789101112131415161718192021 | 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.
123456 | fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; generate_workout(simulated_user_specified_value, simulated_random_number);} |
Closure definition
12345 | let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; |
- The closure definition is the part after the = in the expensive_closure assignment. The closure definition begins with a pair of vertical bars (|), and the closure’s parameters are specified between the bars;
- ifIf there is more than one parameter, they can be separated by commas, for example |param1, param2|。
- After the parameters are the curly braces that hold the closure body — **If the closure body has only one line, the curly braces can be omitted.**After the curly braces, the end of the closure requires a semicolon 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) is used as the return value when the closure is called.
Note:
This let statement means expensive_closure contains an anonymous function’s Definition, not the result of calling the anonymous function Return value。
Refactoring code
File name: src/main.rs
123456789101112131415161718192021 | 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 for closures
- Closures do not require type annotations for parameters and return values.
- Closures are usually short and work in a narrow context, so the compiler can usually infer the types.
- You can manually add types.
12345 | let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; |
Comparison with functions
1234 | fn add_one_v1 (x: u32) -> u32 { x + 1 }//functionlet add_one_v2 = |x: u32| -> u32 { x + 1 };//closureslet add_one_v3 = |x| { x + 1 };//closureslet add_one_v4 = |x| x + 1 ;//closures |
src/main.rs
123 | let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5); |
For this closure, when the second line of code is executed, the compiler can determine that the closure’s type is String, and when the third line is executed, it will report an error.
123456789101112131415161718192021222324252627 | 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 callnote: 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`. |
Closures with generic parameters
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 at every place in the code that needs multiple results from the slow computation closure, so that you can use the variable instead of calling the closure again. However, this would result in many places where the result variable is saved repeatedly.
Fortunately, there is another available solution. You cancreate a struct that stores the 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
- A struct definition needs to know the types of all fields, which means it needs to specify the closure’s type.
- Each closure instance has its own unique anonymous type, even if two closures have exactly the same signature.
- Therefore, we 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 these traits.Fntrait. 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.
1234567891011121314151617181920212223242526272829 | 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 Cacher struct has a field calculation of generic type T.
The trait bound on T specifies that T is a closure that uses the Fn trait.
Any closure we want to store in the calculation field of a Cacher instance must have one u32 parameter (specified within the parentheses after Fn) and must return a u32 (specified after the ->).
The value field is Option
Refactoring code
123456789101112131415161718192021 | 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
- The first problem is that a Cacher instance assumes that for any value of the arg parameter to 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
- The second problem is that it can only accept one parameter of type u32 and return a value of type u32.
Solution:
Introduce two or more generic parameters.
Closures can capture their environment.
- They can capture their environment and access variables from the scope in which they are defined, whereas ordinary functions cannot.
123456789 | fn main() { let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y));} |
- closuresThis incurs memory overhead.
A closure in Rust is a compiler-generated anonymous struct whose fields are the captured variables.
123456789101112 | 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 what is captured isValue, the closure will store that value in its own struct.
- If what is captured isReference, what is stored in the closure struct is a pointer (the reference itself also takes up space).
Overhead size
- Small variables (e.g., i32, bool): almost no extra overhead; it can be stored in the closure’s struct.
- Large variables (e.g., String, Vec, HashMap):
- if
movecapture, the closure will copy or move the entire object (heap memory may be bound to the closure). - If it is only reference capture, the closure only stores a pointer internally, but the reference’s lifetime must be guaranteed valid.
- if
How closures capture values from their environment
The same as the three ways functions receive parameters:
Taking ownership: FnOnce
- FnOnce consumes variables captured from the surrounding scope. The scope surrounding the closure is called its Environment,environment. To consume the captured variables, the closure must take ownership of them and move them into the closure when defining the closure. The ‘Once’ part of its name represents the fact that the closure cannot take ownership of the same variables more than once, so it can only be called once.
Mutable borrowing: FnMut
- FnMut takes mutable borrowed values so it can change its environment.
Immutable borrowing: Fn
- Fn takes immutable borrowed values from its environment.
When creating a closure, Rust infers which trait to use based on how the closure uses the environment values:
- All closures implement FnOnce
- Closures that do not move captured variables implement FnMut
- Closures that do not require mutable access to captured variables implement Fn
In fact, there is a hierarchy: all closures that implement Fn also implement FnMut, and all that implement FnMut also implement FnOnce.
The move keyword
Before the parameter listUsing the move keyword, you can force a closure to take ownership of the environment values it uses.
- when**Pass the closure to a new thread to move data so that it is owned by the new thread.**At this time, this technology is most useful.
Example
1234567891011 | 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 was 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! will fix the problem.
Best Practices
When specifying one of the Fn trait bounds, start with Fn; based on what the closure body does, the compiler will tell you if FnOnce or FnMut is needed.
iterator
The iterator pattern allows you to perform some processing on the items of a sequence.iterator(iterator)responsible for the logic of traversing each item in the sequence and determining when the sequence ends. When using iterators, we don’t need to reimplement this logic.
In Rust,Iterators are lazy (lazy), which means it will have no effect until the method is called to use the iterator.
123 | 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:
1234567 | 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, they define the trait’s Associated Types(associated type)。
This code shows that implementing the Iterator trait requires bothdefine aItemType, this Item type is used as nextThe return type of a methodIn other words, the Item type will be the type of element returned by the iterator.
The Iterator trait only requires implementing one method: next
next:
- Return one item from the iteration each time.
- The return value is wrapped in Some
- When iteration ends, it returns None
You can call the next method directly on the iterator
1234567891011 | 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 an iterator changes the state used to record the position in the sequence. In other words, the code consumeconsumed, or used, the iterator. Each call to next consumes an 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_iter’s ownership and makes v1_iter mutable in the background.
Several iterator methods
- iter Method: oncreates an iterator over immutable references(immutable references to elements)
- into_iter Method: createdthe iterator takes ownership
- iter_mut Method: iterates over mutable references
Methods that consume the iterator
- In the standard library, the Iterator trait is made up of some methods with default implementations
- Some of these methods call the next method
one of the reasons why you must implement the next method when implementing the Iterator trait
- Those that call next are called "consuming adaptor”
because calling them exhausts 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, thus consuming it. As it iterates over each item, it adds each item to a running total and returns the total when iteration is complete.
Filename: src/lib.rs
12345678910 | 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
The Iterator trait defines another kind of method, called iterator adaptors(iterator adaptors),
- They allow you to change the current iterator into a different type of iterator.
- You can chain multiple iterator adaptors.
- However, because all iterators are lazy, you must call a consuming adaptor method to get the results of the iterator adaptor calls.
File name: src/main.rs
123 | let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); |
The map method uses a closure to call each element to generate a new iterator. The closure here creates a new iterator, in which each element of the vector is incremented by 1.
However, this code will produce a warning:
= note: iterators are lazy and do nothing unless consumed
The code doesn’t actually do anything; the specified closure is never called. The warning reminds us why:Iterator adaptors are lazy, and here we need to consume the iterator.
File name: src/main.rs
12345 | 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 is actually letting the compiler infer its type.
The collect method is aconsuming adaptor, 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 as you iterate. This is a great 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 is a filter
- The filter method of an iterator takes a closure that uses each item of 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
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 | 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 from the environment `shoe_size` the variable and uses its value to compare with each shoe's size}mod tests { use super::*; 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_In 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 each shoe’s size, keeping only shoes of the specified size. Finally, it calls collect to gather the values returned by the iterator adapters into a vector and returns it.
Creating Custom Iterators
The only method required to be provided in the Iterator trait definition is the next method. Once it is defined, you can use all other methods provided by the Iterator trait that have default implementations to create custom iterators!
12345678910111213141516171819202122232425262728293031323334353637 | 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 5, next returns the current value wrapped in Some, //However, if count is greater than or equal to 5, the iterator returns None. }} 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 defining the next method to implement the Iterator trait, we can now use any standard library-defined Iterator trait methods that have default implementations, since they all use the functionality of the next method.
For example, for some reason we want to take the values produced by a Counter instance, and pair these values with another Counter instanceomitting the first valueafter that, pair the values produced,multiply each pair of values,keep only those results that are divisible by three,then add all the retained results, which can be done as in the test in Listing 13-23:
Filename: src/lib.rs
123456789 | 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 a variety of methods on the custom Counter iterator, note that Counter itself is an Iterator.
Note that zip only produces four pairs of values; theoretically, the 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 of other methods that call next.
Improving the I/O Project
Use iterators and remove clone
Filename: src/lib.rs
123456789101112131415161718 | 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, }) }} |
Use the iterator returned by env::args directly
File name: src/main.rs
12345678910 | 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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 | 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()}mod tests { use super::*; fn case_sensitive() { let query = "duct"; let contents = "\Rust:safe,fast,productive.Pick three."; assert_eq!(vec!["safe,fast,productive."], search(query, contents)); } 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
123456789101112131415 | 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/iterators
Iterators are Rust’s zero-cost abstraction(zero-cost abstractions) one of, meaning that abstractions do not introduce runtime overhead, and it is exactly as defined by Bjarne Stroustrup (the designer and implementer of C++) in “Foundations of C++” (2012) zero overhead(zero-overhead) is exactly the same:
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 other profiles
Cargo’s two main profiles
- dev profile: for development, cargo build
- release profile: for release, cargo build --release
Custom profiles
Add a [profile.xxxx] section in Cargo.toml, and override a subset of the configuration in it.
Filename: Cargo.toml
12345 | [profile.dev]opt-level = 0[profile.release]opt-level = 3 |
The opt-level setting controls how much optimization Rust applies to your code. The value of this setting ranges from 0 to 3. Higher optimization levels require more compilation time, so if you are developing and compiling frequently, you may want to compile faster at the expense of some code performance. This is why the dev opt-level defaults to 0.
Documentation comments
- Generate HTML documentation
- Documentation comments for the public API: how to use the API
- Use ///
- Supports Markdown
- Placed before the item
Generate documentation
Run the rustdoc tool
1 | cargo doc |
Place the generated documentation under target/doc
Generate documentation and browse it
1 | cargo doc --open |
Common sections
#Examples
Other common sections
123 | Panics: 函数可能发生panic的场景Errors: 如果函数返回Result,描述可能的错误种类,以及可导致错误的条件Safety: 如果函数处于unsafe调用,就应该解释函数unsafe的原因,以及调用者确保的使用前提 |
Documentation comments as tests
Run cargo test: run the example code in documentation comments as tests
Filename: src/lib.rs
12345678910111213 | /// 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 in the example; you should see a section like this in the test results:
123456 | Doc-tests my_craterunning 1 testtest src/lib.rs - add_one (line 5) ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s |
Now try changing the function or the example so that the assert_eq! in the example panics. Run cargo test again, and we will see that the doc test catches that the example and code are no longer in sync!
Add documentation comments to the item that contains the comments
- Symbol: //!
- This type of comment usually 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
1234567 | //! # 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 export a convenient public API
The file structure you use during development may not be convenient for users. Your structure may be a hierarchical structure 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 find it hard to discover that these types exist. They may also be annoyed at having to useuse 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
123456789101112131415161718192021222324252627282930 | //! # 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 }} |
File name: src/main.rs
12345678 | use art::kinds::PrimaryColor;use art::utils::mix;fn main() { let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red, yellow);} |
To remove the crate’s internal organization 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
123456789101112131415 | //! # 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
File name: src/main.rs
123456 | 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 project ready to publish’s Cargo.toml The file might look like this:
Filename: Cargo.toml
123456789 | [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 discovered and used more easily!
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 normally.
Publish a new version of an existing crate.
Modify the version and republish.
Use cargo yank to yank a version from Crates.io.
- You cannot delete previous versions of a crate.
Yanking a version prevents new projects from starting to depend on that version, but all existing projects that depend on it can still download and depend on that version. Essentially, yanking means that all projects with Cargo.lock the dependencies of projects will not be broken, and any newly generated 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 |
Yank does not Delete any code. For example, the recall feature is not intended to delete accidentally uploaded secret information. If this happens, immediately reset these secrets.
Cargo Workspaces
- cargo workspaces: help 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 folder.
Creating a workspace
In order to at the top level add directory run the binary crate, you can use the -p parameter and package name to run cargo run to specify the package in the workspace that we want to use:
1234 | $ 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 will run adder/src/main.rs the code in, 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: a runnable program
- generated by a crate that has src/main.rs or is otherwise designated as a binary
Usually: the README contains a description of the crate:
- Has a library target
- Has a library target
- Has both
cargo install
The binaries installed by cargo install are stored in the bin folder in the root directory
Extending cargo with custom commands
- cargo is designed to be extended with subcommands
- Example: if a binary in $PATH is cargo-something, you can run it like a subcommand:
1 | $ cargo something |
- Custom commands like this can be listed with the command: cargo --list
- Advantages: You can use cargo install to install extensions and run them like built-in tools.
smart pointers
- pointer (pointer) is a general concept for a variable that contains a memory address.
This address references, or “points at,” some other data.
- The most common pointers in Rust are Reference(reference)。
References are marked by the & symbol and borrow the value they point to. They have no other special features besides referring to data. They alsohave no extra overhead., so they are used the most.
- smart pointers(smart pointers) are a class of data structures that behave like pointers but also have additional metadata and functionality.
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 region of memory 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).
Implementing smart pointers
**Smart pointers are usually implemented using structs,**and implement the Deref and Drop traits.
The Deref trait allows instances of a smart pointer struct to be used like references.
The Drop trait allows you to customize the code that runs when a smart pointer instance goes out of scope.
Using BoxPointing to data on the heap.
Box
is the simplest smart pointer: - allows you to store data on the heap (rather than the stack)
- on the stack is a pointer to the heap data
- No performance overhead.
- It has no other extra features.
Box
Implements the Deref trait and the Drop trait.
mostly used in the following scenarios:
- 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 is not copied
- when you want to own a value and only care that it implements a specific trait, rather than its concrete type
1234 | 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 occupies
- But the size of a recursive type cannot be determined at compile time
- But the size of a Box type is fixed
- Using Box in recursive types can solve 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”) uses two arguments to construct a new list, usually a single value and another list.
The concept of the cons function involves more common functional programming terminology; “to x and y cons” usually means constructing a new container and placing x the elements of at the beginning of the new container, followed by the container y the elements of.
Each item in a cons list contains two elements: the value of the current item and the next item. Its 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 representing the termination condition (base case) of recursion is Nil, which announces the termination 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 commonly used collection in Rust
123456789 | use crate::List::{Cons,Nil};fn main() { let list = Cons(1, Cons(2, Cons(3, Nil)));}enum List { Cons(i32,List), Nil,} |
Runtime error
12345678910111213141516 | 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 |
Computing the size of a non-recursive type
Message enum:
123456 | 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 check each variant and find that
- Message::Quit does not need any space,
- Message::Move needs enough space to store two i32 values, and so on.
- Because an enum only actually uses one of its variants, the space required for a Message value is equal to the size of its largest variant.
In contrast, what happens when the Rust compiler checks a recursive type like the List from the previous example? The compiler tries to figure out how much memory is needed to store a List enum, and starts by examining the Cons variant. The Cons variant needs space equal to the size of an i32 plus the size of a List. To calculate how much memory List needs, it checks its variants, starting with the Cons variant. The Cons variant stores an i32 value and a List value, so this calculation would continue indefinitely.
Using BoxGiving recursive types a known size
- Because Box
is 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
- It only provides indirection and heap memory allocation.
- It has no other extra features.
- No performance overhead.
- Suitable for scenarios that require indirect storage, such as a Cons List.
- Implements the Deref trait and the Drop trait.
Dref Trait
- Implementing the Deref trait allows us tocustomize the behavior of the dereference operator*
- By implementing Deref, smart pointers canbe treated like references.
Dereference operator
File name: src/main.rs
1234567 | fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y);} |
Treating Boxas a reference
File name: src/main.rs
1234567 | fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y);} |
Define your own smart pointer
File name: src/main.rs
123456789101112131415 | 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:
12345678910 | $ 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 |
MyBox
Treat 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 inner data
File name: src/main.rs
123456789 | 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,
1234567 | 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)
- Deref Coercion isfor functions and methodsa convenience feature provided
- Suppose T implements the Deref trait: Deref Coercion can convert a reference to T into a reference produced by applying Deref to T.
- When a reference of a type is passed to a function or method, but its type does not match the defined parameter type:
- Deref Coercion will automatically occur
- The compiler makes a series of deref calls to convert it into the required parameter type.
- It is done at compile time, with no extra performance overhead.
File name: src/main.rs
12345678910 | 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);} |
- Here we use &m to call the hello function, which is a MyBox
value’s reference - Because in the example, on MyBox
the Deref trait is implemented, Rust can convert &MyBox through deref calls becomes &String. - The standard library provides a Deref implementation on String that returns a string slice, as 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, in order to use &MyBox
File name: src/main.rs
1234 | fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]);} |
Deref and Mutability
- You can use the DerefMut trait to overload the * operator for mutable references.
- When the type and trait are in the following three situations, Rust performs deref coercion:
- When T: Deref<Target=U>, allow &T to convert to &U
- When T: DerefMut<Target=U>, allow &mut T to convert to &mut U
- When T: Deref<Target=U>, allow &mut T to convert to &U
| Case | Condition | Conversion |
|---|---|---|
| 1 | T: Deref<Target=U> | &Tautomatically becomes&U |
| 2 | T: DerefMut<Target=U> | &mut Tautomatically becomes&mut U |
| 3 | T: Deref<Target=U> | &mut Tmutable referencedowngradeto an immutable reference&U |
Example:
12345678 | 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
For example: file, network resource release, etc.
Any type can implement the Drop trait
The Drop trait only requires you to implement the drop method
- Parameter: a mutable reference to self
The Drop trait is in the prelude
File name: src/main.rs
12345678910111213141516171819 | 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 you run this program, the following output appears:
1234567 | $ 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`! |
Use std::mem::drop to drop a value early
It is difficult to directly disable automatic drop, and there is no need to do so.
- 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 std::mem::drop function from the standard library (prelude) to drop a value early.
File name: src/main.rs
12345678 | 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:
1234567 | $ 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 that are still in use; this would cause a compiler error. The ownership system ensures that references are always valid, and also ensures that drop is called only once, when the value is no longer used.
RcReference Counting Smart Pointer
Sometimes a value has multiple owners.
To support multiple ownership: Rc
reference counting
track references to a value
0 references: the value can be cleaned up.
Data needs to be allocated on the heap, and this datais read by multiple parts of the program (read-only), but atcompile time, it is impossible to determine which part will finish using this data last.
Note Rc
Can only be used in single-threaded scenarios. ;
We want to create two lists that share ownership of a third list. Conceptually, this will look like the figure:
File name: src/main.rs
Cannot use two Boxes
123456789101112 | 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:
123456789 | 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` trait10 | let b = Cons(3, Box::new(a)); | - value moved here11 | let c = Cons(4, Box::new(a)); | ^ value used here after move |
We modify the definition of List to use Rc, which increases the reference count from 1 to 2 and allows a and b to share the Rc
ownership of the data in the Rc. When creating c, it also clones a, which increases the reference count from 2 to 3. Each time Rc::clone is called, the Rc
the reference count of the data in the Rc will increase, and the data will not be cleaned up until there are zero references.
File name: src/main.rs
12345678910111213 | 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:
1234567891011121314151617 | a (Rc) │ ▼ Cons(5) │ ▼ Cons(10) │ ▼ Nilb ---> Cons(3) ───┐ │ ▼ a (共享)c ---> Cons(4) ───┘ |
You could also call a.clone() instead of Rc::clone(&a), but Rust’s convention here is to use Rc::clone.
- Rc::clone’s implementation does not make a deep copy of all the data like most types’ clone implementations do.
- Rc::clone only increases the reference count, which doesn’t take much time. A deep copy can take a long time.。
Cloning Rcincreases the reference count
File name: src/main.rs
Rc::strong_count gets the reference count
1234567891011 | 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:
12345678 | $ 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 = 1count after creating b = 2count after creating c = 3count after c goes out of scope = 2 |
We can see that the Rc in a has 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, when the Rc
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 Rc is completely cleaned up. Using Rc
- Rc
Through theImmutable reference, Rc Allows read-only sharing of data between multiple parts of the program. - If Rc
also allowed multiple mutable references, it would violate one of the borrowing rules discussed in Chapter 4: multiple mutable borrows to the same location could cause data races and inconsistency.
RefCelland interior mutability
Interior mutability
Interior mutability is one of Rust’s design patterns.
It allows you to modify data while holding an immutable reference.
Data structures that implement interior mutability use unsafe code internally to bypass Rust’s normal mutability and borrowing rules.
Similar to Rc
RefCell type represents the sole ownership of the data it holds.
Recall the borrowing rules:
- At any given time, you can have either one mutable reference or any number of immutable references.
- References are always valid.
RefCellUnlike Boxthe difference
| Box | RefCell |
|---|---|
| compilation phaseenforce borrowing rules on code | only atRuntimecheck borrowing rules |
| otherwise an error occurs | otherwise panic |
Comparison of borrowing rules checked at different stages
| compilation phase | Runtime |
|---|---|
| expose problems as early as possible | Problems are exposed late, even in production. |
| No runtime overhead. | Slight performance loss due to reference counting. |
| Best choice for most scenarios. | Implements certain specific memory safety scenarios (modifying own data in an immutable environment). |
| Is Rust’s default behavior. |
- Similar to Rc
can only be used forsingle-threadedScenario
Choosing Box,Rc,RefCellthe basis for
| Box | Rc | RefCell | |
|---|---|---|---|
| owner of the same data | A | multiple | A |
| Mutability, borrow checking | Mutable and immutable borrows (compile-time check) | Immutable borrows (compile-time check) | Mutable and 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 mutably borrow it. For example, the following code will not compile:
1234 | fn main() { let x = 5; let y = &mut x;} |
If you try to compile it, you will get the following error:
1234 | $ 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 |
The following is a scenario we want to test:
We are writing a library that records the difference between a value and a maximum value, and sends messages based on the difference between the current value and the maximum value. For example, this library can be used to track the API call quota allowed for a user.
The library only provides functionality for recording the gap from the maximum value and for deciding what message to send in which situation. Programs that use this library are expected to provide the actual message-sending mechanism: the program can choose to log a message, send an email, send a text message, and so on. 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
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 | 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!"); } }}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)); } } 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 a self’sImmutable referenceand text messages. This trait is the interface that mock objects need to implement, so that the mock can be used like a real object. Another important part is that we need to test LimitTracker’s set_value method. We can change the value of the passed-in value parameter, but set_value does not return any value that can be asserted. That is, if we create a LimitTracker with a value that implements the Messenger trait and a specific max, when different value values are passed, the message sender should be told to send appropriate messages.
The mock object we need is one that, when send is called, 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 attempted mock object implementation, but the borrow checker does not allow it:
However, this test has a problem:
123456789101112131415 | $ 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 mutableFor more information about this error, try `rustc --explain E0596`.error: could not compile `limit-tracker` due to previous errorwarning: build failed, waiting for other jobs to finish...error: build failed |
We cannot modify MockMessenger to record messages because the send method takes an immutable reference to self. We also cannot use &mut self instead as suggested by the error text, because then send’s signature would not match the signature in the Messenger trait definition (you can try changing it to see what error message appears).
This is exactly where interior mutability comes in! We will use RefCell to store sent_messages, and then send will be able to modify sent_messages and store the messages.
Filename: src/lib.rs
123456789101112131415161718192021222324252627282930 | 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)); } } fn it_sends_an_over_75_percent_warning_message() { // --snip-- assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); }} |
Using RefCellRecording borrow information at runtime
Two methods (safe interface)
borrow method: returns the smart pointer Ref
, which implements Deref borrow_mut method: returns RefMut
, which implements Deref
RefCell
It records how many active Ref and RefMut smart pointers Each call to borrow increments the immutable borrow count by 1.
any Ref
When the value goes out of scope and is dropped: immutable borrow count -1 Each call to borrow_mut: mutable borrow count +1
Any RefMut
When the value goes out of scope and is dropped: mutable borrow count -1
Rust uses this count to enforce the borrow checking rules: at any given time, only multiple immutable borrows or one mutable borrow are allowed.
Combining Rc and RefCell to have multiple owners of mutable data
File name: src/main.rs
123456789101112131415161718192021222324 | 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:
1234567 | $ 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 can implement interior mutability
- Cell
: access data by copying - Mutex
: used to implement the interior mutability pattern in cross-thread situations
Reference cycles lead to memory leaks
Rust’s memory safety guarantees make it difficult to accidentally create memory that will never be cleaned up (called memory leak(memory 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 Rc
File name: src/main.rs
12345678910111213141516171819202122232425262728293031323334353637383940414243 | use crate::List::{Cons, Nil};use std::cell::RefCell;use std::rc::Rc;// List is a linked list, where each node is Cons(value, next) or Nilenum 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 out and run the code, you will get the following output:
1234567891011 | $ 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 = 1a next item = Some(RefCell { value: Nil })a rc count after b creation = 2b initial rc count = 1b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })b rc count after changing a = 2a rc count after changing a = 2 |
If you uncomment the last println! and run the program, Rust will try to print a cycle like a pointing to b pointing to a until the stack overflows. This is because:
In Rust,#[derive(Debug)]for enums (such as linked lists) generates a recursive printing logic: note:nextwill also callDebug→ then prints itsnext→ Recursive call, and this recursive call has no termination condition, so it will eventually cause a stack overflow.
1234567891011 | 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") } }} |
Solutions to prevent memory leaks
It relies on the developer to ensure, not on Rust.
Reorganize the data structure: some references express ownership, some references do not express ownership.
Part of the circular reference has an ownership relationship, and the other part does not involve an ownership relationship.
And only ownership relationships affect the cleanup of values.
Avoid reference cycles: turn Rc into Weak
Rc::clone increments the Rc
instance’s strong_count by 1, Rc instance is only cleaned up when strong_count is 0 Rc
An instance can create a Weak Reference (weak reference) to the value by calling the Rc::downgrade method. The return type is Weak
(smart pointer) Calling Rc::downgrade increments weak_count by 1.
Rc
Use weak_count to track how many Weak references exist. weak_count not being 0 does not affect Rc
the cleanup of the instance
Strong VS Weak
- Strong Reference is about how to share Rc
instance ownership - Weak Reference does not express the above meaning.
- Using Weak Reference does not create reference cycles:
When the number of Strong References is 0, the Weak Reference automatically disconnects.
- Before using Weak
, you need to ensure that the value it points to still exists:
On a Weak
Creating a tree data structure: a Node with children
File name: src/main.rs
12345678910111213141516171819 | use std::cell::RefCell;use std::rc::Rc;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 leaf
Adding a reference from child to parent
To make a child node know its parent, we need to add a parent field to the Node struct definition. The question is what the type of parent should be. We know it cannot contain Rc
Now let’s think about this relationship in a different way:
- A parent node should own its child nodes.
- If a parent node is dropped, its child nodes should also be dropped.
- However,A child node should not own its parent node.
- If a child node is dropped, its parent node should still exist.。
This is exactly an example of weak references!
So parent uses Weak
File name: src/main.rs
12345678910111213141516171819202122232425262728 | use std::cell::RefCell;use std::rc::{Rc, Weak};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 the leaf node is similar to how we created the leaf node in Listing 15-27, except the parent field is different: leaf starts with no parent, so we create a new empty Weak reference instance.
At this point, when we try to use the upgrade method to get a reference to leaf’s parent, we get a None value. As shown in the first println! output:
1 | leaf parent = None |
When creating a branch node, it also creates a new Weak
When printing leaf’s parent again, this time you will 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: Weak
123 | leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },children: RefCell { value: [] } }] } }) |
The lack of infinite output shows that this code did not create a reference cycle. This can also be seen from observing the Rc::strong_count and Rc::weak_count call results.
Visualizing changes to strong_count and weak_countLet’s, by creating a new inner scope and placing the creation of branch inside it, observe the Rc
Example: Creating branch in an inner scope and checking its strong and weak reference counts
File name: src/main.rs
1234567891011121314151617181920212223242526272829303132333435363738394041424344 | 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 Rc
When the inner scope ends, branch goes out of scope, and the Rc
If you try to access the parent of leaf after the end of the inner scope, you will get None again. At the end of the program, the Rc in leaf
All of this logic for managing counts and values is built into Rc
Fearless Concurrency
- concurrent programming(Concurrent programming), which means different parts of the program execute independently,
- parallel programming(parallel programming) means different parts of the program execute at the same time
Using Threads to Run Code Simultaneously
In most modern operating systems, the code of an executed program runs in a process(process), and the operating system manages multiple processes. Inside a program, you can also have multiple independent parts running at the same time. The feature that runs these independent parts is called threads(threads)。
Splitting the computation in a program into multiple threads can improve performance, because the program can perform multiple tasks at the same time, but it also adds complexity. Because threads run simultaneously, there is no guarantee about the order in which code in different threads executes. This can lead to problems 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 a resource they own, which prevents them from continuing
- Bugs that occur only in certain situations and are difficult to reproduce and fix reliably
Programming languages have a few different ways to implement threads.
- Many operating systems provide an API for creating new threads. This model, in which a programming language calls the operating system API to create threads, is sometimes called _1:1_1:1 threading, where one OS thread corresponds to one language thread.Rust’s standard library only provides 1:1 thread implementation.; requires a smaller runtime (i.e., 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, and has no extra user-space scheduling).
- There are some crates that implement other thread models with different trade-offs, namely language-implemented threads (green threads): the M:N model. Requires a larger runtime.
Creating a new thread with spawn
To create a new thread, you need to call thread::spawn function and pass a closure containing the code you want to run in the new thread
File name: src/main.rs
12345678910111213141516 | 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.
Using the join Handle to wait for all threads to complete.
- The return type of thread::spawn is JoinHandle.
- JoinHandle is an owned value
- When the join method is called on it, it blocks the currently running thread until the thread represented by the handle terminates.
File name: src/main.rs
123456789101112131415161718 | 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.Blocking(Blocking) Thread means blocking that thread from doing work or exiting. Because we placed the join call after the for loop in the main thread,
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 the ownership of a value from one thread to another.
Example: Trying to use a vector created in the main thread from another thread
File name: src/main.rs
1234567891011 | 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:
12345678910111213141516171819202122232425 | $ 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 will infer how to capture v, because println! only needs a reference to v, so 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 to no longer be valid:
File name: src/main.rs
12345678910111213 | 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 modified code, which compiles and runs as we expect:
Listing: Using the move keyword to force ownership of the values it uses
File name: src/main.rs
1234567891011 | 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
One increasingly popular approach to ensuring safe concurrency is message passing(message 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
- A channel consists of:Transmitting end,Receiving end
- Call the sender’s methods to send data
- The receiving end checks and receives the arriving data.
- If either the sending end or the receiving end is dropped, the channel is “closed”.
Creating a Channel
- usempsc::channel function to create a channel
- mpsc stands for multiple producer,single consumer(multiple producers, one consumer)
- Returns a tuple: the elements are the sending end and the receiving end, respectively.
1 | pub fn channel<T>() -> (Sender<T>, Receiver<T>) |
Let’s move the sending end to a new thread and send a string, so the new thread can communicate with the main thread.
Listing: Moving tx to a new thread and sending “hi”
1234567891011121314 | 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 we again use thread::spawn to create a new thread and use move to move 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 the channel.
The sending side of a channel has a send method that takes the value to be put into the channel. The send method returns a Result<T, E> type, so**if the receiving end has already been dropped, there is no target to send the value to, so the send operation will return an error.**In this example, calling unwrap when an error occurs causes a panic. However, for a real program, you need to handle it properly.
The recv method on the receiving end
- The receiving end of a channel has two useful methods: recv and try_recv.
- Here, we used recv, which is receive short for receive. This method will**block 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 closes, recv returns an error indicating that no new values will arrive.
- try_recv does not block, instead it immediately returns a Result<T, E>: an Ok value contains available information, and an Err value means there is no message at this time. If the thread has other work to do while waiting for messages, use try_recv is useful: you can write a loop that frequently calls try_recv, processing messages when available, and otherwise doing some other work for a while until checking again.
Channels and Ownership Transference
Now let’s do an experiment to see how channels and ownership work together to avoid problems: we will try to send the val value through the channel in the new thread after use it. Try compiling the code in the following example and see why this is not allowed:
Example: Attempting to use val after we have sent it through the channel
File name: src/main.rs
123456789101112131415 | 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 drop 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:
1234567891011121314 | $ 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` trait9 | tx.send(val).unwrap(); | --- value moved here10 | println!("val is {}", val); | ^^^ value borrowed here after moveFor more information about this error, try `rustc --explain E0382`.error: could not compile `message-passing` due to previous error |
Our concurrency error causes a compile-time error. The send function takes ownership of its parameter and moves the value to be owned by the receiver. This prevents accidentally using the value again after sending; the ownership system checks that everything is in order.
Sending Multiple Values and Seeing the Receiver Waiting
Example: Sending multiple messages and pausing for a while after each send
File name: src/main.rs
12345678910111213141516171819202122232425 | 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, there is a vector of strings in the newly created thread that we want to send to the main thread. We iterate over them, send each string individually, and call the thread::sleep function with a Duration value to pause for one second.
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 you run the code in Example 16-10, you will see the following output, with each line pausing for one second:
1234 | Got: hiGot: fromGot: theGot: thread |
Because there is no code in the for loop in the main thread that pauses or waits, it can be said that the main thread is waiting to receive values from the newly created thread.
Create multiple producers by cloning the sender
Example: Sending multiple messages from multiple producers
File name: src/main.rs
1234567891011121314151617181920212223242526272829303132333435363738 | // --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 a new thread, we called the clone method on the sending end of the channel. This will give us a**The sender 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 way there will be two threads, each sending a different message to the channel’s receiver.
If you run these codes, you possible You will see output like this:
12345678 | Got: hiGot: moreGot: fromGot: messagesGot: forGot: theGot: threadGot: you |
Although you may see these values appear in a different order; this depends on your system. This is why concurrency is both interesting and difficult. If you experiment with thread::sleep, providing different values in different threads, you will find that their execution is more nondeterministic and produces different output each time.
Shared-state concurrency
- To some extent, channels in any programming language are similar to single ownership, because once a value is sent into a channel, it can no longer be used.
- Shared memory is similar to multiple ownership: multiple threads can simultaneously access the same memory location.
A mutex allows only one thread to access data at a time.
mutex(mutex) are mutual exclusion abbreviation, that is, at any time, it only allows one thread to access certain data. To access the data in the mutex, a thread first needs to acquire the mutex’s lock(lock) to indicate that it wishes to access the data. A lock is a data structure that is part of a mutex, which records who has exclusive access to the data. Therefore, we describe the mutex as a lock system. protect(guarding) its data.
Mutexes are notorious for being difficult to use, because you have to remember:
- Attempt to acquire the lock before using the data.
- After you have finished 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 can’t get locking and unlocking wrong.
The 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: For simplicity, exploring the Mutex in a single-threaded context
File name: src/main.rs
123456789101112 | 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 functionnewto create aMutex.lockmethod to acquire the lock to access the data in the mutex. This call blocks the current thread until we have the lock.
Once the lock is acquired, we can treat the return value (here,num)**as a mutable reference to its inner data.**The type system ensures that we acquire the lock before using the value inm:Mutexi32, so Must You must acquire the lock to use thisi32value. We won’t forget to do this because otherwise the type system won’t allow access to the inneri32value.
Mutexlockcall returns a smart pointer calledMutexGuard.。This smart pointer implementsDerefto point to its inner data; it also provides aDropimplementation that automatically releases the lock whenMutexGuardgoes out of scope.which happens at the end of the inner scope in Listing 16-12. For this reason, we don’t risk forgetting to release the lock and blocking the mutex from being used by other threads, becausethe lock is released automatically.。
Sharing a Mutex Between Threads
Now let’s try using a MutexMutex
Example: The program spawns 10 threads, each of which uses a Mutex
File name: src/main.rs
12345678910111213141516171819202122 | 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 containing an i32.
In the main thread, we collect all the join handles and call their join methods to ensure all threads finish. At this point, the main thread acquires the lock and prints the program’s result.
Compilation fails:
123456789101112131415 | $ 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 loop10 | let mut num = counter.lock().unwrap(); | ------- use occurs due to use in closureFor 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 loop iteration. So Rust tells uscannotcountermove the ownership of the lock to multiple threads。
Multiple Threads and Multiple Ownership
By using the smart pointer Rc
Example: Attempting to Use Rc
File name: src/main.rs
123456789101112131415161718192021222324 | 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!
123456789101112131415161718192021 | $ 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 safely12 | | 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 line of the error indicates Rc<Mutex cannot be sent between threads safely. The compiler also tells us the reasonthe trait Sendis not implemented forRc<Mutex
Unfortunately,Rc
Atomically Reference Counted Arc
Arc
Why aren’t all primitive types atomic? Why aren’t all types in the standard library implemented with Arc by default?
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 Arc
File name: src/main.rs
1234567891011121314151617181920212223 | 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 |
The Similarity Between RefCell/Rc and Mutex/Arc
- Because counter is immutable, but we can get a mutable reference to its inner value; this means Mutex
provides interior mutability, just like the Cell family of types. Just as you can use RefCell to change the contents of Rc in the same way, you can use Mutex To change Arc The content within. - Rust cannot avoid using Mutex
The entire logic error. Recall using Rc. there is a risk of causing a reference cycle, where two Rc Mutual references between values cause memory leaks. Similarly, Mutex also causes deadlock(deadlock) risk. This occurs when an operation needs to lock two resources and two threads each hold one lock, causing them to wait for each other forever.
Scalable Concurrency with Sync and Send Traits
An interesting aspect of Rust’s concurrency model is that the language itself knows very little about concurrency. very fewAlmost everything we discussed earlier belongs to the standard library, not the language itself. Since the language does not 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:embedded in the language:The Sync and Send traits in std::marker.
Allow transferring ownership between threads via Send
- The Send marker trait indicates that ownership of values of types that implement Send can be transferred between threads.
- Almost all Rust types are Send,
- However, there are some exceptions, including Rc
: This cannot be sent,
Because if Rc is cloned
- Rust’s type system and trait bounds ensure that the unsafe Rc will never be accidentally…
Sending between threads. When trying to do so, you get the error the trait Send is not implemented for Rc<Mutex >. But using Arc, which is marked as Send, there is no problem. - Any type that is entirely composed of Send types is automatically marked as Send as well.Almost all primitive types are Send, except for raw pointers.。
Sync allows access from multiple threads.
- The Sync marker trait indicates that a type that implements Sync cansafely have references to its value in multiple threads.。
- In other words,**for any type T, T is Sync if &T (an immutable reference to T) is Send,**which means that references to it 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 Rc
is also not Sync, for the same reason it is not Send. RefCell and Cell series types are not Sync. RefCell The borrow checking performed at runtime is also not thread-safe. - Mutex
is Sync , as described in the “Sharing a Mutex Between Threads” section, 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 need any methods to be implemented. They are only used to enforce invariants related to concurrency.
- Manually implementing these marker traits involves writing unsafe Rust code,
What is important now is that when creating a new concurrent type made up of parts that are not Send and Sync, you need to be careful to ensure that its safety guarantees are maintained.“The Rustonomicon” contains more information about these guarantees and how to maintain them.
Rust’s object-oriented features
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 not called objects, they provide the same functionality as objects,
- Encapsulation hides implementation details
Wrapper(_encapsulation_The idea: the implementation details of an object cannot be accessed by code using the object. Therefore, the only way to interact with an object is through the public API it provides; code using the object cannot reach into the object’s internals and directly change data or behavior. Encapsulation makes it possible to change and refactor an object’s internals without changing the code that uses the object.
In Rust, the pub keyword can be used to make modules, types, functions, and methods public, while by default everything else is private.
Example:
For example, we can define a struct AveragedCollection that contains a vector of i32 values. The struct can also have a field that holds the average of all values in the vector. This way, anyone who wants to know the average of the vector in the struct can get it at any time without having to calculate it themselves. In other words, AveragedCollection will cache 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
1234 | pub struct AveragedCollection { list: Vec<i32>, average: f64,} |
Note thatthe struct itself is marked as pub, so other code can use this struct, but the fields inside the struct remain private. This is very important, because we want to ensure that when a value is added to the list or removed from the list, the average is updated at the same time. This can be done by implementing add, remove, and average methods on the struct, as shown in the example:
Example: The add, remove, and average public methods are implemented on the AveragedCollection struct
Filename: src/lib.rs
1234567891011121314151617181920212223242526 | 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 the add method is used to add an element to list, or the remove method is used 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 list; otherwise, when list changes, the average field might become out of sync. The average method returns the value of the average field, which allows external code to only read 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 can use HashSet
If encapsulation is a necessary aspect for a language to be considered object-oriented, then Rust satisfies this requirement. Using pub or not in different parts of the code can encapsulate implementation details.
- Inheritance as a type system and code sharing
inherit(Inheritance) is a mechanism provided by many programming languages, where an object can be defined to inherit the definition of another object, allowing it to obtain the parent object’s data and behavior without redefining them.
**If a language must have inheritance to be called an object-oriented language, then Rust is not object-oriented.**It is not possible to define a struct that inherits the members and methods of a parent struct. However, if you have often used inheritance in your programming toolbox, Rust also provides other solutions, depending on the reason you originally considered inheritance.
There are two main reasons for choosing inheritance.
- The first is to reuse code: once a particular behavior is implemented for a type, inheritance can reuse that implementation for a different type. In contrast, Rust code can use default trait method implementations for sharing,
- The second reason for using inheritance relates to the type system: it manifests as subtypes being usable where the parent type is used. This is also known as polymorphism(polymorphism), which means that if multiple objects share specific properties, they can be used interchangeably.
Recently, inheritance as a language design solution has fallen out of favor in many languages, as 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 makes program design more inflexible and introduces meaningless subclass method calls., or the possibility of errors due to methods not actually being applicable to subclasses. Some languages also only allow subclasses to inherit from one parent class, further limiting the flexibility of program design.
Trait objects for values of different types
The limitation that a vector can only store 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 that represents a row of cells. This works perfectly when you know at compile time the fixed set of types you want to be able to use interchangeably.
However, sometimes we want library users to be able to extend the set of valid types in specific situations.
To demonstrate how to implement 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 that developers can 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 an Image, and another might add a SelectBox.
This example won’t implement a fully functional GUI library, but it will show how the various parts fit together. When writing a library, we cannot know and define all the types that other programmers might want to create. What we do know is that the GUI needs to keep track of a series 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 the draw method 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. Other classes such as Button, Image, and SelectBox derive from Component and thus inherit the draw method. Each of them can override the draw method to define its 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 have to find another way.
Defining a trait for common behavior
To achieve the behavior expected by the GUI, let’s define a Draw trait that contains a method named draw. Then we can define a storage trait object The vector. A trait object points to an instance of a type that implements the trait we specify, as well as a table used to look up the trait’s methods for that type at runtime. We create trait objects by specifying some kind of pointer, such as a & reference or Box.
Rust deliberately does not refer to structs and enums as “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 separated, unlike in other languages where data and behavior are combined into a concept called an object.
Trait objects combine both data and behavior; in this sense, then they are more 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-purpose as objects in other languages: their (the trait object’s) specific role 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
123 | pub trait Draw { fn draw(&self);} |
Below, we define a struct named Screen that holds a vector named components. The type of this vector is Box
Filename: src/lib.rs
123 | pub struct Screen { pub components: Vec<Box<dyn Draw>>,} |
Example: A definition of a Screen struct that has a field named components, which contains a vector of trait objects that implement 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
1234567 | 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, and Trait objects, on the other hand, allow multiple concrete types to be substituted at runtime. For example, you could define the Screen struct to use generics and trait bounds, as shown in the example:
Example: An alternative implementation of the Screen struct whose run method uses generics and trait bounds
Filename: src/lib.rs
1234567891011121314151617 | 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 must be of the same type, meaning you must have a list of components that are all Button types or all TextField types. If you only needhomogeneous (same-type) collections, then you would prefer to use generics and trait bounds, because their definitions are monomorphized with concrete types at compile time.
12345678910111213 | 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 either Button or TextField.
Implementing the Trait
Now let’s add some types that implement the Draw trait. We will provide the Button type. Actually implementing a GUI library is out of scope, so the draw method body won’t have any meaningful implementation. To imagine what this implementation might look like, a Button struct might have width, height, and label fields, as shown in the example:
Filename: src/lib.rs
Example: A Button struct that implements the Draw trait
1234567891011 | 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; for example, TextField 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 particular type, like the Button type here (it doesn’t contain any actual GUI code, which is beyond the scope of this chapter). In addition to implementing the Draw trait, Button might also have another impl block containing methods for how the button responds to clicks. Such methods are not applicable to types like TextField.
If a user of the library decides to implement a SelectBox struct 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 a SelectBox struct in another crate that uses gui
File name: src/main.rs
12345678910111213 | 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 Box
Example: Using trait objects to store values of different types that implement the same trait
File name: src/main.rs
123456789101112131415161718192021222324 | 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 a library, we don’t know who might add a SelectBox type at some point, but the Screen implementation can operate on and draw this new type because SelectBox implements the Draw trait, which means it implements the draw method.
This concept — caring only about the information a value reflects rather than its concrete type — is similar to what is called duck typing(duck typing) concept: if it walks like a duck and quacks like a duck, then it’s a duck! In the run implementation on Screen in the example, run doesn’t need to know what the concrete types of the components are.It doesn’t check whether a component isButtonorSelectBoxan instance of。By specifyingBox
The advantage of using trait objects and Rust’s type system to perform duck-typing-like operations is that you don’t need to check at runtime whether a value implements a particular method or worry about errors when calling it because the value doesn’t implement the method. If a value doesn’t implement the trait required by the trait object, Rust won’t compile the code.
For example, the example shows what happens when creating a Screen that uses String as its component:
Example: Attempting to use a type that doesn’t implement the trait required by the trait object
File name: src/main.rs
123456789 | use gui::Screen;fn main() { let screen = Screen { components: vec![Box::new(String::from("Hi"))], }; screen.run();} |
We’ll get this error because String doesn’t implement the rust_gui::Draw trait:
123456789101112 | $ 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 pass 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 the compiler performs 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 Static dispatch(static 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 can only determine at runtime which method is being called.
When using trait objects, Rust must use dynamic dispatch.. The compiler cannot know all the types that might be used with trait object code, so it also does not know which method implementation of which type to call. For this reason, Rust uses the 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 the extra flexibility is indeed gained in the process of writing examples and the code that supports the examples, there are still trade-offs to be made.
Trait objects require type safety.
Only object-safe traits can be implemented as trait objects. 。
There are some complex rules for achieving object safety for traits, but in practice, only two rules are relevant.
A trait is object-safe if all the methods defined in the trait satisfy the following rules:
- The return type is not Self.
- There are no generic type parameters.
When we use trait objects, we are actually doing dynamic dispatch.. Rust at runtime uses a so-called vtable method table to find the corresponding method implementation. To achieve this, the trait’s methods must be able to at compile time fully determine the method signature.(including type information of return values, etc.), cannot rely on unknown type information.
But some trait methods depend onSelfor generics, so the compiler cannot 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:
123 | pub trait Clone { fn clone(&self) -> Self;} |
The String type implements the Clone trait. When we call the clone method on a String instance, we get a String instance. Similarly, if we call Vec
When we try to compile code that violates the object safety rules of trait objects, we will receive a compiler hint. For example, we want to implement a Screen struct to hold a type that implements the Clone trait instead of the Draw trait, as shown below
123 | pub struct Screen { pub components: Vec<Box<dyn Clone>>,} |
We will receive the following error:
12345678910111213 | $ 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.
123456789101112131415161718192021222324 | // Use at least two ways to make the code work// Do not add/delete any lines of codetrait 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 way to modify
1234567891011121314151617181920 | 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 way to modify
1234567891011121314151617181920 | 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 pattern(state pattern) is an object-oriented design pattern. The key to this pattern is that a value has some internal state, represented by a series of state objects, and the value’s behavior changes with its internal state
- State objects share 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 knows nothing about the behavior 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 change the code that holds the state or uses the value. We only need to update the code in a state object to change its rules, or add more state objects.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 | 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
Pattern:
- Patterns are a special syntax in Rust for matching the structure of complex and simple types.
- Combining patterns with match expressions and other constructs gives you more control over the program’s control flow.
- Patterns consist of some combination of the following elements:
- Literals
- Destructured arrays, enums, structs, and tuples
- variable
- Wildcard
- Placeholders
Match arms
match VALUE
Requirement for expressions: exhaustive (include all possibilities)
A special pattern: _ (underscore): it does not match anything, does not bind to a variable, and is usually used for the last arm of a match, or to ignore certain values.
Conditional if let expressions
The if let expression is mainly a concise way to 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 exhaustiveness, for example:
12345678910111213141516171819 | 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 loops
- As long as the pattern continues to match, it allows the while loop to keep running.
12345678910 | 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.
123456 | 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
12 | let a = 5;let (x,y,z) = (1,2,3); |
Function parameters
- Function parameters can also be patterns
1234567 | 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
- The two forms of patterns: refutable and irrefutable
- A pattern that can match any possible value is irrefutable, for example
let x = 4; - A pattern that cannot match some possible values is refutable, for example
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
1234 | fn main() { let a: Option<i32> = Some(5); let Some(x) = a;} |
output
12345678 | 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
Modify
123456 | 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 must be irrefutable because it needs to match all remaining cases
Matching literals
123456789 | fn main(){ let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), }} |
Matching named variables
- A named variable is an irrefutable pattern that matches any value
12345678910 | 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);} |
Here, in the second arm of the matchy is a new variablethat exists in the scope of that arm
Matching a mutable reference
Using patterns&mut VWhen matching a mutable reference, note that if the patternnotWrite&mut(for example, directly usingvalue), Rust’s match ergonomics will automatically make the binding a mutable reference; if you explicitly write&mut V, thenVit will be the destructured value (which triggers a move).
123456789 | fn main() { let mut v = String::from("hello,"); let r = &mut v; match r { // match ergonomics: value is automatically bound as &mut String value => value.push_str(" world!") }} |
Multiple patterns
- In a match expression, using the | syntax (meaning ‘or’) can match multiple patterns.
12345678 | 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.
12345678910111213 | 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"), }} |
Numbers or characters are both acceptable.
Destructuring to decompose values.
- You can use patterns to destructure structs, enums, and tuples, thereby referencing different parts of these types’ values.
Destructuring assignment
123456 | fn main() { let (x, y); (x,..) = (3, 4); [.., y] = [1, 2]; assert_eq!([x,y],[3,2]);} |
Destructuring tuples
1234567 | fn main() { let (mut x, y) = (1, 2); x += 2; assert_eq!(x, 3); assert_eq!(y, 2);} |
Destructuring structs
12345678910111213141516171819202122 | 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),//Requires y to be 0. Point {x:0,y} => println!("On the y axis at {}",y),//Requires x to be 0. Point {x,y} => println!("On neither axis:({},{})",x,y), }} |
Destructuring enums
Note: The following code triggers a copy or move.
1234567891011121314151617181920212223 | 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:
1234567891011121314151617181920212223242526 | 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:
123456789101112131415161718 | 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.
1234567891011121314151617181920212223 | 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
12345678910111213141516171819202122 | 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
1234567 | 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 entire values or parts of values in patterns:
_ ignores the entire value
123456 | fn foo(_:i32,y:i32){ println!("y is {}",y);}fn main(){ foo(3, 4);} |
Use nested _ to ignore part of a value.
1234567891011121314151617181920 | 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); } }} |
Use names starting with _ to ignore unused variables.
12345678 | 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, pattern matching moves ownership of s to_s, accessing s later will cause an error.
123456789101112131415161718 | 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 |
Use _
1234567 | fn main(){ let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{:?}",s);} |
_, no binding occurs, no ownership is moved.
… (ignore the rest of the value)
123456789101112131415161718 | 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) } }} |
Need to add a comma,
12345678910 | 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); } }} |
Use match guards to provide additional conditions.
- A match guard is an additional if condition after the match arm pattern; it must also be satisfied for the pattern to match.
- Match guards are suitable for more complex scenarios.
123456789101112131415161718192021222324252627 | 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, it does not bind 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 lets us create a variable that holds a value while testing whether that value matches a pattern.
It is equivalent to an equals sign.
123456789101112131415161718 | 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); } }} |
12345678910111213141516 | 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), }} |
Application scenarios:
The following code will cause an error.
1234567891011121314151617 | 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.
1234567891011121314151617 | 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, ithas no enforced memory safety guarantees.: unsafe Rust (unsafe Rust)
Same as regular Rust, but provides extra superpowers.
- Reasons for Unsafe Rust’s existence:
- Static analysis is conservative; using unsafe Rust is like telling the compiler: I know what I’m doing, and I’ll take the corresponding risks.
- Computer hardware is inherently unsafe; Rust needs to be able to do low-level system 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):
- Dereference raw pointers.
- Calling an unsafe function or method
- Accessing or modifying a mutable static variable
- Implementing an unsafe trait
- Note:
unsafe blockdoes not turn off the borrow checker or disable other safety checks
Any memory safety-related errors must remain inside unsafe blocks
Isolate unsafe code as much as possible, preferably encapsulating it in a safe abstraction that provides a safe API
Dereference raw pointers.
- Raw pointers
*Mutable: mut T
Immutable: const T, meaningAfter the pointer is dereferenced, it cannot be directly assigned to*
Note: The * here is not a dereference operator; it is part of the type name
- Unlike references, raw pointers:
- Allow ignoring the borrowing rules by having both mutable and immutable pointers, or mutable pointers to the same location
- Cannot guarantee that they point to valid memory
- Allowed to be null
- Do not implement any automatic cleanup
- Give up guaranteed safety in exchange for better performance or the ability to interface with other languages or hardware
1234567 | 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;} |
You can create raw pointers in safe code blocks, but you cannot dereference them
12345678910111213141516 | 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
- Building safe abstractions that the borrow checker cannot understand
Calling an unsafe function or method
- Unsafe functions or methods: prefix the definition with the unsafe keyword.
- Before calling, you must manually satisfy certain conditions (mainly by reading the documentation), because Rust cannot verify these conditions.
- It must be called within an unsafe block.
123456 | unsafe fn dangerous(){}fn main(){ unsafe{ dangerous(); }} |
Creating safe abstractions around unsafe code.
- A function containing unsafe code does not mean the entire function needs to be marked unsafe.
- Wrapping unsafe code in a safe function is a common abstraction.
1234567891011121314 | 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
1234567891011121314 | 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
12345678910111213141516171819 | 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
- 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 other programming languages can call.
- Functions declared in an extern block are always unsafe to call from Rust code. Because other languages do not enforce Rust’s rules and Rust cannot check them, ensuring safety is the programmer’s responsibility.
12345678 | 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 you want to call.}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; 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.
- Before Add the extern keyword before fn and specify the ABI.
- Also need toAdd the #[no_mangle] attribute: prevents Rust from changing its name at compile time.
12345678 | 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; using extern does not require unsafe.}fn main(){} |
Accessing or modifying a mutable static variable
- Rust supports global variables, but the ownership mechanism can cause certain problems, such as data races.
- In Rust, global variables are called static variables.
12345 | static HELLO_WORLD: &str = "Hello, world!";fn main() { println!("name is: {}", HELLO_WORLD);} |
- Static variables are similar to constants.
- Usually static variable names use SCREAMING_SNAKE_CASE notation.
- Static variables can only store references with the 'static lifetime, which means the Rust compiler can figure out their lifetime on its own without needing explicit annotations.
- AccessImmutable static variablesare safe.
Differences between static variables and constants:
- The value in a static variablehas a fixed memory addressUsing this value will always access the same address. Constants, however, allow their data to becopied whenever they are used.。
- Static variables can be mutable.Accessing and modifying mutable static variables is unsafe.
123456789101112131415 | 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 mutable data that is globally accessible makes it difficult to guarantee the absence of data races, which is why Rust considers mutable static variables unsafe. Whenever possible, prefer smart pointers, so that the compiler can detect whether data access between different threads is safe.
Implementing unsafe traits
- A trait is unsafe when at least one of its methods contains an invariant that the compiler cannot verify.
- You can declare a trait as unsafe by adding the unsafe keyword before the trait, and implementations of the trait must also be marked as unsafe.
123456789 | 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 it can be safely sent across threads or accessed between threads, so we need to check it ourselves and indicate it with unsafe.
Accessing fields of a union
The last operation that is only applicable to unsafe is accessing union fields. union is similar to struct, but only one declared field can be used at a time in an instance. Unions are mainly used to interact with unions in C code. Accessing union fields is unsafe because **Rust cannot guarantee the type of data currently stored in the union instance.**For more information about unions, see (https://doc.rust-lang.org/reference/items/unions.html).
When to Use Unsafe Code
Using unsafe to perform one of these five operations (superpowers) is fine, and doesn’t even require much deliberation, 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 to Specify Placeholder Types in Trait Definitions
- An associated type is a type placeholder in a trait that can be used in the trait’s method signatures:
You can define a trait that includes certain types without needing to know what those types are before implementing it.
1234567 | pub trait Iterator { type Item; fn next(&mut self)->Option<Self::Item>;}fn main(){ println!("Hello World");} |
Differences Between Associated Types and Generics
| Generics | Associated Types |
|---|---|
| Annotate the type each time you implement the trait | No need to annotate types |
| Can implement a trait multiple times for a type (with different generic parameters) | Cannot implement a trait multiple times for a single type |
1234567891011121314151617181920212223242526272829303132333435363738 | 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 it for String a second time will cause 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:
1234 | 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 to implement this trait.
Example: Using Associated Types:
123456789101112131415161718192021222324252627282930313233343536373839404142 | struct Container(i32, i32);// Re-implement the following traits 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));} |
Implementation
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 | 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 parameter when using generics.
- Syntax: <PlaceholderType=ConcreteType>
- This technique is commonly used for 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.
12345678910111213141516171819202122 | use std::ops::Add;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 we useAddthe default generic parameter of the traitRhs = Self(i.e., the right operand type defaults to Self)
123456789101112 | 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+(rhs.0*1000)) }}fn main(){} |
Here the generic parameter is specified.
Main use cases of default generic parameters
- Extending a type without breaking existing code.
- Allowing customization in specific scenarios that most users do not need.
Fully Qualified Syntax
1234567891011121314151617181920212223242526272829 | 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();//Call the method on itself. Pilot::fly(&person);//Call the method from the Pilot trait. Wizard::fly(&person);//Call the method from the Wizard trait.} |
The parameterless form.
1234567891011121314151617181920 | 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.
- Fully qualified syntax:
::function(receiver_if_method,netx_arg,…) ; - Can be used anywhere you call a function or method.
- Allows ignoring parts that can be inferred from other context.
- This syntax is only needed when Rust cannot distinguish which specific implementation you expect to call.
1234567891011121314151617181920 | 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());} |
Use supertraits to require a trait to include the functionality of other traits.
- Need to use the functionality of other traits within a trait.
- The dependent trait also needs to be implemented.
- The trait that is indirectly depended upon is the supertrait of the current trait.
123456789101112131415161718192021222324252627 | 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 implement a trait for a type only if 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)
123456789101112 | 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 with one another and indicate the units of a value.
- Provide abstraction for certain details of a type.
- Hide internal implementation details through a lightweight wrapper.
Using type aliases to create type synonyms.
- Rust provides type aliases: — create another name (synonym) for an existing type — not an independent type — use the
typekeyword. - Main purpose: reduce code repetition.
- Similar to C’s typedef.
123456789 | 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
12345678910111213141516171819202122 | 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
12345678910111213 | 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 named
!:- It has no values, and is referred to as the empty type in jargon.
- We tend to call it the never type because it serves as the return type for functions that never return.
Functions that never return are also called diverging functions.
123456789101112131415 | 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, }; }} |
The match expression requires all branches to return the same type, and continue returns the never type, which can be safely coerced to the type corresponding to num.
12345678 | 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.
Dynamically Sized Types and the Sized Trait
Rust needs to determine at compile time how much space to allocate for a value of a particular 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 a string can only be determined at runtime.
The following code does not work:
12 | let s1:str = "Hello there";let s2:str = "How is it going"; |
Because they are all the same type, the space required should be the same, but the common space is not determined at declaration time.
Solution: use the &str string slice type.
Rust’s general way of using dynamically sized types.
- Attach some extra metadata to store the size of the dynamic information.
- When using a dynamically sized type, its value is always placed behind some kind of pointer.
Another dynamically sized type: trait.
- each **Every trait is a dynamically sized type.**It can be referenced by 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 after )
- For example, &dyn Trait or Box
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.
12345678 | 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 constraint
123 | 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 pointer
- You can pass functions to other functions
- Functions are coerced to the fn type when passed
- The fn type is function pointer
12345678910 | 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);} |
Differences between function pointers and closures
fn is a type, not a trait
- You can directly specify fn as a 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, prefer writing functions with generics that use closure traits: this accepts both closures and regular functions
In some situations, you only want to accept fn and not closures
- Interacting with external code that doesn’t support closures: C functions
123456789101112131415161718192021 | 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, so you cannot return a closure directly from a function. You can return a concrete type that implements the trait
1234567891011 | // 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(){} |
Macro
Reference:
Official documentation:
Rust Language Bible
The Little Book of Rust Macros
Asynchronous Programming
Reference:
Asynchronous Programming in Rust
Rust Language Bible
Formatting output
Positional argument
123456 | 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 argument
1234567891011 | 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.
12345678910 | 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, and filled with a specified character.
12345678910111213 | 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.
12345678910 | 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
1234567891011 | 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
123456789 | 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
123456789101112 | 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!")} |
Capture values from the environment
1234567891011121314151617181920212223 | 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
123456789101112 | 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 that marks paths that the program should not enter. If the program enters these paths, the program will panic and return the error message “‘internal error: entered unreachable code’”.
123456789101112131415 | 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.
12345678910 | // --- 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- if
T: 'staticthenTmust be immutable - if
T: '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:
123 | 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.
12345678910111213141516 | // 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.
1234567 | use rand;// generate random 'static str refs at run-timefn 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:
12345678910111213141516171819202122232425262728 | 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”if
T: 'staticthenTcan be a borrowed type with a'staticlifetime or an owned typesince
1T: 'staticincludes owned types that means
1T- 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
