Make a backup
Rust
case
Guess number
use std::io; use rand::Rng; use std::cmp::Ordering; fn main() { let mut guess = String::new(); let secret_number = rand::thread_rng().gen_range(1, 101); // 1-100 println!("{}", secret_number); loop { io::stdin().read_line(&mut guess).expect("cant read line"); let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; match guess.cmp(&secret_number) { Ordering::Less => println!("Too small"), Ordering::Greater => println!("Too big"), Ordering::Equal => { println!("You win"); break; }, } } }
variable
const MAX_P: u32 = 100_000; fn main() { let x = 5; let x = x ** 5; println!("{}", x); let spaces = " "; let spaces = spaces.len(); println!("{}", spaces); }
Tuple
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); let (x, y, z) = tup; println!("{} {} {}", x, y, z); println!("{} {} {}", tup.0, tup.1, tup.2); }
array
fn main() { let a = [1, 2, 3, 4, 5]; let months = [ "one", "two", ]; // Array type [type; length] let b: [i32; 5] = [1, 2, 3, 4, 5]; // If every element in the array is the same // Equivalent to let c = [3,3,3,3,3]; let c = [3; 5]; let one = months[0]; let two = months[1]; }
function
fn main() { another_function(); other_function(5); let y = { let x = 1; x + 1 }; let five = five(); } fn another_function() { println!("the value is 5"); } fn other_function(x: i32) { println!("the value is {}", x); } fn five() -> i32 { 5 }
Conditional judgment
fn main() { let number = 5; if number < 5 { println!("less than 5"); } else if number > 5 { println!("bigger than 5"); } else { println!("equal 5"); } // This is because the if statement is an expression let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {}", number); }
loop
fn main() { loop { println!("hello world"); } let mut count = 0; let result = loop { count += 1; if count == 10 { break count * 2; } }; println!("The result is {}", result); let mut number = 3; while number != 0 { println!("{}", number); number -= 1; } // Returns an iterative pointer to an array element let a = [1, 2, 3, 4, 5]; for element in a.iter() { println!("The value is {}", element); } // Left closed right open // rev() can reverse the range for number in (1 .. 4).rev() { println!("The value is {}", number); } }
ownership
- Ownership is born to manage data in heap
- Problems solved by ownership
- Tracks which parts of the code are using the data in the heap
- Minimize the amount of duplicate data on the heap
- Clean up unused data on the heap to avoid running out of space
Ownership rules, memory and allocation
String
- String literals cannot be modified
- The value of String type can be modified
Memory and allocation
-
The literal value of the string will be known at compile time, and its text content can be directly hard coded into the final executable file
- Fast and efficient
-
String is saved in heap
-
In Rust, when a value goes out of the scope, the memory will be automatically returned to the operating system immediately (by automatically calling the drop() function)
fn main() { let s1 = String::from("Hello"); let s2 = s1; // So s1 failed let s3 = s2.clone(); // copy // There is no move on the stack let x = 5; let y = x; }
Own copy trail
- Any simple scalar combination type can be Copy
- Any that needs to allocate memory or some kind of resource is not Copy
- Some types with copy trail
- All integer types
- bool
- char
- All floating point types
- Tuple, if all its fields are Copy
Ownership and function
- Semantically, passing values to functions is similar to assigning values to variables
- Passing a value to a function moves or copies it
- Return value and scope
- The transfer of ownership will also occur when the function returns a value
- Ownership of a variable always follows the same pattern
- Movement occurs when a value is assigned to another variable
- When a variable containing Heap data leaves the scope, its value will be cleaned up by the drop function unless the ownership of the data moves to another variable
fn main() { let s = String::from("Hello World"); take_ownership(s); let x = 5; makes_copy(x); println!("x: {}", x); } fn take_ownership(some_thing: String) { println!("{}", some_thing); } fn makes_copy(some_number: i32) { println!("{}", some_number); }
fn main() { let s1 = gives_ownership(); let s2 = String::from("hello"); let s3 = takes_and_gives_back(s2); } fn gives_ownership() -> String { let some_string = String::from("hello"); some_string } fn takes_and_gives_back(a_string: String) -> String { a_string }
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); (s, length) }
Reference and borrowing
-
After borrowing a variable as an immutable reference, you cannot borrow a variable as a variable reference
-
A reference allows a value to be used without taking ownership of it
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG plprjxna-1639451769486) (/ users / chenghaijie / library / Application Support / typera user images / image-20211203224800474. PNG)]
-
You can't modify borrowed things
-
Like variables, references are immutable by default, but can be modified when a variable reference is created
-
Within a specific scope, there can only be one variable reference to a piece of data
- This has the advantage of preventing data contention at compile time
fn main() { let s1 = String::from("Hello"); let len = calculate_length(&s1); println!("The length of {} is {}", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
fn main() { let mut s1 = String::from("Hello"); let len = calculate_length(&mut s1); println!("The length of {} is {}", s1, len); } fn calculate_length(s: &mut String) -> usize { s.push_str(" world"); s.len() }
section
-
String literals are slices
-
Slices are data types that do not hold ownership
fn main() { let mut s = String::from("Hello World"); let worldIndex = first_world(&s); s.clear(); println!("{}", worldIndex); } fn first_world(s: &String) -> usize { let bytes = s.as_bytes(); // enumerate wraps the iteration results into primitives for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() }
-
A string slice is a reference to a part of a string (closed on the left and open on the right)
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-yzrkcz00-1639451769487) (/ users / chenghaijie / library / Application Support / typora user images / image-20211204085727364. PNG)]
fn main() { let s = String::from("Hello World"); let hello = &s[0..5]; let world = &s[6..]; let whole = &s[0..s.len()]; }
-
String literals are immutable references
-
Experienced developers usually set the function parameters and return value to & STR, because it can accept different types of parameters
fn main() { let mut s = String::from("Hello World"); let worldIndex = first_world(&s[..]); let my_s = "Hello World"; let worldIndex = first_world(my_s); } fn first_world(s: &str) -> &str { let bytes = s.as_bytes(); // enumerate wraps the iteration results into primitives for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[0..] }
-
Other types of slices
fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; }
Define and instantiate Struct
- To assign a new value to a structure field, the structure variable must be variable
- Once the struct instance is mutable, all fields in the instance are mutable
- struct can be used as the return value of the function, and the name is the structure name
- Shorthand can be used when the field name and field value are equal
- struct supports update syntax
- Define a tuple like struct
struct User { username: String, email: String, sign_in_count: u64, active: bool, } fn main() { let mut user = User { username: String::from("Hello World"), email: String::from("123@qq.com"), sign_in_count: 12, active: true, }; user.email = String::from("456@qq.com"); // Abbreviation let mut user2 = User { username, email, sign_in_count: 23, active: false, }; // Update syntax let user2 = User { username, ..user }; // Truple Struct struct Color(i32, i32, i32); struct Point(u32, u32, u32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0); } fn build_user() -> User { User { username: String::from("Hello World"), email: String::from("123@qq.com"), sign_in_count: 12, active: true, } }
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect = Rectangle { width: 30, height: 20, }; // {:#?} The structure will be clearer println!("{:?}", rect); } fn area(dim: (u32, u32)) -> u32 { dim.0 * dim.1 } fn _area_(rect: &Rectangle) -> u32 { rect.width * rect.height }
struct method
- Differences between methods and functions
- Methods are defined in the context of struct or enum, trait
- The first parameter is self, which represents the struct instance in which the method is called
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size, } } } fn main() { let rect = Rectangle { width: 30, height: 20, }; let rect1 = Rectangle::square(20); println!("{}", rect.area()); }
enumeration
- Enumeration type is a custom data type
enum IpAddrKind{ V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } fn main() { let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), }; }
- T rust supports attaching data directly to enumerated variants
enum IpAddrKind { V4(u8, u8, u8, u8), V6(String) } fn main() { let home = IpAddrKind::V4(127, 0, 0, 1); let loopback = IpAddrKind::V6(String::from("::1")); }
- Define methods for enumerations
enum IpAddrKind { V4(u8, u8, u8, u8), V6(String) } impl IpAddrKind { fn call(&self) {} } fn main() { let home = IpAddrKind::V4(127, 0, 0, 1); let loopback = IpAddrKind::V6(String::from("::1")); home.call(); }
Option enumeration
-
In Prelude (pre import module), so it can be used directly
-
Describes a situation where a value may or may not exist (of a certain type)
-
Rust is not NULL
-
Definitions in the standard library
enum Option<T> { Some(T), None, }
-
Option is better than NULL in that:
- These are two different types. You can'T directly regard Option as T
- If you want to use T in Option, you must convert it
fn main() { let some_number = Some(5); let somt_string = Some(String::from("hello world")); let absent_number: Option<i32> = None; }
match operator
enum Coin { Penny, Dime, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, _ => 0, } } fn main() { }
- match must do everything possible
enum Coin { Penny, Dime, Quarter(i32) } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Quarter(value) => value, _ => 0, } } fn main() { let c = Coin::Quarter(5); println!("{}", value_in_cents(c)); }
if let
- Only care about one match and ignore other matches
- Of course, it can be used with else
fn main() { let v = Some(0u8); if let Some(3) = v { println!("thrwee"); } else { println!("others"); } }
Code organization
- Modular system
- Package
- Crite (unit package)
- Module
- Path
- By default, all entries (functions, methods, structures, enumerations, modules, constants) of t rust are private by default, because the internal details are expected to be hidden during the design of trust
- The parent module cannot access private entries in the child module
- Entries in all ancestor modules can be used in sub modules
- If it is located at the file root level, it can be called directly, whether public or private
- The super keyword is similar to
- pub struct
- The structure is public
- Structure fields are private
- The structure field needs to be set to pub separately to become public
- pub enum
- Enumerations are public
- Fields are also public
- use keyword
- Use this keyword to import the path into the scope, but still abide by the privacy rules
Vector
- Vec is called vector
- Provided by standard library
- Multiple values can be stored
- Only the same type of data can be stored
- Values are stored continuously in memory
fn main() { let v: Vec<i32> = Vec::new(); let vv = vec![1, 2, 3]; }
fn main() { let mut v: Vec<i32> = Vec::new(); v.push(1); v.push(2); v.push(3); let mut vv = vec![1, 2, 3]; vv.push(4); }
- Read elements in Vector
- Indexes
- get method
fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("{}", third); match v.ge(2) { Some(third) => println!("{}", third), None => println!("None"), } }
Vector + enum
-
Different types of data are stored in vec
enum Speed { Int(i32), Text(String), } fn main() { let row = vec![ Speed::Int(12), Speed::Text(String::from("hello")), ]; }
String
- The string in t rust is a collection of bytes
- Using utf8 encoding
fn main() { let str1 = "hello".to_string(); let str2 = String::from("hello"); }
fn main() { let mut s = String::from("foo"); let s1 = String::from("bar"); s.push_str(&s1); // Added is a string slice s.push('l'); }
fn main() { let s1 = "hello".to_string(); let s2 = " world".to_string(); // s1 ownership has been moved and therefore lapsed let s3 = s1 + &s2; }
fn main() { let s1 = "hello".to_string(); let s2 = " world".to_string(); // He will not take ownership of any parameters let s3 = format!("{}-{}", s1, s2); }
- The string in t rust does not support index syntax to access
- String is the encapsulation of Vec
fn main() { let len = String::from("iot").len(); }
- T rust has three ways to look at strings
- Bytes bytes
- Scalar Values scalar values
- Grapheme Clusters, which is the most consistent with the concept of string in our cognition
Bytes:
fn main() { let w = "Hello"; for b in w.bytes() { println!("{}", b); } }
Variable value:
fn main() { let w = "Hello"; for b in w.chars() { println!("{}", b); } }
- Cut String
- It must be used with caution
- If the character boundary is crossed during cutting, the program will panic
- Method of traversing String
- For scalar values, use the chars() method
- For bytes, use the bytes() method
- For font assembly, it is very complex, and the standard library does not provide it
HashMap
- HashMap is used less often, so it is not in Prelude
- The standard library supports it less, and there is no built-in macro to create HashMap
- Data village is clustered on heap
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert("blue".to_string(), 10); scores.insert("gree".to_string(), 20); let score = scores.get("blue"); match score { Some(s) => println!("{}", s), None => println!("team not exists"), } }
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert("blue".to_string(), 10); scores.insert("gree".to_string(), 20); // Reference is used because we usually continue to use HashMap after traversal for (k, v) in &scores { println!("{}: {}", k, v); } }
Update HashMap
-
k already exists
-
Replace existing v
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert("blue".to_string(), 10); scores.insert("blue".to_string(), 20); }
-
Keep the existing V and ignore the new v
use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert("blue".to_string(), 10); // Insert only if it does not exist scores.entry("blue".to_string()).or_insert(50); }
-
Merge existing v and new v
-
-
k does not exist
Count the number of occurrences of words
use std::collections::HashMap; fn main() { let text = "hello world hi world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } println!("{:?}", map); }
Unrecoverable error in panic
- Wrong classification
- recoverable
- For example, if the file is not found, try again
- Unrecoverable
- Bug: for example, the index accessed is out of range
- recoverable
- T rust has no exception mechanism similar to java
- Recoverable error
- Result<T, E>
- Unrecoverable
- Panic! macro
- Recoverable error
The Result enumeration applies to recoverable errors
-
Enum Result<T, E> { Ok(T), Err(E), }
-
Result is pre imported
Common matching error
use std::fs::File; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => { panic!("Error: {:?}", error) } }; }
-
Unwrap()
-
If the Result is Ok, return the value in Ok
-
If the result is Err, call panic! macro
-
This is a shortcut for match
-
use std::fs::File; fn main() { let f = File::open("hello.txt").unwrap(); }
-
-
Expect()
-
Similar to unwrap(), but with error messages
-
use std::fs::File; fn main() { let f = File::open("hello.txt").expect("File open error"); }
-
-
Propagation error
-
Handling errors in functions
-
Returns the error to the caller
-
Traditional ways of spreading errors
use std::fs::File; use std::io::Read; use std::io; fn main() { let result = read_username_from_file(); } fn read_username_from_file() -> Result<String, io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s){ Ok(_) => Ok(s), Err(e) => Err(e), } }
-
? Simplify the operation of propagating errors
use std::fs::File; use std::io::Read; use std::io; fn main() { let result = read_username_from_file(); } fn read_username_from_file() -> Result<String, io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s); }
or
use std::fs::File; use std::io::Read; use std::io; fn main() { let result = read_username_from_file(); } fn read_username_from_file() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s); Ok(s); }
? Operator can only be used with functions that return Result
-
When to use panic
In fact, an explicit call to panic will execute an unrecoverable error
-
If you can be sure that the Result is OK, you can use unwrap directly
use std::net::IpAddr; fn main() { let home: IpAddr = "127.0.0.1".parse().unwrap(); }
fn main() { let guess = "32"; let guess: i32 = match guess.trim().parse() { Ok(num) => num, Err(_) => 0, }; if guess < 1 || guess > 100 { println!("yes"); } }
Extraction function
-
Original low level code
fn main() { let num_list = vec![34, 50, 25, 100, 65]; let mut largest = num_list[0]; for num in num_list { if num > largest { largest = num; } } println!("The largest num is {}", largest); }
-
After extracting the function
fn main() { let num_list = vec![34, 50, 25, 100, 65]; println!("{}", largest(&num_list)); } fn largest(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest }
generic paradigm
-
Improve code reuse capability
The code you write is not the final code, but a template with some "placeholders" "
in a word
Generics are similar to macro definitions
-
Structs use generics
struct Point<T> { x: T, y: T, } struct PPoint<T, U> { x: T, y: U, } fn main() { let integer = Point {x: 5, y: 10}; let integer = PPoint {x: 5, y: 9.1}; }
-
More complex structures use generics
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } // These functions are available only for i32 impl Point<i32> { fn x1(&self) -> &i32 { &self.x } } fn main() { let integer = Point {x: 5, y: 10}; }
Trait
- Definition of Trait: put method signatures together to define a set of behaviors necessary to achieve a certain purpose
- Only method signature, no specific implementation
- Each method ends with a semicolon
- The type implementing the trait must provide a specific method implementation
- In fact, it is similar to the concept of interface
pub trait Summary { fn summarize(self) -> String; // If you write it in trait, you don't have to implement it when you implement trait fn _summarize(self) -> String { "hello".to_string() } } pub struct NewArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewArticle { fn summarize(self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } fn main() { }
// This parameter must implement the Summary trait. This is a simple case fn notify1(item: impl Summary) { println!("{}", item._summarize()) } // Complex, using generics fn notify2<T: Summary>(item: T) { println!("{}", item._summarize()) } // Implementation of multiple trait s fn notify3(item: impl Summary + Dis) { println!("{}", item._summarize()) } fn notify4<T: Summary + Dis>(item: T) { println!("{}", item._summarize()) } // Use the where clause to make it look neat fn notify5<T, U>(a: T, b: U) -> String where T: Summary + Dis, U: Summary, { format!("hello{}", a.summarize()) }
life cycle
-
In most cases, the life cycle is implicit and can be inferred
-
When referenced lifecycles may be related to each other in different ways, you need to label the lifecycles manually
-
Meaning of life cycle existence: avoid hanging references
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-mxwsiog8-1639451769488) (/ users / chenghaijie / library / Application Support / typera user images / image-20211208121129662. PNG)]
-
T rust implements lifecycle checking through the lifecycle checker
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-afkcqiv0-1639451769489) (/ users / chenghaijie / library / Application Support / typora user images / image-20211208121429097. PNG)]
-
This is not allowed because it is not known whether the return value comes from x or y
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-ogusowim-1639451769490) (/ users / chenghaijie / library / Application Support / typera user images / image-20211208121613886. PNG)]
-
This still doesn't reflect whether the returned type has been released
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-muu2rf18-1639451769491) (/ users / chenghaijie / library / Application Support / typera user images / image-20211208121735144. PNG)]
-
That's it, 'a takes the one with the shortest life cycle
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-wdbwi410-1639451769492) (/ users / chenghaijie / library / Application Support / typera user images / image-20211208122342800. PNG)]
-
In a structure, this means that the field must survive longer than the structure
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-jxrmojdu-1639451769493) (/ users / chenghaijie / library / Application Support / typera user images / image-20211208122608753. PNG)]
-
We know:
- Each reference has a lifecycle
- You need to specify lifecycle parameters for functions or struct s that use lifecycles
Small project
use std::{env, fs, process}; fn main() { // collect returns a collection let args: Vec<String> = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { println!("Msg: {}", err); process::exit(1); }); let contents = fs::read_to_string(config.filename).expect("Error to do this work"); println!("With text:\n{}", contents); } struct Config { query: String, filename: String, } impl Config { 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(); Ok(Config { query, filename }) } }
Using environment variables
use std::env; fn main() { // Judge whether it exists. Of course, it returns result < > let var = env::var("CASE").is_err(); println!("{}", var); }
closure
Creating abstract behaviors using closures
-
Closure:
You can capture anonymous functions in their environment
-
Closure:
- Is an anonymous function
- Save as variable, as parameter
- You can create closures in one place, and then invoke closures in another context to complete operations.
- Values can be captured from the scope they define
fn main() { let exp = |num| { num }; let add = |a: i32, b: i32| -> i32 { a + b }; let v4 = |a: i32, b: i32| a + b; }
iterator
-
He is an inert production consumer model
-
Create a custom iterator in one step:
Just implement the next() method
struct Counter { count: u32 } impl Counter { fn new() -> Counter { Counter { count: 0 } } } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { if self.count < 5 { self.count += 1; Some(self.count) } else { None } } } fn main() { let mut counter = Counter::new(); for i in vec![1, 2, 3, 4, 5] { println!("{}", counter.next().unwrap()) } }
-
In fact, the collect() method turns iterators into collections
Smart pointer
Smart pointers are such data structures - Behavior is similar to pointers - Additional metadata and functionality
Smart pointers are usually implemented using struct, and two trait s, Deref and Drop, are implemented
Deref trait: allows instances of smart pointer struct to be used as references
Drop trait: allows you to customize the code when the smart pointer instance goes out of scope
- Common smart pointers in standard libraries
- Box: allocate values on heap memory
- Rc: reference count type with multiple ownership enabled
- Ref and RefMut: accessed through RefCell: force borrowing of rule types at run time rather than compile time
Use the Box to point to the data on the heap
-
In fact, when rust allocates space for enumeration types, it is similar to the union of c language (in fact, this is how rust determines the space size of non recursive types)
-
T rust cannot calculate the size of recursive types. It uses box to avoid self-contained types, but it can access box values (from direct storage to pointer pointing storage)
-
This is the simplest smart pointer
- Allows you to store data on heap instead of stack
- On the Stack is a pointer to the heap data
- No performance overhead
- There are no additional features
- Derek trait and Drop trait are implemented (this is why smart pointers are called)
-
Common scenarios
- At compile time, the size of a type cannot be determined. However, when using this type, the context needs to know its exact size
- When you have a lot of data, you want to transfer ownership, but you need to ensure that the data will not be copied during operation
- When using a value, you only care whether it implements a specific trait, not its specific type
-
Using Box to store data on heap
fn main() { let b = Box::new(5); println!("b = {}", b); }
-
Enable recursive types using Box
- At compile time, Rust needs to know the space occupied by a type
- The size of recursive types cannot be determined at compile time
- And Box is certain
Deref Trait
- With this, smart pointers can be handled like regular references
- Box is defined as a tuple struct with one element
Smart pointer example
-
String and Vec
-
Each has a memory area and allows users to operate on it
-
It also has metadata (such as capacity)
-
Enable recursive types using Box
This is because the size of the recursive type cannot be determined at compile time For recursive types, Box There is a way to solve this problem
Box
-
summary
-
Only indirect storage and heap memory allocation are provided
-
There is no additional performance overhead
-
Applicable to scenes that require "indirect" storage, such as Cons List
use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); } enum List { Cons(i32, Box<List>), Nil, }
-
-
Derek Trait
-
Implementing deref trait allows us to customize the behavior of the dereference operator *
-
By implementing deref trait, smart pointers can be handled like regular references
fn main() { let x = 5; let y = &x; assert_eq!(5, x); // Dereference assert_eq!(5, *y); println!("Hello, world!"); }
It is implemented with Box
fn main() { let x = 5; let y = Box::new(5); assert_eq!(5, x); // Dereference assert_eq!(5, *y); println!("Hello, world!"); }
-
-
Define your own smart pointer
-
Box is defined as a tuple struct with one element
-
The deref trait in the standard library requires us to implement a deref method
-
This method borrows self
-
Returns a reference to internal data
-
Implementation example
use std::ops::Deref; struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
-
-
-
Implicit dereference transformation of functions and methods (Deref Coercion)
-
This is a convenient feature for functions and methods
principle
-
Deref Coercion can convert the reference of T into the reference generated by T after deref operation
-
sketch
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 happen automatically
-
The compiler makes a series of calls to deref to convert it to the required parameter type
-
This is done at compile time without additional performance overhead
-
example
use std::ops::Deref; fn hello(name: &str) { println!("Hello: {}", name); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); hello("Rust"); hello(&m); } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } }
-
-
-
-
-
Dereference and variability
-
The * operator of variable references can be overloaded with DerefMut trait
-
When type and trait occur in the following three cases, Rust will execute deref coercion
- When t: deref < target = u >, allow & T to convert to & U
- When t: derefmut < target = u >, allow & mut t to be converted to & mut U
- When t: deref < target = u >, allow & mut t to convert to & U
Just understand these things, and you will naturally know in the future. This is a great philosophy
-
Drop Trait
-
Any type can implement drop trait
-
drop trait only requires you to implement the drop method
- Parameters of this method: only a variable reference to self
-
Drop trail pre import module (Prelude)
-
Implementing drop trail allows us to customize the action that occurs when the value is about to leave the scope
- For example, network connection, resource release, etc
-
example
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!("CustomSmartPointer created"); }
-
Use to disable the automatic drop function directly. It's not necessary
- This is because the purpose of drop trail is to automatically release processing logic
-
Rust does not allow you to manually call the drop method of Drop trait, that is
c.drop()
Is not allowed
-
In fact, this trait is similar to a destructor
-
But you can call the std::mem::drop function of the standard library to invoke the drop value in advance.
This function is located in the pre import module, so it does not need to be referenced manually
Of course, this will not lead to double release
example
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-9emjlo0d-1639451769494) (/ users / chenghaijie / library / Application Support / typera user images / image-20211210231520329. PNG)]
Rc
Reference count smart pointer
-
Sometimes, a value corresponds to multiple owners, such as in a graph data structure
Illustration
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-uxxxtzef-1639451769494) (/ users / chenghaijie / library / Application Support / typera user images / image-20211210231934499. PNG)]
For example, when releasing a node, the central node is multi pointed
RC = reference counting
This thing will maintain a counter internally to judge that the value release is still referenced. If the counter is 0, it indicates that it can be released
Safe release
-
Usage scenario
- This thing is only suitable for single threaded scenarios
- Data needs to be allocated on the heap, which is read by multiple parts of the program (read-only), but it is impossible to determine which part will use up the data at compile time
-
Note that this is not in the pre import module
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-bmifizj9-1639451769495) (/ users / chenghaijie / library / Application Support / typera user images / image-20211210232522918. PNG)]
-
example
Two lists share ownership of the other list
Figure [external link image transfer fails, and the source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-es5ojkoy-1639451769497) (/ users / chenghaijie / library / Application Support / typera user images / image-20211210232625328. PNG)]
Note that this thing is set to read-only through immutable references
Well, I don't know why the code is wrong
enum List { Cons(i32, Rc<List>), Nil } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Cons(5, Rc::new(Cons(10, Rc::new(Nil)))); let b = Cons(3, Rc::clone(&a)); let c = Cons(3, Rc::new(&a)); }
alas
pattern
-
classification
-
Refutable: a pattern that matches any value that may be passed
let x = 5;
-
Irrefutable: a pattern that cannot be matched for some possible values
if let Some(x) = a_value
-
Function parameters, let statements, and for loops only accept irrefutable patterns
-
ordinary
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 = {:?}", x); }
-
Multiple matching
fn main() { let x = 5; let y = 10; match x { 1 | 2=> println!("Got 50"), Some(y) => println!("Matched, y = {:?}", y), _ => println!("Default case, x = {:?}", x) } println!("at the end: x = {:?}", x); }
fn main() { let x = 5; match x { 1..=5 => println!("one enough five"), _ => println!("something else") } let x = 'c'; match x { 'a'..='j' => println!("early ascii letter"), 'k'..='z' => println!("late ascii letter"), _ => println!("something else") } }
-
Deconstruction
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); let Point { x, y } = p; assert_eq!(0, a); assert_eq!(7, b); }
-
Useful deconstruction
struct Point { x: i32, y: i32 } fn main() { let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: 10 }); }
-
Better pattern matching
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { // .. It is placed in the middle and used to ignore some values (first, .., last) => { println!("Some numbers: {} {}", first, last) } } }
-
Using match guard to provide additional conditions is actually an additional if condition after match arm mode, which is suitable for more complex scenes
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 = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"), } }
This will save that value
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), _ => println!("else lalala") } }
-
Unsafe Rust
- Rust hides the second language, it meiyo
- Mandatory memory security
- It is the same as normal t rust, but it provides super power
- Existing reasons
- Static analysis is conservative
- Use unsafe Trust: tell the compiler that I know what I'm doing and take the corresponding risks
- The computer hardware itself is not secure, and t rust needs to be able to program the system
- Static analysis is conservative
Unsafe superpower
-
Use the unsafe keyword to switch to unsafe trust and open a block containing unsafe code
-
Four actions that can be executed in unsafe trust (unsafe trust) super ability
- Dereference original pointer
- Calling unsafe functions and methods
- Accessing and modifying variable static variables
- Implement unsafe trait
-
be careful
- unsafe does not turn off borrowing checks or disable other security checks
- Any memory security related errors must stay in the unsafe block
- Isolate unsafe code as much as possible, and it is best to encapsulate it in a secure abstraction to provide a secure api
-
Dereference original pointer
-
Raw pointer
- Variable: * mut T
- Immutable: * const T, which means that the pointer cannot be directly assigned after dereference
- Note that * here is not a dereference symbol, it is part of the type name
-
Unlike references, raw pointers
- Allows you to ignore borrowing rules by having both immutable and variable pointers or multiple variable pointers pointing to the same location
- There is no guarantee that it can point to reasonable memory
- null allowed
- No automatic cleaning is achieved
- Allows you to ignore borrowing rules by having both immutable and variable pointers or multiple variable pointers pointing to the same location
-
Give up the guaranteed security in exchange for better performance / the ability to interface with other languages or hardware
-
example
fn main() { let mut num = 5; // Pointer to determine validity let r1 = &num as *const i32; let r2 = &mut num as *mut i32; // The original pointer must be dereferenced in an unsafe block unsafe { println!("r1: {}", *r1); println!("r2: {}", *r2); } // Unable to determine valid pointer let address = 0x012345usize; let r = address as *const i32; // Even if there is an illegal access error, you have to deal with it yourself unsafe { println!("r: {}", *r); } }
Why use raw pointers - And c Language interface - Construct a security abstraction that the borrowing checker cannot understand
-
Calling an unsafe function or method
-
Before calling, you need to meet some conditions manually (mainly by looking at the documents), because t rust cannot verify these conditions
-
It needs to be called in the unsafe block
-
example
unsafe fn dangerous() { } fn main() { unsafe { dangerous(); } }
-
-
Create a security abstraction for unsafe code
- Just because a function contains unsafe code does not mean that the entire function needs to be marked unsafe
- Wrapping unsafe code in secure functions is a common abstraction
-
Use the extern function to call external code
-
extern keyword: simplify and use the process of external function interface (FFI)
-
FFI: allows one programming language to define functions and other programming languages to call these functions
-
Any code in an extern block is unsafe
-
example
#[no_mangle] pub extern "C" fn call() { println!("hello world") } fn main() { }
-
-
-
Access or modify a variable static variable
-
In t rust, global variables are called static variables
-
And constant types
static HELLO_WORLD: &str = "hello, world"; fn main() { println!("{}", HELLO_WORLD) }
-
-
Implement unsafe trait
-
When at least one method in a trait has an unsafe factor that cannot be verified by the compiler, it is said that the trait is unsafe
-
Life unsafe trait: add the unsafe keyword before the definition
- This trait can only be implemented in unsafe code blocks
-
example
unsafe trait Foo { } unsafe impl Foo for i32 { } fn main() { }
-
-
When to use unsafe code
- The compiler cannot guarantee memory security. It is not easy to ensure that unsafe code is correct
- You can do this when you have good reason to use unsafe code
- By explicitly marking unsafe, you can easily locate problems when they occur
Advanced trait
-
Use the association type in the trait definition to specify the placeholder type
-
An association type is a type placeholder in a trait that can be used in a trait method signature
-
You can define trait s that contain certain types without knowing what they are before implementation
-
In fact, it feels a bit like generics
pub trait Iterator { type Item; fn text(&mut self) -> Option<Self::Item>; } fn main() { println!("hello world") }
-
-
The difference between association types and generics
generic paradigm Association type Label the type each time you implement trait No dimension type is required A trait can be implemented multiple times for a type (through different type parameters) A trait cannot be implemented more than once for a single type
-
-
Default generic parameters and operator overloading
-
You can specify a default concrete type for a generic when using generic parameters
- Syntax: < placeholdertype = concretetype >
-
This technique is often used for operator overloading (although t rust does not allow you to create your own operators and overload any operators)
-
However, you can overload some of the corresponding operators by implementing the trait s listed in std::ops
-
Example: overloaded addition operator
use std::ops::Add; #[derive(Debug, PartialEq)] struct Point { x: i32, y: i32 } impl Add for Point { type Output = Point; fn add(self, rhs: Self) -> Self::Output { Point { x: self.x + rhs.x, y: self.y + rhs.y } } } fn main() { assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 } ) }
-
Fully qualified syntax, how to call a method with the same name
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your caption 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(); Pilot::fly(&person); Wizard::fly(&person); }
-
Fully qualified syntax
::function(receiver_if_method, next_arg, ...)
-
Can be used anywhere a function or method is called
-
Allows you to ignore parts that can be inferred from other contexts
-
This syntax is needed when t rust cannot distinguish which specific implementation you expect to call, because it is cumbersome to write
<Human as Pilot>::fly(&Human);
-
-
-
Implementing type safety and abstraction using the new type pattern
-
Create type synonyms using type aliases
-
Produce another name (synonym) for an existing type
-
Is not a separate type
-
Use the type keyword
-
Main purpose: reduce code character repetition
type Killmeters = i32; fn main() { let x: i32 = 5; let y: Killmeters = 5; println!("{}", x + y) }
-
-
-
never type
- There is one named! Special types of
- It has no value and is called empty type in jargon
- We prefer to call it never type because it acts as a return type in functions that do not return
- A function that does not return a value is also called a divergence function
- There is one named! Special types of
-
Dynamic size and Sized Trait
-
T rust needs to determine at compile time how much space is allocated for a particular type of value
-
There is the concept of dynamic size type (DST) in trust
- When writing code, use values that can only be sized at run time
-
For example, str is a type of dynamic size (note not & str): only the runtime can determine the length of the string
-
The following code does not work
let s1: str = "hello"; let s2: str = "world...";
-
Use & STR to solve this problem. In fact, it saves
- str address
- Length of str
-
-
A common way for t rust to use dynamic size types
- Additional metadata is attached to store the size of the dynamic information
- Like & STR, when using a dynamic size type, its value is always placed behind some kind of pointer
- Additional metadata is attached to store the size of the dynamic information
-
In fact, trait is also a type of dynamic size
-
Advanced functions and closures
-
You can pass functions to other functions
-
The function is cast to fn type during passing
-
fn type is called function pointer
-
Transfer function pointer instance
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!("{}", answer) }
-
The difference between function pointers and closures
fn closure -
Return closure from function
fn return_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) } fn main() { }
macro
macro
- A macro in t rust refers to a collection of related features
- Using macro_rules! Built declaration macro
- Three process macros
- Custom (#[derive]) macros for struct and enum, for which you can specify the code added with the derive attribute
- A macro similar to an attribute that adds a custom attribute to any entry
- A macro similar to a function looks like a function call and operates on the token specified as a parameter
- Differences between functions and macros
- In essence, macros are used to write code that can generate other code (metaprogramming)
- When defining a signature, a function must declare the number and type of parameters. Macros can handle variable parameters
- The definition of macros is much more complex than functions. Macros must be defined in advance or introduced into the current scope
- Functions can be defined anywhere and called anywhere