Timeline
Timeline
2025-09-11
init
2025-10-19
modify some format errors
2025-10-22
move something to another post
This article introduces the basics of the Rust language, covering installation and debugging, the use of the Cargo project management tool, as well as core concepts such as variables and mutability, constants, and shadowing. The article details Rust's scalar data types, including integers, floating-point numbers, booleans, and characters, and discusses integer overflow and how to handle it. Through this article, readers can grasp the essential syntax and type system points of Rust.
Installation and Debugging
Install
On Linux
123 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh# Follow the prompts to installrustc --version |
Debugging
Debugging with rust-gdb on Linux
1 | rust-gdb target/debug/your_program |
Cargo
Creating a Project
cargo new project_name
Help information
12345678910111213141516171819202122232425 | Create a new cargo package at <path>Usage: cargo.exe new [OPTIONS] <path>Arguments: <path>Options: -q, --quiet Do not print cargo log messages --registry <REGISTRY> Registry to use --vcs <VCS> Initialize a new repository for the given version control system (git, hg, pijul, or fossil) or do not initialize any version control at all (none), overriding a global configuration. [possible values: git, hg, pijul, fossil, none] --bin Use a binary (application) template [default] -v, --verbose... Use verbose output (-vv very verbose/build.rs output) --lib Use a library template --color <WHEN> Coloring: auto, always, never --edition <YEAR> Edition to set for the crate generated [possible values: 2015, 2018, 2021] --frozen Require Cargo.lock and cache are up to date --name <NAME> Set the resulting package name, defaults to the directory name --locked Require Cargo.lock is up to date --offline Run without accessing the network --config <KEY=VALUE> Override a configuration value -Z <FLAG> Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details -h, --help Print help informationRun `cargo help new` for more detailed information. |
Cargo.toml
TOML (Tom’s Obvious, Minimal Language) format, which is Cargo’s configuration format.
123456789 | [package]#Section heading, indicating that the following is used to configure the package.name = "hello" #Project nameversion = "0.1.0" #Project versionauthors = ["cauchy <731005515@qq.com>"] #Authoredition = "2021" #The Rust version used# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]#Dependencies |
In Rust, a package of code is calledcrate
Building a Cargo project
1 | cargo build |
Creates the executable target/debug/hello_cargo or target\debug\hello_cargo.exe(Windows)
Run .\target\debug\hello_cargo.exe
The first run generates the cargo.lock file
This file is responsible for tracking the exact versions of project dependencies; there is no need to manually modify this file.
Building and running a Cargo project
1 | cargo run |
If it has been compiled before and the code has not been modified, it will execute directly.
cargo check
1 | cargo check |
Checks the code to ensure it compiles, but does not produce any executable file.
cargo check is much faster than cargo build
Release build
1 | cargo build --release |
It optimizes during compilation, so the code runs faster but compilation takes longer.
It generates the executable in target/release instead of target/debug.
Variables and Mutability
variable
Declaring and Usingletkeyword
By default, variables are immutable.
When declaring a variable, prefix it withmutthe keyword, and the variable becomes mutable.
let mut x = 3;
Constant
Similar to immutable variables,Constants are values bound to a name that are not allowed to change, but there are still some differences between constants and variables.
You cannot use mut with constants. Constants are not just immutable by default; they are always immutable.
Declare constants with the const keyword instead of let, and mustannotate the type of the value.。
3.Constants can be declared in any scope, including the global scope,
- The last difference is that constants can only be set toa constant expression, andand cannot be any other value that can only be computed at runtime.。
12 | const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;const MAX_POINTS: u32 = 100_000; |
Naming convention: all uppercase, separated by underscores.
Shadowing
123456789101112 | fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("The value of x in the inner scope is: {x}"); } println!("The value of x is: {x}");} |
We can define a new variable with the same name as a previous variable,A new variable shadows a previously declared variable with the same name.
Shadowing and marking a variable as mutare different.
- If you don’t use the let keyword, assigning to a non-mut variable causes a compile-time error.
- A new variable with the same name declared using let is also immutable.
- A new variable with the same name declared using let,its type can be different from before.
Allowing unused variables
Two ways
1234567 | fn main() { let _x = 1;}fn main() { let x = 1;} |
Data Type
Rust isa statically typed languagethe types of all variables must be known at compile time.
Scalar types
Rust has four basic scalar types:Integer types、Floating-point types、Boolean typeandCharacter type
Integer types
If we don’t explicitly give a variable a type, the compiler will automatically infer one for us.
12345678 | fn main(){ let x = 5; assert_eq!("i32".to_string(),type_of(&x));}// The following function can get the type of the passed-in parameter and return the type as a string.fn type_of<T>(_: &T) -> String{ format!("{}",std::any::type_name::<T>())} |
If an integer is not given a type, it defaults to i32.
123 | fn main() { let v: u16 = 38_u8 as u16;} |
| Length | signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
The isize and usize types depend on the computer architecture on which the program is running: on 64-bit architectures they are 64-bit, and on 32-bit architectures they are 32-bit.
1234 | fn main() { assert_eq!(i8::MAX, 127); assert_eq!(u8::MAX, 255);} |
Integer literals
| Numeric literals | Example |
|---|---|
| Decimal | 98_222 |
| Hex | 0xff |
| Octal | 0o77 |
| Binary | 0b1111_0000 |
| Byte (single-byte character) (u8 only) | b’A’ |
The default numeric type in Rust is i32. isize or usize are mainly used as indices for some collections.
Integer overflow
For example, there is a u8, which can store values from 0 to 255. So what happens when you change it to 256? This is called “integer overflow” (integer overflow ), which leads to one of the following two behaviors. WhenWhen compiling in debug mode, Rust checks for such problems and causes the program to panic, a term Rust uses to indicate that the program exits due to an error.
**In release builds, Rust does not detect overflow; instead, it performs an operation known as two’s complement wrapping.**In short, values larger than the maximum value this type can hold wrap around to the minimum value, so 256 becomes 0, 257 becomes 1, and so on. Relying on integer wrapping is considered an error, even though this behavior may occur. If you really need this behavior, the standard library has a type that explicitly provides this functionality, Wrapping. To explicitly handle the possibility of overflow, you can use the following methods provided by the standard library on native numeric types:
- Available in all modes wrapping_ methods for wrapping*, such as wrapping_add
- if checked_ methods*If overflow occurs, returns None
- Use overflowing_ methods*Returns a value and a boolean indicating whether overflow occurred
- Use saturating_ methods*Saturates at the minimum or maximum value
123456 | // Handling errors in code and `panic`fn main() { let v1 = 251_u8 + 8; let v2 = i8::checked_add(251, 8).unwrap(); println!("{},{}",v1,v2);} |
Modify
1234567891011 | fn main() { let v1 = 247_u8 + 8; let v2 = i8::checked_add(119, 8).unwrap(); println!("{},{}",v1,v2); }fn main() { let v1 = 251_u16 + 8; let v2 = u16::checked_add(251, 8).unwrap(); println!("{},{}",v1,v2); } |
Floating-point types
Rust’s floating-point types are f32 and f64, which take up 32 bits and 64 bits respectively. The default type is f64 because on modern CPUs it is almost the same speed as f32, but with higher precision. All floating-point types are signed.
12345 | fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32} |
Floating-point numbers are represented using the IEEE-754 standard. f32 is a single-precision floating-point number, and f64 is a double-precision floating-point number.
12345 | fn main() { let x = 1_000.000_1; // f64 let y: f32 = 0.12; // f32 let z = 0.01_f64; // f64} |
Numeric operations
All numeric types in Rust support basic mathematical operations: addition, subtraction, multiplication, division, and remainder. Integer division willtruncate toward zero(discarding the fractional part)
123456 | fn main() { assert_eq!(0.1+0.2,0.3);//Error }thread 'main' panicked at 'assertion failed: `(left == right)` left: `0.30000000000000004`, right: `0.3`', src\main.rs:5:5 |
Two ways to modify
123456 | fn main() { assert!(0.1+0.2>=0.3);}fn main() { assert!(0.1_f32+0.2_f32==0.3_f32);} |
Calculate
12345678910111213141516171819202122232425262728293031323334 | use std::fmt::Display;fn print_something<T >(something:T)where T : Display{ println!("{}",something);}fn main() { // Integer addition print_something(1u32 + 2 ); // Integer subtraction print_something(1i32 - 2 ); print_something(1i8 - 2); print_something(3 * 50 ); print_something(9 / 3 == 3); // error! Fix it to make the code work print_something(24 % 5 ); // Logical AND, OR, NOT operations print_something(true && false ); print_something(true || false ); print_something(!true ); // Bit manipulation println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101); println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101); println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101); println!("1 << 5 is {}", 1u32 << 5); println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);} |
Sequence
1234567891011121314151617 | fn main() { let mut sum = 0; for i in -3..2 { println!("i is {}",i);//-3 to 1, not including 2 } for c in 'a'..='z' { println!("{}",c);//a-z, including z }}// Handling errors in code and `panic`use std::ops::{Range, RangeInclusive};fn main() { assert_eq!((1..5), Range{ start: 1, end: 5 }); assert_eq!((1..=5), RangeInclusive::new(1, 5));} |
Boolean type
The boolean type in Rust is represented by bool
123456789101112 | fn main() { let t = true; let f: bool = false; // with explicit type annotation}fn main() { let f = true; let t = true && false || true;//Boolean operations assert_eq!(t, f); println!("Success!")} |
Character type
Rust’s char type is the most native character type in the language.
12345678910111213 | fn main() { let c = 'z'; let z: char = 'ℤ'; // with explicit type annotation let heart_eyed_cat = '😻';}fn main() { let c1 = '中'; print_char(c1);}fn print_char(c : char) { println!("{}", c);} |
Char literals are declared with single quotes, whereas string literals are declared with double quotes. The size of Rust’s char type isfour bytes(four bytes) and represents a Unicode Scalar Value, which means it can represent much more than ASCII. In Rust, accented letters, characters such as Chinese, Japanese, and Korean, emoji, and zero-width whitespace characters are all valid char values. Unicode scalar values include values from U+0000 to U+D7FF and U+E000 to U+10FFFF. However, “character” is not a concept in Unicode, so the intuitive “character” may not correspond to Rust’s char.
Size
123456789101112 | use std::mem::size_of_val;fn main() { let c1 = 'a'; assert_eq!(size_of_val(&c1),4); //One character is 4 bytes let c2 = '中'; assert_eq!(size_of_val(&c2),4); println!("Success!")} |
Unit type
123456789101112 | fn main() { let _v: () = (); let v = (2, 3); assert_eq!(_v, implicitly_ret_unit()); println!("Success!")}fn implicitly_ret_unit() { println!("I will return a ()")} |
The memory occupied by the unit type is 0!!!
1234567 | use std::mem::size_of_val;fn main() { let unit: () = (); assert!(size_of_val(&unit) == 0); println!("Success!")} |
Compound types
Compound types(Compound types) can combine multiple values into one type. Rust has two primitive compound types: tuples and arrays.
Tuple type
TupleFixed length: once declared, its length cannot grow or shrink
Use a comma-separated list of values inside parentheses to create a tuple. Each position in the tuple has a type, and the types of these different valuesdo not have to be the same
123 | fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1);} |
The tup variable binds to the entire tuple, because a tuple is a single compound element. To get individual values from a tuple, you can usepattern matching(pattern matching) todestructuring(destructure) tuple values, like this:
1234567891011121314151617 | fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {y}");}fn main() { let (x, y, z); // Fill in the blanks (y,z,x) = (1, 2, 3); assert_eq!(x, 3); assert_eq!(y, 1); assert_eq!(z, 2);} |
The program first creates a tuple and binds it to the tup variable. Then it uses let and a pattern to split tup into three different variables, x, y, and z. This is called destructuring(destructuring), because it splits a tuple into three parts.
You can alsouse a dot (.) followed by the index of the value to directly access them. The first index of a tuple is 0. For example:
123456789 | fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1; let one = x.2;} |
A tuple without any values has a special name, called unit tuple. This value and its corresponding type are both written as (), representing an empty value or an empty return type. If an expression returns no other value, it implicitly returns the unit value.
Tuples that are too long cannot be printed
12345678910 | // Fix the code errorfn main() { let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); println!("too long tuple: {:?}", too_long_tuple);}//Fixfn main() { let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); println!("too long tuple: {:?}", too_long_tuple);} |
Array type
Unlike tuples, every element in an array must have the same type. Arrays in Rust are different from arrays in some other languages; arrays in Rust have a fixed length.
The type of an array is [T; Length],The length of an array is part of its type signature, so the length of an array must be known at compile time.
The vector type is an array-like collection type provided by the standard library that allows growing and shrinking in length. When you’re unsure whether to use an array or a vector, you should probably use a vector.
You can write the type of an array like this: include the type of each element in square brackets, followed by a semicolon, and then the number of elements in the array.
let a: [i32; 5] = [1, 2, 3, 4, 5];
Here, i32 is the type of each element. After the semicolon, the number 5 indicates that the array contains five elements.
123456789 | fn main() { // In many cases, we can omit part or all of the array's type and let the compiler help us infer it. let arr0 = [1, 2, 3]; let arr: [char; 3] = ['a', 'b', 'c']; // Arrays are allocated on the stack, `std::mem::size_of_val` the function returns the entire memory space occupied by the array. // Each char element in the array occupies 4 bytes of memory space because in Rust, char is a Unicode character. assert!(std::mem::size_of_val(&arr) == 12);} |
You can also create one by specifying an initial value in square brackets, followed by a semicolon, and then the number of elements,an array where every element has the same value.:
1 | let a = [3; 5]; |
The array named a will contain 5 elements, and the values of these elements will initially all be set to 3. This syntax is equivalent to let a = [3, 3, 3, 3, 3]; but is more concise.
Accessing Array Elements
An array is a single memory block of known, fixed size that can be allocated on the stack. You can use indexing to access elements of an array, like this:
123456789101112131415 | fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1];}fn main() { let names = [String::from("Sunfei"), "Sunface".to_string()]; // `get` Return `Option<T>` type, so its use is very safe let name0 = names.get(0).unwrap(); // However, subscript indexing carries the risk of going out of bounds. let _name1 = &names[1];} |
Invalid Array Access
If we access an element past the end of the array, the program, when an invalid value is used in an index operation, causes Runtime error. The program exits with an error message. Whenattempting to access an element with an index, Rust checks whether the specified index is less than the length of the array. If the index exceeds the array length, Rust will panic, which is a Rust term used for situations where the program exits because of an error.
This check must be performed at runtime, especially in certain cases, because the compiler cannot possibly know what value the user will input when running the code later.
Type Conversion
Using as for Basic Type Conversion
- Rust doesnot provide implicit type conversion (coercion) for primitive types, but we can use
asfor explicit conversion.
1234567891011121314 | fn main() { let decimal = 97.123_f32; let integer: u8 = decimal as u8; let c1: char = decimal as u8 as char; let c2 = integer as char; println!("c1 is {}",c1); assert_eq!(integer, 'b' as u8 - 1); println!("Success!")} |
2.By default, integer literal overflow is detected at compile time and reported as an error., but we can add a line of global annotation #![allow(overflowing_literals)] to avoid compilation errors (overflow will still occur).
12345 | fn main() { assert_eq!(u8::MAX, 255); let v = 1000 as u8;} |
- When converting any numeric value to an unsigned integer type T, if the current value is not within the range of the new type, we canadd or subtract from the current value (increase or decrease by T::MAX + 1), until the new value is within the range of the new type. Suppose we want to convert 300 to u8. Since the maximum value of u8 is 255, 300 is not within the range of the new type and is greater than the maximum value of the new type, so we need to subtract T::MAX + 1, that is, 300 - 256 = 44.
12345678910111213141516171819202122232425262728 | fn main() { assert_eq!(1000 as u16, 1000); assert_eq!(1000 as u8, 232); // In fact, the rule mentioned earlier for positive integers is the following modulo operation println!("1000 mod 256 is : {}", 1000 % 256); assert_eq!(-1_i8 as u8, 255); // Since Rust 1.45, when a floating-point number exceeds the range of the target integer, the conversion will directly take the maximum or minimum value of the positive integer range. assert_eq!(300.1_f32 as u8, 255); assert_eq!(-100.1_f32 as u8, 0); // The above floating-point conversion has a slight performance cost. If you have extreme performance requirements for a certain piece of code, // you can consider the following methods, but the results of these methods may overflow and return meaningless values. // In short, use with caution. unsafe { // 300.0 is 44 println!("300.0 is {}", 300.0_f32.to_int_unchecked::<u8>()); // -100.0 as u8 is 156 println!("-100.0 as u8 is {}", (-100.0_f32).to_int_unchecked::<u8>()); // nan as u8 is 0 println!("nan as u8 is {}", f32::NAN.to_int_unchecked::<u8>()); }} |
- Raw pointers can be converted to and from integers representing memory addresses.
12345678910111213141516171819202122 | fn main() { let mut values: [i32; 2] = [1, 2]; let p1: *mut i32 = values.as_mut_ptr(); let first_address = p1 as usize; let second_address = first_address + 4; // 4 == std::mem::size_of::<i32>() let p2 = second_address as *mut i32; unsafe { *p2 += 1; } assert_eq!(values[1], 3); println!("Success!")}fn main() { let arr :[u64; 13] = [0; 13]; assert_eq!(std::mem::size_of_val(&arr), 8 * 13); let a: *const [u64] = &arr; let b = a as *const [u8]; unsafe { assert_eq!(std::mem::size_of_val(&*b), 13) }} |
From/Into
- The
Fromtrait allows a typeto define how to create itself based on another type., so it provides a very convenient way of type conversion. FromandIntoare paired. As long as we implement the former, the latter will be automatically implemented.: as long as you implementimpl Fromfor U, you can use the following two methods: let u: U = U::from(T)andlet u: U = T.into(). The former is provided by theFromtrait, while the latter is provided by the automatically implementedIntotrait.- Note that when using the
intomethod, you need to explicitly annotate the type, because the compiler is likely unable to infer the required type for us.
Example
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354 | fn main() { let my_str = "hello"; // The following three conversions all rely on the fact that `String` implements the `From<&str>` trait. let string1 = String::from(my_str); let string2 = my_str.to_string(); // Explicit type annotation is required here. let string3: String = my_str.into();}fn main() { // impl From<bool> for i32 let i1:i32 = false.into(); let i2:i32 = i32::from(false); assert_eq!(i1, i2); assert_eq!(i1, 0); // Fix the following errors in two ways. // 1. Which type implements the From trait: impl From<char> for ?, we can look at the documentation mentioned earlier to find the appropriate type // 2. A keyword introduced in the previous chapter let i3: i32 = 'a'.into(); // Use two methods to solve the error let s: String = 'a' as String; println!("Success!")}//The first methodfn main() { // impl From<bool> for i32 let i1:i32 = false.into(); let i2:i32 = i32::from(false); assert_eq!(i1, i2); assert_eq!(i1, 0); let i3:u32 = 'a'.into(); let s: String = 'a'.into(); println!("Success!")}//The second methodfn main() { // impl From<bool> for i32 let i1:i32 = false.into(); let i2:i32 = i32::from(false); assert_eq!(i1, i2); assert_eq!(i1, 0); let i3: u32 = 'a' as u32 ; let s: String = String::from('a');} |
Implement the From trait for a custom type
12345678910111213141516171819202122232425 | // From is included in `std::prelude` so we don't need to manually bring it into the current scope// use std::convert::From;struct Number { value: i32,}impl From<i32> for Number { // Implementation `from` Method fn from(item: i32) -> Self { Number { value: item } }}// Fill in the blanksfn main() { let num = Number::from(30); assert_eq!(num.value, 30); let num: Number = 30.into(); assert_eq!(num.value, 30); println!("Success!")} |
When doing error handling, implementing the From trait for our custom error type is very useful. This way we canautomatically convert an error type to our custom error type via ?
1234567891011121314151617181920212223242526272829303132 | use std::fs;use std::io;use std::num;enum CliError { IoError(io::Error), ParseError(num::ParseIntError),}impl From<io::Error> for CliError { fn from(error: io::Error) -> Self { CliError::IoError(error) }}impl From<num::ParseIntError> for CliError { fn from(error: num::ParseIntError) -> Self { CliError::ParseError(error) }}fn open_and_parse_file(file_name: &str) -> Result<i32, CliError> { // ? automatically converts io::Error to CliError let contents = fs::read_to_string(&file_name)?; // num::ParseIntError -> CliError let num: i32 = contents.trim().parse()?; Ok(num)}fn main() { println!("Success!")} |
TryFrom / TryInto
Similar to From and Into, TryFrom and TryInto are also generic traits for type conversion.
But unlike From/Into, TryFrom and TryInto can handle conversion failures and return a Result。
1234567891011121314151617 | fn main() { let n: i16 = 256; // The Into trait has a method`into`, // Therefore TryInto has a method ? let n: u8 = match n.try_into() { Ok(n) => n, Err(e) => { println!("there is an error when converting: {:?}, but we catch it", e.to_string()); 0 } }; assert_eq!(n, 0); println!("Success!")} |
Custom implementation
12345678910111213141516171819202122232425262728 | struct EvenNum(i32);impl TryFrom<i32> for EvenNum { type Error = (); // Implementation `try_from` fn try_from(value: i32) -> Result<Self, Self::Error> { if value % 2 == 0 { Ok(EvenNum(value)) } else { Err(()) } }}fn main() { assert_eq!(EvenNum::try_from(8), Ok(EvenNum(8))); assert_eq!(EvenNum::try_from(5), Err(())); // Fill in the blanks let result: Result<EvenNum, ()> = 8i32.try_into(); assert_eq!(result, Ok(EvenNum(8))); let result: Result<EvenNum, ()> = 5i32.try_into(); assert_eq!(result,Err(())); println!("Success!")} |
Other conversions
Convert any type to String
As long as a type implements ToString, any type can be converted to String. In fact, this approach is not the best; you can use the fmt::Display trait? It can control how a type is printed, and implementing it also automatically implements ToString. Because to_string is implemented based on fmt::Display.
1234567891011121314151617181920 | use std::fmt;struct Point { x: i32, y: i32,}impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "The point is ({}, {})", self.x, self.y) }}fn main() { let origin = Point { x: 0, y: 0 }; assert_eq!(origin.to_string(), "The point is (0, 0)"); assert_eq!(format!("{}", origin), "The point is (0, 0)"); println!("Success!")} |
Parsing String
Using the parse method, a String can be converted to an i32 number, because the standard library implements FromStr for i32: impl FromStr for i32
1234567891011 | // To use `from_str` method, you needs to introduce this trait into the current scope.use std::str::FromStr;fn main() { let parsed: i32 = "5".parse().unwrap(); let turbo_parsed = "10".parse::<i32>().unwrap(); let from_str = i32::from_str("20").unwrap(); let sum = parsed + turbo_parsed + from_str; assert_eq!(sum, 35); println!("Success!")} |
Custom implementation of the FromStr trait
12345678910111213141516171819202122232425262728 | use std::str::FromStr;use std::num::ParseIntError;struct Point { x: i32, y: i32}impl FromStr for Point { // Associated type: a type parameter defined in a trait and "bound" to that trait. type Err = ParseIntError; fn from_str(s: &str) -> Result<Self, Self::Err> { let coords: Vec<&str> = s.trim_matches(|p| p == '(' || p == ')' ) .split(',') .collect(); let x_fromstr = coords[0].parse::<i32>()?; let y_fromstr = coords[1].parse::<i32>()?; Ok(Point { x: x_fromstr, y: y_fromstr }) }}fn main() { let p = "(3,4)".parse::<Point>(); assert_eq!(p.unwrap(), Point{ x: 3, y: 4} )} |
transmute
std::mem::transmute is an unsafe function that caninterpret one type as another type bitwise, where the two typesmust have the same size。
transmute is equivalent to reinterpreting the bit pattern of one type as another type. It does not copy data; it simply interprets the bits of the source value directly as the target type, and then forgets the source value.
Because of this,transmuteit is very, very unsafe! The caller must ensure the safety of the code themselves; of course, this is also the purpose of unsafe.
example
- transmute can convert a pointer into a function pointer. This conversion is not portable because on different machines, function pointers and data pointers may have different sizes.
1234567891011 | fn foo() -> i32 { 0}fn main() { let pointer = foo as *const (); let function = unsafe { std::mem::transmute::<*const (), fn() -> i32>(pointer) }; assert_eq!(function(), 0);} |
- transmute can also extend or shorten the lifetime of an invariant, that is,an ‘illegal conversion’ of lifetimes!
1234567891011 | // R is a struct that wraps a reference. It does not store the i32 itself, but stores a reference to the i32.struct R<'a>(&'a i32);unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> { std::mem::transmute::<R<'b>, R<'static>>(r)}unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>) -> &'b mut R<'c> { std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r)} |
- In fact, we can also use some safe methods to replace transmute.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 | fn main() { /*Turning raw bytes(&[u8]) to u32, f64, etc.: */ let raw_bytes = [0x78, 0x56, 0x34, 0x12]; let num = unsafe { std::mem::transmute::<[u8; 4], u32>(raw_bytes) }; // use `u32::from_ne_bytes` instead // according to the native endianness let num = u32::from_ne_bytes(raw_bytes); // or use `u32::from_le_bytes` or `u32::from_be_bytes` to specify the endianness // little-endian let num = u32::from_le_bytes(raw_bytes); assert_eq!(num, 0x12345678); // big-endian let num = u32::from_be_bytes(raw_bytes); assert_eq!(num, 0x78563412); /*Turning a pointer into a usize: */ let ptr = &0; let ptr_num_transmute = unsafe { std::mem::transmute::<&i32, usize>(ptr) }; // Use an `as` cast instead let ptr_num_cast = ptr as *const i32 as usize; /*Turning an &mut T into an &mut U: */ let ptr = &mut 0; let val_transmuted = unsafe { std::mem::transmute::<&mut i32, &mut u32>(ptr) }; // ptr as *mut i32: convert &mut i32 to a raw pointer *mut i32. // as *mut u32: convert the raw pointer further to *mut u32. // &mut *(...): convert the raw pointer back to a mutable reference. let val_casts = unsafe { &mut *(ptr as *mut i32 as *mut u32) }; /*Turning an &str into a &[u8]: */ // this is not a good way to do this. let slice = unsafe { std::mem::transmute::<&str, &[u8]>("Rust") }; assert_eq!(slice, &[82, 117, 115, 116]); // You could use `str::as_bytes` let slice = "Rust".as_bytes(); assert_eq!(slice, &[82, 117, 115, 116]); // Or, just use a byte string, if you have control over the string // literal assert_eq!(b"Rust", &[82, 117, 115, 116]);} |
function
In Rust code,function and variable namesuse snake case conventional style. In snake case, all letters are lowercase and words are separated by underscores.
In Rust, we define functions by entering fn followed by the function name and a pair of parentheses. The curly braces tell the compiler where the function body begins and ends.
Parameters
We can define it as owning Parameters(parameters) of the function, parameters are special variables and are part of the function signature. When a function has parameters (formal parameters), concrete values can be provided for these parameters (actual arguments). Technically, these concrete values are called arguments (arguments)
1234567 | fn main() { another_function(5);}fn another_function(x: i32) { println!("The value of x is: {x}");} |
When defining multiple parameters, separate them with commas.
Statements and Expressions
The function body consists of a series of statements and an optional final expression.
statement(Statements)are instructions that perform some actions but do not return a value。
expression(Expressions)evaluate and produce a value。
Statements do not return values; expressions evaluate to a value.
1234567891011121314151617181920 | fn main() { let x = 5u32; let y = { let x_squared = x * x; let x_cube = x_squared * x; // The value of the following expression will be assigned to `y` x_cube + x_squared + x }; let z = { // The semicolon turns an expression into a statement, so what is returned is no longer an expression `2 * x` value, but rather the value of the statement `()` 2 * x; }; println!("x is {:?}", x); println!("y is {:?}", y); println!("z is {:?}", z);} |
In the statementlet y = 6;, the 6 is an expression that evaluates to the value 6.A function call is an expression。A macro call is an expression。A new block scope created with braces is also an expressionFor example:
12345678 | fn main() { let y = { let x = 3; x + 1 }; println!("The value of y is: {y}");} |
This expression:
1234 | { let x = 3; x + 1} |
is a code block, and its value is 4. There is no semicolon at the end of the expression. If you add a semicolon at the end of the expression, it becomes a statement, and statements do not return values.
Return value
Do not name the return value, but you must declare its type after the arrow (->).
In Rust, the return value of a function is equivalent to the value of the last expression in the function body. You can return early from a function using thereturnkeyword and a specified value; however, most functions implicitly return the last expression.
123456789 | fn main() { let x = plus_one(5); println!("The value of x is: {x}");}fn plus_one(x: i32) -> i32 { x + 1} |
The return type is ()
123456789 | fn main(){ println!("{}",type_of(&println!("helloworld")))}fn type_of<T>(_: &T) -> String{ format!("{}",std::any::type_name::<T>())}//output://helloworld//() |
The return type is never
123456789101112131415 | use std::thread;use std::time;fn never_return() -> ! { // implement this function, don't modify fn signatures loop { println!("I return nothing"); // sleeping for 1 second to avoid exhausting the cpu resource thread::sleep(time::Duration::from_secs(1)) }}fn main() { never_return();} |
Diverging function
Diverging functions do not return any value, so they can be used anywhere a return value is needed.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859 | fn main() { println!("Success!");}fn get_option(tp: u8) -> Option<i32> { match tp { 1 => { // TODO } _ => { // TODO } }; // Here, rather than returning a `None`, it is better to use a diverging function instead. never_return_fn()}// Implement the following diverging function using three methods.fn never_return_fn() -> ! {}fn main() { println!("Success!");}fn get_option(tp: u8) -> Option<i32> { match tp { 1 => { // TODO } _ => { // TODO } }; never_return_fn()}// IMPLEMENT this function// DON'T change any code elsefn never_return_fn() -> ! { unimplemented!()}// IMPLEMENT this function in THREE waysfn never_return_fn() -> ! { panic!()}// IMPLEMENT this function in THREE waysfn never_return_fn() -> ! { todo!();}// IMPLEMENT this function in THREE waysfn never_return_fn() -> ! { loop { std::thread::sleep(std::time::Duration::from_secs(1)) }} |
The difference betweenunimplemented!()andtodo!()is that while todo!()conveys an intent of implementing the functionality later and the message is “not yet implemented”,unimplemented!()makes no such claims. Its message is “not implemented”. Also some IDEs will marktodo!()s.
Call
123456789101112131415161718192021222324252627 | fn main() { get_option(3); println!("Success!");}fn get_option(tp: u8) -> Option<i32> { match tp { 1 => { // TODO } _ => { // TODO } }; never_return_fn()}// IMPLEMENT this function// DON'T change any code elsefn never_return_fn() -> ! { loop { std::thread::sleep(std::time::Duration::from_secs(1)) }} |
Usingunimplemented!()andtodo!()will report the following error:
thread ‘main’ panicked at ‘not implemented’, src\main.rs:24:5
12345678 | let _v = match b { true => 1, // Diverging functions can also be used in `match` expressions, to substitute for values of any type false => { println!("Success!"); panic!("we have no value for `false`, but we can panic") } }; |
Control flow
if expressions
123456789 | fn main() { let number = 3; if number < 5 { println!("condition was true"); } else { println!("condition was false"); }} |
The code blocks associated with conditions in an if expression are sometimes called arms.
if/else can be used as an expression for assignment.
12345678910111213141516 | fn main() { let n = 5; let big_n = if n < 10 && n > -10 { println!(" 数字太小,先增加 10 倍再说"); 10 * n } else { println!("数字太大,我们得让它减半"); n / 2 }; println!("{} -> {}", n, big_n);} |
Note
In the code,the condition must be bool value. If the condition is not a bool value, we will get an error. Rust does not attempt to automatically convert non-boolean values to boolean. You must always explicitly use a boolean value as the condition for if.
If more than one else if is used, it is best to use match to refactor the code.
Using if in a let statement
Because if is an expression, we can use it on the right side of a let statement.
123456 | fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}");} |
The possible return values of each branch of an if must be the same type.
Note
The expression in the if block returns an integer, while the expression in the else block returns a string. This won’t work because a variable must have only one type. Rust needs to know exactly the type of the variable at compile time.
Loop
loop
12345 | fn main() { loop { println!("again!"); }} |
Returning Values from Loops
12345678910111213 | fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}");} |
loop label
If there are nested loops, break and continue apply to the innermost loop at that point. You can optionally specify a loop label(loop label), and then use the label with break or continue, so that these keywords apply to the labeled loop instead of the innermost loop.
123456789101112131415161718192021 | fn main() { let mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {count}");} |
while
1234567891011 | fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!");} |
for
1234567 | fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); }} |
The for loop iterates over collection elements, which enhances code safety compared to a while loop, and eliminates bugs that may be caused by going beyond the end of an array or by not iterating enough and missing some elements.
For iterable objects that do not implement Copy, for in will take ownership.
12345678910111213141516 | fn main() { let names = [String::from("liming"),String::from("hanmeimei")]; for name in &names { // do something with name... } println!("{:?}", names); let numbers = [1, 2, 3]; // The elements in numbers implement Copy, so there is no need to transfer ownership. for n in numbers { // do something with name... } println!("{:?}", numbers);} |
Iterating over an array by index and value
12345678 | fn main() { let a = [4,3,2,1]; // Iterating over an array by index and value `a` for (i,v) in a.iter().enumerate() { println!("第{}个元素是{}",i+1,v); }} |
Range
It is a type provided by the standard library, used to generate a sequence of all numbers starting from one number and ending before another number. (The ending number is not included.)
The rev method can reverse a range.
123456 | fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!");} |
Ownership, References, and Borrowing
The Stack and the Heap
Both the stack and the heap are parts of memory available to your code to use at runtime, but they are structured in different ways. The stack stores values in the order it gets them and removes the values in the opposite order. This is also called last in, first out(last in, first out)。
Adding data is called pushing onto the stack(pushing onto the stack), and removing data is called popping off the stack(popping off the stack). All data on the stack must occupy a known and fixed size.
Data with an unknown size at compile time or a size that might change must be stored on the heap instead.. The heap is less organized: when you put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot somewhere on the heap that is big enough, marks it as in use, and returns a pointer that represents the address of that location pointer(pointer). This process is called allocating on the heap(allocating on the heap), sometimes simply called “allocating.” (Pushing values onto the stack is not considered allocating.)Because the pointer to the data on the heap is known and has a fixed size, you can store the pointer on the stackbut when you want the actual data, you must follow the pointer.
**Pushing to the stack is faster than allocating on the heap because the allocator does not have to search for a place to store new data; that location is always at the top of the stack.**In contrast, allocating on the heap requires more work because the allocator must first find a big enough space to hold the data and then perform bookkeeping to prepare for the next allocation.
Accessing data on the heap is slower than accessing data on the stack because you have to follow a pointer. Modern processors are faster if they jump around in memory less (caching). For the same reason, processors work better when the data they’re processing is close together (such as on the stack) rather than far apart (such as on the heap).
When your code calls a function, the values passed into the function (including, potentially, pointers to data on the heap) and the function’s local variables get pushed onto the stack. When the function is over, those values get popped off the stack.
Keeping track of what parts of code are using what data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap so you don’t run out of space are all problems that ownership addresses. Once you understand ownership, you won’t need to think about the stack and the heap very often, but knowing that the main purpose of ownership is to manage heap data can help explain why it works the way it does.
Ownership Rules
- Each value in Rust has an owner.
- There can be only one owner at a time.
- When the owner (variable) goes out of scope, the value will be dropped.
Example of ownership:
Modify the code below:
12345678910111213 | fn main() { let s = give_ownership(); println!("{}", s);}// Only modify the code below!fn give_ownership() -> String { let s = String::from("hello, world"); // convert String to Vec // Convert String to Vec type let _s = s.into_bytes();//into_bytes transfers ownership s} |
Method
123456789101112 | fn main() { let s = give_ownership(); println!("{}", s);}// Only modify the code below!fn give_ownership() -> String { let s = String::from("hello, world"); // convert String to Vec let _s = s.as_bytes();//as_bytes does not transfer ownership s} |
or
12345678910 | fn main() { let s = give_ownership(); println!("{}", s);}// Only modify the code below!fn give_ownership() -> String { let s = String::from("hello, world"); s} |
When ownership is transferred, mutability can also change accordingly.
1234567 | fn main() { let s = String::from("hello, "); let mut s1 = s; s1.push_str("world")} |
Variables and Scope
A scope is the range in which an item is valid in a program. Suppose we have a variable like this:
let s = “hello”;
The variable s is bound to a string literal, and this string value ishardcodedinto the program code. This variable is valid from the point of declaration until the current scope ends. The comments in Example 4-1 indicate where the variable s is valid.
12345 | { // s is not valid here; it is not yet declared let s = "hello"; // From this point on, s is valid // use s} // This scope has ended, s is no longer valid |
- When senters scope it is valid.
- This continues until it leaves scope .
str and &str
Under normal circumstances, we cannot use the str type, but we can use &str instead.
123 | fn main() { let s: &str = "hello, world";} |
If you want to use the str type, it can only be used with Box.
12345678 | fn main() { let s: Box<str> = "hello, world".into(); greetings(s)}fn greetings(s: Box<str>) { println!("{}",s)} |
& can be used to convert Box&Box<str>automatically convert to&str
12345678 | fn main() { let s: Box<str> = "hello, world".into(); greetings(&s)}fn greetings(s: &str) { println!("{}",s)} |
String type
String is a type defined in the standard library, allocated on the heap, and can grow dynamically. Its underlying storage is a dynamic byte array (Vec
The difference between Unicode and encoding methods
- Unicode
- is a character set, which specifies that each character corresponds to a unique code point
- Code point form:
U+0000~U+10FFFF- For example:
U+4F60→ ‘you’U+1F600→ 😀- UTF-8 / UTF-16 / UTF-32
- is to convert Unicode code points into byte sequence the specific method
- In other words, Unicode is a “character table”, and UTF-8 is the “encoding rule for how to store or transmit these characters”.
String.chars() and String.bytes() iterate over Unicode characters and bytes, respectively.
- The length of a Unicode character is not fixed.
- A Unicode character is not necessarily a fully displayed character.
In Unicode and text processing,Grapheme cluster is a user-perceived “character unit”, that is, it is a complete character seen by the user, but it may consist of multiple Unicode scalar values (char).
In simple terms:
- A grapheme cluster ≈ “a complete displayed character”
- Unlike Rust’s
char,charis a single Unicode scalar value (which may be a letter, a Chinese character, or a component of an emoji) - A grapheme cluster may contain:
- Base character + combining mark (such as a diacritic)
- emoji combination (such as the 👨👩👧👦 family emoji, consisting of multiple emoji and zero-width joiners)
To iterate over grapheme clusters, you need a third-party package, such as:
String manages data allocated on the heap, so it can store text whose size is unknown at compile time. You can create a String from a string literal using the from function.
1 | let s = String::from("hello"); |
OK Modifying such a string:
12345 | let mut s = String::from("hello"); s.push_str(", world!"); // push_str() appends a literal to the end of the string println!("{}", s); // will print `hello, world!` |
Memory and Allocation
As forstring literalswe know their contents at compile time, sothe text is directly hardcoded into the final executableThis makes string literals fast and efficient. However, these properties are all due to the immutability of string literals. Unfortunately, we cannot put a block of memory into the binary for every text whose size is unknown at compile time, and its size may also change as the program runs.
For the String type, in order to support a mutable, growable piece of text, it needs to allocate a block of memory on the heap whose size is unknown at compile time to store the contents. This means:
- It must request memory from the memory allocator at runtime.
- It needs a way to return the memory to the allocator when we are done with the String. (In some languages, this is garbage collection, GC.)
Rust takes a different strategy:**The memory is automatically freed after the variable that owns it goes out of scope.**Below is a version of the scope example from Listing 4-1 that uses a String instead of a string literal:
123456 | { let s = String::from("hello"); // From this point on, s is valid // use s } // This scope is now over, // s is no longer valid |
This is a very natural place to return the memory that String needs to the allocator: when s goes out of scope. When a variable goes out of scope, Rust calls a special function for us. This function is called drop, and here the author of String can place the code to free the memory. Rust automatically calls drop at the closing }.
Ways in which variables and data interact
move
Stack-only data
12 | let x = 5;let y = x; |
bind 5 to x; then make a copy of the value in x and bind it to y. Now there are two variables, x and y, both equal to 5.
Because integers are simple values with a known fixed size, these two 5s are placed on the stack.
For this type of data, there is no difference between moving and cloning.
12 | let s1 = String::from("hello");let s2 = s1; |
A String consists of three parts:
- a pointer to the memory that holds the contents of the string
- a length, len, which is the number of bytes required to store the string’s contents
- a capacity, capacity, which is the total number of bytes of memory the String has obtained from the operating system
The aboveare stored on the stack,, and the part that holds the string contents is stored on the heap.
When we assign s1 to s2, if the String’s data were copied, this would mean we copy its pointer, length, and capacity from the stack. We do not copy the heap data that the pointer refers to.
When the variables go out of scope, drop is called, causing a double free (repeated deallocation).
To ensure memory safety,
- Rust does not attempt to copy the allocated memory.
- Rust invalidates s1, so when the variable s1 goes out of scope, it does not need to free anything (corresponding to ownership rule 2: a value has one and only one owner at any given time).
12345678910111213141516171819202122232425262728 | let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1);warning: unused variable: `s2` --> src\main.rs:3:9 |3 | let s2 = s1; | ^^ help: if this is intentional, prefix it with an underscore: `_s2` | = note: `` on by defaulterror[E0382]: borrow of moved value: `s1` --> src\main.rs:5:28 |2 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait3 | let s2 = s1; | -- value moved here4 |5 | println!("{}, world!", s1); | ^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)For more information about this error, try `rustc --explain E0382`.warning: `loop_test` (bin "loop_test") generated 1 warningerror: could not compile `loop_test` due to previous error; 1 warning emitted |
Rust’s approach here is different from a shallow copy, because**While performing a shallow copy, the copied-from variable is invalidated.**Therefore, the new term ‘move’ is used.
Implicit design principle : Rust does not automatically create deep copies of data.
Because in terms of runtime performance, any automatic copy operation in Rust is cheap (only involving shallow copies of data on the stack).
Partial move
When destructuring a variable, you can use both move and reference pattern bindings at the same time. When you do this, a partial move occurs:Ownership of part of the variable is transferred to other variables, while for another part we obtain a reference to it.
In this case,**The original variable can no longer be used, but the part whose ownership was not transferred can still be used,**i.e., the part that was previously referenced.
1234567891011121314151617181920212223242526 | fn main() { struct Person { name: String, age: Box<u8>, } let person = Person { name: String::from("Alice"), age: Box::new(20), }; // Through this destructuring pattern matching, ownership of person.name is transferred to a new variable. `name` // But here, `age` the variable is a reference to person.age; using ref here is equivalent to: let age = &person.age let Person { name, ref age } = person; println!("The person's age is {}", age); println!("The person's name is {}", name); // Error! The reason is that part of person has already been moved, so we can no longer use it. //println!("The person struct is {:?}", person); // Although `person` As a whole, it can no longer be used, but `person.age` can still be used. println!("The person's age from person struct is {}", person.age);} |
clone
If you want to deep-copy data on the heap, not just data on the stack, you can use the clone method.
123456789101112 | let s1=String::from("Hello");let s2=s1.clone();println!("{},{}",s1,s2);fn main() { let t = (String::from("hello"), String::from("world")); let (s1, s2) = t.clone(); println!("{:?}, {:?}, {:?}", s1, s2, t); // -> "hello", "world", ("hello", "world")} |
copy
Copy traitIt can be used for types that are entirely on the stack, such as integers.
- If a type implements the Copy trait, then the old variable remains usable after assignment.
- If a type or part of that type implements the Drop trait, then Rust does not allow it to also implement the Copy trait.
Any simple scalar and its composite types are Copy.
Anything that requires allocating memory or some resource is not Copy.
Some types that implement the Copy trait:
- All integer types, such as u32.
- Boolean type, bool, whose values are true and false.
- All floating-point types, such as f64.
- Character type, char.
- Tuples, if and only if the types they contain also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not.
Ownership and Functions
Passing a value to a function is similar to assigning a value to a variable. Passing a value to a function may move or copy, just like an assignment statement.
1234567891011121314151617181920212223 | fn main() { let s = String::from("hello"); // s enters scope takes_ownership(s); // s's value moves into the function ... // ... and so is no longer valid here let x = 5; // x enters scope makes_copy(x); // x would move into the function, // but i32 is Copy, // so x can still be used afterward} // Here, x goes out of scope first, then s. But because s's value has been moved, // nothing special happens.fn takes_ownership(some_string: String) { // some_string enters scope println!("{}", some_string);} // Here, some_string goes out of scope and calls `drop` the method. // The memory it occupies is freed.fn makes_copy(some_integer: i32) { // some_integer enters scope println!("{}", some_integer);} // Here, some_integer goes out of scope. Nothing special happens. |
When you try to use s after calling takes_ownership, Rust throws a compile-time error.
Return Values and Scope
Return values can also transfer ownership.
1234567891011121314151617181920212223242526272829 | fn main() { let s1 = gives_ownership(); // gives_ownership returns a value // moved to s1 let s2 = String::from("hello"); // s2 enters scope let s3 = takes_and_gives_back(s2); // s2 is moved to // takes_and_in gives_back, // it also moves the return value to s3} // Here, s3 moves out of scope and is dropped. s2 also moves out of scope, but it has already been moved, // so nothing happens. s1 leaves scope and is dropped.fn gives_ownership() -> String { // gives_ownership will // move the return value to // the function that calls it let some_string = String::from("yours"); // some_string enters scope. some_string // returns some_string // and moves out to the calling function //}// takes_and_gives_back takes the passed-in string and returns that valuefn takes_and_gives_back(a_string: String) -> String { // a_string enters scope // a_string // returns a_string and moves out to the calling function} |
The ownership of variables always follows the same pattern:
- Move it when assigning the value to another variable.。
- when holdinga variable with a value on the heap leaves scopeits value will be cleaned up by drop unless the data has been moved to be owned by another variable.
If you want a function to obtain the value without taking ownership, you need to return the passed-in parameter.
12345678910111213 | fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len);}fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of the string. (s, length)} |
This is too cumbersome. Rust provides a feature for this that allows using a value without taking ownership, called Reference(references)。
References and Borrowing
1234567891011 | 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()} |
Reference(reference) is like a pointer, becauseit is an address, and we can access data stored at that address that belongs to other variables.
Unlike pointers,references ensure they point to a valid value of a particular type.
1234567 | fn main() { let x = 5; // Fill in the blank let p = &x; println!("x 的内存地址是 {:p}", p); // output: 0x16fa3ac84} |
Note: the operation opposite to using & to reference is dereferencing(dereferencing), which uses the dereference operator " * "
123 | let s1 = String::from("hello"); let len = calculate_length(&s1); |
The &s1 syntax lets us create a point to reference to the value s1, but does not own it. Because we do not own the value,when the reference stops being used, the value it points to will not be dropped.。
We call the behavior of creating a reference borrowing(borrowing)
Just as variables are immutable by default, so are references.References (by default) do not allow modifying the value they refer to.
Rust will automatically dereference in certain cases.
1234567 | fn main() { let mut s = String::from("hello, "); let p = &mut s; p.push_str("world");} |
Example:
12345678910111213141516 | fn main() { let mut s = String::from("hello, "); borrow_object(&s)}fn borrow_object(s: &String) {}fn main() { let mut s = String::from("hello, "); push_str(&mut s)}fn push_str(s: &mut String) { s.push_str("world")} |
mutable reference
123456789 | fn main() { let mut s = String::from("hello"); change(&mut s);}fn change(some_string: &mut String) { some_string.push_str(", world");} |
Mutable references have a big restriction:only one mutable reference can exist at a time
123456 | let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2); |
The benefit of this restriction is that Rust can avoid data races at compile time.data race(data race) is similar to a race condition, and it can be caused by these three behaviors:
- Two or more pointers access the same data at the same time.
- At least one pointer is used to write data.
- There is no mechanism for synchronizing data access.
You can use curly braces to create a new scope to allow having multiple mutable references, just not simultaneously have:
1234567 | let mut s = String::from("hello");{ let r1 = &mut s;} // r1 goes out of scope here, so we can definitely create a new referencelet r2 = &mut s; |
Another restriction:You cannot have a mutable reference and an immutable reference at the same time.
However,Multiple immutable references are allowed.
1234567 | let mut s = String::from("hello");let r1 = &s; // No problem.let r2 = &s; // No problem.let r3 = &mut s; // Big problem.println!("{}, {}, and {}", r1, r2, r3); |
Dangling References
In Rust, the compiler ensures that references never become dangling: when you have a reference to some data, the compiler ensures that the data does not go out of scope before its reference.
Let’s try to create a dangling reference, and Rust will prevent it with a compile-time error:
File name: src/main.rs
1234567891011121314151617181920212223 | fn main() { let reference_to_nothing = dangle();}fn dangle() -> &String { let s = String::from("hello");//s is dropped after the function ends. &s//Returning a reference, but the address is freed after the function ends.}Compiling loop_test v0.1.0 (C:\Users\cauchy\Desktop\rust\loop_test)error[E0106]: missing lifetime specifier --> src\main.rs:5:16 |5 | fn dangle() -> &String { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed fromhelp: consider using the `'static` lifetime |5 | fn dangle() -> &'static String { | +++++++For more information about this error, try `rustc --explain E0106`.error: could not compile `loop_test` due to previous error |
ref
ref is similar to &, and can be used to get a reference to a value, but their usage differs.
1234567891011121314151617 | fn main() { let c = '中'; let r1 = &c; let ref r2 = c; assert_eq!(*r1, *r2); // Determine whether the string representations of two memory addresses are equal. assert_eq!(get_addr(r1),get_addr(r2));}// Get the string representation of the memory address of the passed reference.fn get_addr(r: &char) -> String { format!("{:p}", r)} |
Summary of reference rules (borrowing rules).
- At any given time,or there can only be one mutable reference,or there can only be multiple immutable references.
- References must always be valid.
Ok: Borrowing immutably from a mutable object
123456789 | fn main() { let mut s = String::from("hello, "); borrow_object(&s); s.push_str("world");}fn borrow_object(s: &String) {} |
None Lexical Lifetimes(NLL)
Non-lexical lifetimes
Example
1234567891011 | // Comment out a line of code to make it workfn main() { let mut s = String::from("hello, "); let r1 = &mut s; r1.push_str("world"); let r2 = &mut s; r2.push_str("!"); println!("{}",r1);} |
Just comment out the println
1234567891011121314151617 | fn main() { let mut s = String::from("hello, "); let r1 = &mut s; r1.push_str("world");//The Rust compiler knows that after this, r1's borrow of s has ended. let r2 = &mut s; r2.push_str("!"); //println!("{}",r1);}fn main() { let mut x = 22; let p = &mut x; // mutable borrow println!("{}", x); // later used} |
This code compiles successfully because the compiler knows that the mutable borrow of x does not last until the end of the scope, but ends before x is used again, so there is no conflict here.
12345678910 | fn main() { let mut s = String::from("hello, "); let r1 = &mut s; let r2 = &mut s; // Add a line of code below to artificially create a compile error: cannot borrow `s` as mutable more than once at a time // You cannot use r1 and r2 at the same time.} |
Just add r1.push_str(“world”);
12345678910111213141516171819202122 | warning: unused variable: `r2` --> src/main.rs:5:9 |5 | let r2 = &mut s; | ^^ help: if this is intentional, prefix it with an underscore: `_r2` | = note: `#[warn(unused_variables)]` on by defaulterror[E0499]: cannot borrow `s` as mutable more than once at a time --> src/main.rs:5:14 |4 | let r1 = &mut s; | ------ first mutable borrow occurs here5 | let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here...9 | r1.push_str("world"); | -- first borrow later used hereFor more information about this error, try `rustc --explain E0499`.warning: `rust_programming` (bin "rust_programming") generated 1 warningerror: could not compile `rust_programming` (bin "rust_programming") due to 1 previous error; 1 warning emitted |
Slice Type
slice Allows you to reference a contiguous sequence of elements in a collection without referencing the entire collection. A slice is a kind of reference, so it does not have ownership.
12345678910 | fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len()} |
This function takes a string of words separated by spaces and returns the first word found in that string. If the function does not find a space in the string, then the entire string is a single word, so it should return the whole string.
The first_word function has a parameter &String. Because we don’t need ownership, this is fine. But what should it return? We don’t actually have a way to get… only partially a way to get a string. However, we can return the index of the end of the word, indicated by a space.
Because we need to check the String element by element to see if a value is a space, we need to use the as_bytes method to convert the String to a byte array:
let bytes = s.as_bytes();
Next, use the iter method to create an iterator over the byte array:
for (i, &item) in bytes.iter().enumerate() {
Because the enumerate method returns a tuple, we can use a pattern to destructure it, so in the for loop, we specify a pattern where i in the tuple is the index and &item in the tuple is a single byte. Because we get a reference to the collection element from .iter().enumerate(), we use & in the pattern.
However, there is a problem. We return an independent usize, but it is only a meaningful number in the context of the &String. In other words, because it is a value separate from the String, there is no guarantee that it will still be valid in the future.
String slice
String slice(string slice) is a reference to a part of a String, and it looks like this:
1234 | let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; |
[starting index…ending index]
[starting_index…ending_index]
where starting_index is the first position of the slice, ending_index is the slice The value after the last position.
If you want to start at index 0, you can omit the value before the two dots.
1234 | let s = String::from("hello");let slice = &s[0..2];let slice = &s[..2]; |
If the slice includes the last byte of the String, you can also omit the trailing number.
123456 | let s = String::from("hello");let len = s.len();let slice = &s[3..len];let slice = &s[3..]; |
You can also omit both values to get a slice of the entire string.
123456 | let s = String::from("hello");let len = s.len();let slice = &s[0..len];let slice = &s[..]; |
Note:**String slice range indices must be at valid UTF-8 character boundaries,**If you try to create a string slice from the middle of a multi-byte character, the program will exit with an error.
123456 | fn main() { let s = "你好,世界"; let slice = &s[0..2]; println!("{}",slice); assert!(slice == "你");} |
123 | thread 'main' panicked at src/main.rs:3:19:byte index 2 is not a char boundary; it is inside '你' (bytes 0..3) of `你好,世界`note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace |
String Indexing in Rusts[i]It does not directly return a character because UTF-8 is a variable-length encoding. You need to usechars()orbytes()to iterate over or manipulate the string content:chars()can iterate over Unicode characters, whilebytes()returns individual bytes.
Rewrite the function to return a slice (the return value of a string slice can be written as: &str)
1234567891011 | fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..]} |
Call
123456789 | fn main() { let mut s = String::from("hello world"); let word = first_word(&s); s.clear(); // Error! s needs to be a mutable reference. println!("the first word is: {}", word);} |
When you have an immutable reference to a value, you cannot also take a mutable reference. Because clear needs to empty the String, it attempts to take a mutable reference. The println! after the call to clear uses the reference in word, so this immutable reference must still be valid at that point. Rust does not allow the mutable reference in clear and the immutable reference in word to exist at the same time, so compilation fails.
String literals are slices.
1 | let s = "Hello, world!"; |
Here the type of s is &str:It is a slice pointing to a specific location in the binary program., which is why string literals are immutable; &str is an immutable reference.
String Slices as Parameters
Now that we know we can take slices of literals and Strings, we improved first_word. Here is its signature:
fn first_word(s: &String) -> &str {
A more experienced Rustacean would write the following signature, because it allows the same function to be used with both &String values and &str values:
fn first_word(s: &str) -> &str {
If you have a string slice, you can pass it directly. If you have a String, you can pass a slice of the entire String or a reference to the String. This flexibility takes advantage of deref coercions the benefits of defining a function that takes a string slice instead of a String reference, which makes our API more general and does not lose any functionality:
1234567891011121314151617181920 | fn main() { let my_string = String::from("hello world"); // `first_word` Applicable to `String`(a slice of), in whole or in full let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // `first_word` also applies to `String` reference to, // This is equivalent to the entire `String` slice of let word = first_word(&my_string); let my_string_literal = "hello world"; // `first_word` applies to string literals, in whole or in part let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // because string literals are already string slices // This also applies, no slice syntax needed! let word = first_word(my_string_literal);} |
&String can be implicitly converted to &str type
12345678910111213 | fn main() { let mut s = String::from("hello world"); // Here, &s is `&String` type, but `first_character` what the function needs is `&str` type. // Although the two types are different, the code still works because `&String` will be implicitly converted to `&str` Type let ch = first_character(&s); println!("the first character is: {}", ch); s.clear();}fn first_character(s: &str) -> &str { &s[..1]} |
slices of other types
String slices are for strings. However, there are also more general slice types. Consider this array:
let a = [1, 2, 3, 4, 5];
Just as we might want to get a part of a string, we might also want to reference a part of an array. We can do this:
12345 | let a = [1, 2, 3, 4, 5];let slice = &a[1..3];assert_eq!(slice, &[2, 3]); |
The type of this slice is &[i32]. It works the same way as string slices, by storing a reference to the first element of the collection and the total length of the collection. You can use this kind of slice for all other collections.
Slices are similar to arrays, butthe length of a slice cannot be known at compile time, so youcannot directly use the slice type。
1234567891011121314 | // Fix the errors in the code, do not add new lines of code!fn main() { let arr = [1, 2, 3]; let s1: [i32] = arr[0..2]; let s2: str = "hello, world" as str;}//After fixingfn main() { let arr = [1, 2, 3]; let s1: &[i32] = &arr[0..2]; let s2: &str = "hello, world" as &str;} |
- A slice reference occupies2 wordsof memory space (from now on, for the sake of brevity, unless there is a special reason, we will uniformly use ‘slice’ to specifically refer to slice references). The slice’sThe first word is a pointer to the data, and the second word is the length of the slice.。
- The size of a word depends on the processor architecture. For example, on x86-64, a word is 64 bits, or 8 bytes, so a slice reference is 16 bytes in size.
1234567 | fn main() { let arr: [char; 3] = ['中', '国', '人']; let slice = &arr[..2]; assert!(std::mem::size_of_val(&slice) == 16);} |
- A slice (reference) can be used toborrow a contiguous portion of an arrayThe corresponding signature is &[T], which can be compared with the array signature [T; Length].
123456 | fn main() { let arr: [i32; 5] = [1, 2, 3, 4, 5]; let slice: &[i32] = &arr[1..4]; assert_eq!(slice, &[2, 3, 4]);} |
Struct
Defining a struct
You need to use the struct keyword and provide a name for the entire struct.
Inside the curly braces, we define the name and type of each piece of data, which we call field(field)
123456 | struct User { active: bool, username: String, email: String, sign_in_count: u64,} |
Instantiation
12345678 | fn main() { let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, };} |
You can mark an entire struct as mutable when instantiating it, but Rust does not allow us to designate a specific field of a struct as mutable.
12345678910111213 | struct Person { name: String, age: u8,}fn main() { let age = 18; let mut p = Person { name: String::from("sunface"), age, }; p.age = 30; p.name = String::from("sunfei");} |
Access
12345678910 | fn main() { let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com");} |
Once an instance of a struct is mutable, all fields in the instance are mutable.
Field Init Shorthand
When the field name and the variable name corresponding to the field value are the same, you can use the field init shorthand.
12345678 | fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, }} |
Struct Update Syntax
Create a new instance based on an existing struct instance
12345678910 | fn main() { // --snip-- let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, };} |
Using struct update syntax
12345678 | fn main() { // --snip-- let user2 = User { email: String::from("another@example.com"), ..user1 };} |
Tuple Structs
Tuple structs have the meaning provided by the struct name, but do not have specific field names; they only have field types.
This is suitable for when you want to give the entire tuple a name and make the tuple a different type from other tuples.
1234567 | struct Color(i32, i32, i32);struct Point(i32, i32, i32);fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0);} |
Accessing such a struct is the same as accessing a tuple:
123456 | struct Point(i32, i32);fn main() { let p = Point(10, 20); println!("x = {}, y = {}", p.0, p.1);} |
Unit-like structs with no fields
Unit-like structs with no fields, which are similar to ()
12345 | struct AlwaysEqual;fn main() { let subject = AlwaysEqual;} |
Ownership in structs
In the definition of the User struct in Listing 5-1, we used the owned String type rather than the &str string slice type. This is a deliberate choice because we wantThis struct owns all of its data.,Therefore, as long as the entire struct is valid, its data is also valid.
It allows a struct to store references to data owned by other objects, but doing so requires using Lifecycle(lifetimes)
123456789101112131415 | struct User { active: bool, username: &str, email: &str, sign_in_count: u64,}fn main() { let user1 = User { email: "someone@example.com", username: "someusername123", active: true, sign_in_count: 1, };} |
Error: missing lifecycle identifier
1234567891011121314151617181920212223242526272829 | error[E0106]: missing lifetime specifier --> src/main.rs:3:15 |3 | username: &str, | ^ expected named lifetime parameter |help: consider introducing a named lifetime parameter |1 ~ struct User<'a> {2 | active: bool,3 ~ username: &'a str, |error[E0106]: missing lifetime specifier --> src/main.rs:4:12 |4 | email: &str, | ^ expected named lifetime parameter |help: consider introducing a named lifetime parameter |1 ~ struct User<'a> {2 | active: bool,3 | username: &str,4 ~ email: &'a str, |For more information about this error, try `rustc --explain E0106`.error: could not compile `rust_programming` (bin "rust_programming") due to 2 previous errors |
print struct
12345678910111213141516 | struct Rectangle{ width: u32, length: u32,}fn main() { let rect=Rectangle{ width:30, length:50, }; println!("{}",area(&rect)); println!("{:#?}",rect)}fn area(rect: &Rectangle)->u32{ rect.width*rect.length} |
struct methods
123456789101112131415161718 | struct Rectangle{ width: u32, length: u32,}impl Rectangle{ fn area(&self)->u32{ self.width*self.length }}fn main() { let rect=Rectangle{ width:30, length:50, }; println!("{}",rect.area()); println!("{:#?}",rect)} |
- Define methods in an impl block
- The first parameter of a method can be &self, or it can take ownership or a mutable borrow, just like other parameters.
- Better code organization
Method invocation operator
In C/C++, there are two different operators to call methods: . calls a method directly on an object, while -> calls a method on a pointer to an object, in which case the pointer must first be dereferenced. In other words, if object is a pointer, then object->something() is like (*object).something().
Rust does not have an operator equivalent to ->; instead, Rust has a called Automatic referencing and dereferencing(_automatic referencing and dereferencing_the function of ).Method callis in RustA few places that exhibit this behavior。
It works like this: when calling a method with object.something(), Rust automatically adds &, &mut, or * to object to make it match the method signature. That is, these pieces of code are equivalent:
12 | p1.distance(&p2);(&p1).distance(&p2); |
This automatic citation behavior works becauseA method has an explicit receiver.———— the type of selfGiven the receiver and method name, Rust can clearly determine whether the method only reads (&self), mutates (&mut self), or takes ownership (self).
correlation function
12345678 | impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } }} |
All functions defined in an impl block are called correlation function(associated functions)
Associated functions that are not methods are often used as constructors that return a new instance of a struct. These functions are usually named new, but new is not a keyword。
Use the struct name and :: syntax to call this associated function: for example, let sq = Rectangle::square(3);. This function resides in the struct’s namespace: the :: syntax is used for namespaces created by associated functions and modules.
Each struct is allowed to have multiple impl blocks.
example
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 | struct Point { x: f64, y: f64,}// `Point` The associated functions of ... are all placed in the following `impl` in the blockimpl Point { // Associated functions are used in a very similar way to constructors fn origin() -> Point { Point { x: 0.0, y: 0.0 } } // Another associated function, with two parameters fn new(x: f64, y: f64) -> Point { Point { x: x, y: y } }}struct Rectangle { p1: Point, p2: Point,}impl Rectangle { // This is a method // `&self` Yes `self: &Self` syntactic sugar for // `Self` is the type of the current calling object; for this example, `Self` = `Rectangle` fn area(&self) -> f64 { // Use the dot operator to access `self` the struct fields in let Point { x: x1, y: y1 } = self.p1; let Point { x: x2, y: y2 } = self.p2; // `abs` is a `f64` method of the type, returns the absolute value of the caller ((x1 - x2) * (y1 - y2)).abs() } fn perimeter(&self) -> f64 { let Point { x: x1, y: y1 } = self.p1; let Point { x: x2, y: y2 } = self.p2; 2.0 * ((x1 - x2).abs() + (y1 - y2).abs()) } // This method requires the caller to be mutable,`&mut self` Yes `self: &mut Self` syntactic sugar for fn translate(&mut self, x: f64, y: f64) { self.p1.x += x; self.p2.x += x; self.p1.y += y; self.p2.y += y; }}// `Pair` holds two integers allocated on the heapstruct Pair(Box<i32>, Box<i32>);impl Pair { // This method takes ownership of the caller // `self` Yes `self: Self` syntactic sugar for fn destroy(self) { let Pair(first, second) = self; println!("Destroying Pair({}, {})", first, second); // `first` and `second` goes out of scope here and is freed }}fn main() { let rectangle = Rectangle { // Associated functions are not called via the dot operator, but using `::` p1: Point::origin(), p2: Point::new(3.0, 4.0), }; // Methods are called via the dot operator // Note that the method here requires `&self` but we did not use `(&rectangle).perimeter()` The reason for calling is: // The compiler will automatically take a reference for us. // `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)` println!("Rectangle perimeter: {}", rectangle.perimeter()); println!("Rectangle area: {}", rectangle.area()); let mut square = Rectangle { p1: Point::origin(), p2: Point::new(1.0, 1.0), }; // Error!`rectangle` is immutable, but this method requires a mutable object. //rectangle.translate(1.0, 0.0); // TODO ^ Try uncommenting this line to see what happens. // Yes! Mutable objects can call mutable methods. square.translate(1.0, 1.0); let pair = Pair(Box::new(1), Box::new(2)); pair.destroy(); // Error! The previous `destroy` call took away `pair` the ownership of //pair.destroy(); // TODO ^ Try uncommenting this line.} |
Enumeration
Defining an enum
1234 | enum IpAddrKind { V4, V6,} |
By defining an IpAddrKind enum in code to represent this concept and list the possible IP address types, V4 and V6. This is called the enum’s Member(variants):
When creating an enum, you can use explicitintegerto set the value of the enum variant.
12345678910111213141516171819202122232425262728293031 | enum Number { Zero, One, Two,}enum Number1 { Zero = 0, One, Two,}// Error, cannot use decimals.//enum Number2 {// Zero = 0.0,// One = 1.0,// Two = 2.0,//}// C-like enumenum Number2 { Zero = 0, One = 1, Two = 2,}fn main() { // Through the `as` You can cast enum values to integer types. assert_eq!(Number::One as u8, Number1::One as u8); assert_eq!(Number1::One as u8, Number2::One as u8);} |
Enum value
You can create instances of the two different variants of IpAddrKind like this:
12 | let four = IpAddrKind::V4; let six = IpAddrKind::V6; |
The values in enum variants can be obtained using pattern matching.
12345678910111213141516171819 | enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),}fn main() { let msg = Message::Move{x: 1, y: 1}; if let Message::Move{x:a,y: b} = msg { // It can also be written as // if let Message::Move{x, y} = msg { assert_eq!(a, b); } else { panic!("不要让这行代码运行!"); }} |
Attaching data to the enum variant
Using struct
12345678910111213141516171819 | enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), }; |
Simply use the enum and put data directly into each enum variant instead of using the enum as part of a struct. The new definition of the IpAddr enum shows that both V4 and V6 variants are associated with String values:
12345678 | enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1")); |
Wedirectly attach data to each enum variant., so there is no need for an extra struct.
12345678 | enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); |
Note that even though the standard library contains a definition for IpAddr, we can still create and use our own definition without conflict because we have not brought the standard library’s definition into scope.
Enums can embed multiple types.
123456 | enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),} |
- Quit has no associated data.
- Move, like a struct, includes named fields.
- Write contains a single String.
- ChangeColor contains three i32s.
Defining methods on enums
Structs and enums have another similarity: just asyou can use impl to define methods for structs, you can also define methods on enums.. Here is a method named call defined on our Message enum:
12345678 | impl Message { fn call(&self) { // Define the method body here. } } let m = Message::Write(String::from("hello")); m.call(); |
The method body uses self to get the value on which the method is called. In this example, we created a variable m with the value Message::Write(String::from(“hello”)), and that is what self will be in the call method when m.call() is run.
12345678910111213141516171819202122232425 | enum TrafficLightColor { Red, Yellow, Green,}// implement TrafficLightColor with a methodimpl TrafficLightColor { fn color(&self) -> String { match *self { TrafficLightColor::Red => "red".to_string(), TrafficLightColor::Yellow => "yellow".to_string(), TrafficLightColor::Green => "green".to_string(), } }}fn main() { let c = TrafficLightColor::Yellow; assert_eq!(c.color(), "yellow"); println!("{:?}", c);} |
The Option Enum
Defined in the standard library, in the prelude.
Rust does not have Null, but provides an enum, Option, that is similar to the concept of Null.
1234 | enum Option<T> { None, Some(T),} |
use
1234 | let some_number = Some(5);let some_char = Some('e');let absent_number: Option<i32> = None; |
When we have a Some value, we know that a value exists, and that value is stored in the Some. When we have a None value, in a sense, it has the same meaning as null: there is no valid value. So, Option
In short, because Option
1234 | let x: i8 = 5; let y: Option<i8> = Some(5); let sum = x + y; |
In fact, the error message means that Rust does not know how to add an Option
In other words, when dealing with Option
To have a value that might be null, you must explicitly put it into the Option of the corresponding type
This is a deliberate design decision in Rust tolimit the proliferation of null valuesto increase the safety of Rust code.
Implementing a Linked List with Enums
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960 | enum List { // Cons: a node in the linked list that contains a value. The node is a tuple type; the first element is the value of the node, and the second element is a pointer to the next node. Cons(u32, Box<List>), // Nil: the last node in the linked list, used to indicate the end of the list. Nil,}// Implementing Methods for the Enumimpl List { // Create an empty linked list. fn new() -> List { // Since there are no nodes, return the Nil node directly. // The type of the enum variant Nil is List. Nil } // Add a new node at the front of the old linked list and return the new linked list. fn prepend(self, elem: u32) -> List { Cons(elem, Box::new(self)) } // Return the length of the linked list. fn len(&self) -> u32 { match *self { // Here we cannot take ownership of tail, so we need to get a reference to it and compute recursively. Cons(_,ref tail) => 1 + tail.len(), // The length of an empty linked list is 0. Nil => 0 } } // Return the string representation of the linked list, used for printing output. fn stringify(&self) -> String { match *self { Cons(head, ref tail) => { // Recursively generate the string. format!("{}, {}", head, tail.stringify()) }, Nil => { format!("Nil") }, } }}fn main() { // Create a new linked list (also empty). let mut list = List::new(); // Add some elements list = list.prepend(1); list = list.prepend(2); list = list.prepend(3); // Print the current state of the list println!("链表的长度是: {}", list.len()); println!("{}", list.stringify());} |
pattern matching
match control flow construct
Rust has an extremely powerful control flow operator called match that allows us toa valueanda series of patternscompare, and execute the corresponding code according to the matched pattern
1234567891011121314151617181920212223242526 | enum Coin { Penny, Nickel, Dime, Quarter,}fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }}fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }} |
matches!
matches! looks like match, but it can do something special.
12345678 | fn main() { let alphabets = ['a', 'E', 'Z', '0', 'x', '9' , 'Y']; // fill the blank with `matches!` to make the code work for ab in alphabets { assert!(matches!(ab, 'a'..='z' | 'A'..='Z' | '0'..='9')) }} |
The following code will cause an error because enums do not implement PartialEq by default, so they cannot be compared with ==
1234567891011121314151617181920212223242526272829303132333435363738 | enum MyEnum { Foo, Bar}fn main() { let mut count = 0; let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo]; for e in v { if e == MyEnum::Foo { // Fix the error, only modify this line of code. count += 1; } } assert_eq!(count, 2);}Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo)error[E0369]: binary operation `==` cannot be applied to type `MyEnum` --> src\main.rs:13:14 |13 | if e == MyEnum::Foo { // Fix the error, only modify this line of code. | - ^^ ----------- MyEnum | | | MyEnum |note: an implementation of `PartialEq<_>` might be missing for `MyEnum` --> src\main.rs:3:1 |3 | enum MyEnum { | ^^^^^^^^^^^ must implement `PartialEq<_>`help: consider annotating `MyEnum` with `` |3 | |For more information about this error, try `rustc --explain E0369`.error: could not compile `demo` due to previous error |
Change to
1234567891011121314151617 | enum MyEnum { Foo, Bar}fn main() { let mut count = 0; let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo]; for e in v { if matches!(e, MyEnum::Foo) { // Fix the error, only modify this line of code. count += 1; } } assert_eq!(count, 2);} |
Patterns that bind values
Another useful feature of match arms is that they can bind to parts of the values that match the pattern. This is how we extract values from enum variants.
1234567891011121314151617181920212223242526272829 | // This way we can immediately see the name of the state.enum UsState { Alabama, Alaska, // --snip--}enum Coin { Penny, Nickel, Dime, Quarter(UsState),}fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 } }}fn main(){ let c= Coin::Quarter(UsState::Alaska); println!("{}",value_in_cents(c));} |
Matching Option
1234567891011 | fn main(){ let five = Some(5); let six = plus_one(five); let none = plus_one(None);}fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), }}//It takes an Option<i32> and if it contains a value, add one to it. If it contains no value, the function should return None and not attempt to perform any operation. |
match must be exhaustive
useThe _ placeholder(must be placed at the very end)
12345 | match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => reroll(),} |
if let concise control flow
Handle cases where you only care about one pattern match and ignore other matches.
12345 | let config_max = Some(3u8);match config_max { Some(max) => println!("The maximum is configured to be {}", max), _ => (), } |
Using if let
1234 | let config_max = Some(3u8); if let Some(max) = config_max { println!("The maximum is configured to be {}", max); } |
In this example, the pattern is Some(max), and max is bound to the value inside the Some. You can then use max within the if let block, just as you would in the corresponding match arm. If the pattern doesn’t match, the code in the if let block won’t execute.
forgoes exhaustiveness
You can think of if let as syntactic sugar for match.
Using it with else
123456 | let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {:?}!", state); } else { count += 1; } |
Variable shadowing in pattern matching
1234567891011121314 | fn main() { let age = Some(30); if let Some(age) = age { // Create a new variable that is the same as the previous `age` variable with the same name assert_eq!(age, 30); } // new `age` The variable goes out of scope here. match age { // `match` can also achieve variable shadowing Some(age) => println!("age 是一个新的变量,它的值是 {}",age), _ => () } }//output://age is a new variable, and its value is 30. |
Workspace, Package,Crate,Module
- Packet(Packages): A feature of Cargo that allows you to build, test, and share crates.
- Crate : A tree structure of modules that forms a library or binary project.
- module(Modules) and use: Allows you to control the privacy of scopes and paths.
- path(path): A way of naming items such as structs, functions, or modules.
| Level | Name | Description | example |
|---|---|---|---|
| 📦 Package | “Package”, the unit managed by Cargo (a project). | Contains one or more crates, andCargo.toml | the entire project directory |
| 🧩 Crate | “Single compilation unit”, which can be a library or an executable program. | Each timerustcwhat is compiled is a crate | src/lib.rsorsrc/main.rs |
| 📁 Module | Module, a structure for organizing code within a crate. | Similar to C++ namespaces or Python modules. | mod network; |
| 🛣️ Path | Paths used to access modules, structs, and functions. | Similarstd::io::Write | crate::foo::bar() |
Crate types
- binary
- library
Each crate is an independent compilation unit.
The Rust compiler compiles only one crate at a time.
Crate Root
Is the source code file from which the Rust compiler starts, forming the root module of the crate.
Crate root is The starting file for the Rust compiler when building a crate,
It determines module tree the top-level structure of.
For example,
Suppose we have a very simple project:
12345 | my_project/├── Cargo.toml└── src/ ├── main.rs └── lib.rs |
Both of these files are possible crate root。
| files | crate types | Function |
|---|---|---|
src/main.rs | binary crate root | Program entry point (must containfn main()) |
src/lib.rs | library crate root | Library entry point (defines the library’s module structure) |
The compiler’s working logic
When runningcargo build:
Cargo will:
- Read
Cargo.toml; - Find the current package’s crate roots(e.g.
src/main.rs、src/lib.rs); - For each crate root, invoke the compiler, building the entire crate’s module tree starting from this file.
A Package
- Contains 1 Cargo.toml, which describes how to build these crates.
- Can only contain 0-1 library crates.
- can contain any number of binary crates
- but mustcontain at least one crate(library or binary)
Example:
12345678 | my_package/├── Cargo.toml├── src/│ ├── lib.rs # library crate│ ├── main.rs # main binary crate│ └── bin/│ ├── tool1.rs # second binary crate│ └── tool2.rs # third binary crate |
No special configuration is needed in Cargo.toml. Run:
123456 | # main.rscargo run# tool1.rscargo run --bin tool1# tool2.rscargo run --bin tool2 |
workspace
A workspace is a collection of multiple packages, each package can have its own library crate. For example:
123456789101112 | my_workspace/├── Cargo.toml # workspace declaration├── app/ # a binary crate package│ ├── Cargo.toml│ └── src/main.rs├── utils/ # a library crate package│ ├── Cargo.toml│ └── src/lib.rs└── network/ # another library crate package ├── Cargo.toml └── src/lib.rs |
TOML for the workspace declaration
12 | [workspace]members = ["app", "utils", "network"] |
Cargo’s conventions
src/main.rs
- binary crate’s crate root
- The crate name is the same as the package name.
src/lib.rs
- package contains a library crate
- library crate’s crate root
- The crate name is the same as the package name.
A package can contain both src/main.rs and src/lib.rs.
A package can have multiple binary crates:
Files placed in src/bin, each file is a separate binary crate.
Define modules to control scope and privacy.
Module
- Within a crate, group the crate.
- Control the privacy of items: public, private
Create a module
- The mod keyword
- Can be nested
- Can contain definitions of other items (struct, enum, constants, traits, functions, etc.)
123456789101112131415 | mod front_of_house { mod hosting { fn add_to_waitlist() {} fn seat_at_table() {} } mod serving { fn take_order() {} fn serve_order() {} fn take_payment() {} }} |
The module tree of the above code
123456789 | crate └── front_of_house ├── hosting │ ├── add_to_waitlist │ └── seat_at_table └── serving ├── take_order ├── serve_order └── take_payment |
src/main.rs and src/lib.rs are called crate roots
- The contents of these two files (either one) form a module named crate, located at the root of the entire module tree.
- The entire module tree is under the implicit crate module.
path
To find an item in Rust’s modules, you need to usepath
- absolute pathStarting from the crate root, use the crate name or the literal
crate. - Relative pathStarting from the current module, use self, super, or the identifier of the current module.
A path consists of at least one identifier, with identifiers separated by ::.
src/lib.rs
12345678910111213 | mod front_of_house { mod hosting { fn add_to_waitlist() {} }}pub fn eat_at_restaurant() {//Public // absolute path crate::front_of_house::hosting::add_to_waitlist(); // Relative path front_of_house::hosting::add_to_waitlist();} |
Privacy boundary
- All items in Rust (functions, methods, structs, enums, modules, constants) are private by default
- Parent modules cannot access the private items of child modules.
- Can be used in child modulesAllItems in ancestor modules
- Sibling modulesOKCall each other
- The pub keyword can mark items as public.
super
super: used to access content in the parent module path, similar to … in the file system
12345678910 | fn serve_order() {}mod back_of_house { fn fix_incorrect_order() { cook_order(); super::serve_order(); } fn cook_order() {}} |
pub struct
pub before struct:
- the struct is public
- struct fields are private by default,Adding pub before a field makes it public
Filename: src/lib.rs
123456789101112131415161718192021222324252627 | mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast { toast: String::from(toast), seasonal_fruit: String::from("peaches"), } } }}pub fn eat_at_restaurant() { // In summer, order a rye toast for breakfast let mut meal = back_of_house::Breakfast::summer("Rye"); // Change your mind and choose a different type of bread meal.toast = String::from("Wheat"); println!("I'd like {} toast please", meal.toast); // If you uncomment the next line, the code will not compile; // You are not allowed to view or modify the seasonal fruit that comes with the breakfast // meal.seasonal_fruit = String::from("blueberries");} |
pub enum
Put pub before the enum:
- the enum is public
- The variants of an enum are also public by default (no need to add the pub keyword)
the use keyword
Use the use keyword to bring paths into scope
12345678910111213 | mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }}use crate::front_of_house::hosting;pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist();} |
- Privacy rules still apply
- Use use to specify relative paths
Idiomatic usage of use
- Functions: bring the function’s parent module into scope (specify the parent)
Filename: src/lib.rs
12345678910111213 | mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }}use self::front_of_house::hosting;pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist();} |
- struct, enum, others: specify the full path (specify the item itself)
File name: src/main.rs
123456 | use std::collections::HashMap;fn main() { let mut map = HashMap::new(); map.insert(1, 2);} |
When two items with the same name are brought into scope
Filename: src/lib.rs
12345678910 | use std::fmt;use std::io;fn function1() -> fmt::Result { // --snip--}fn function2() -> io::Result<()> { // --snip--} |
Use the as keyword to provide a new name
Filename: src/lib.rs
12345678910 | use std::fmt::Result;use std::io::Result as IoResult;fn function1() -> Result { // --snip--}fn function2() -> IoResult<()> { // --snip--} |
pub use re-export
After using use to bring a path (name) into scope,that name is private in this scope
If you want External modules can also access through your path. That item (e.g., function, struct, module, etc.) can be re-exported usingpub use.
pub use: Re-export
- Bring items into scope
- The item can beexternal codebrought into their scope
pub(in Crate)
Sometimes we want an item to be visible only to a specific crate, so we can use thepub(in Crate)syntax.
Example:
123456789101112131415161718192021 | pub mod a { pub const I: i32 = 3; fn semisecret(x: i32) -> i32 { use self::b::c::J; x + J } pub fn bar(z: i32) -> i32 { semisecret(I) * z } pub fn foo(y: i32) -> i32 { semisecret(I) + y } mod b { pub(in crate::a) mod c { pub(in crate::a) const J: i32 = 4; } }} |
Using external packages
Add the dependency package in Cargo.toml
Use
useto bring specific items into scope.
- The standard library
stdis also treated as an external package, but you don’t need to modify Cargo.toml to includestd. - You need to use
useto bring specific items fromstdinto the current scope.
Use nested paths to clean up a large number ofusestatements.
The same part of the path::{the differing part of the path}
12 | use std::{cmp::Ordering,io};fn main() |
If one of the twousepaths is a subpath of the other
Usingself
123 | //use std::io;//use std::io::Write;use std::io::{self,Write} |
Wildcard *
Using*can bring all public items in the path into scope.
Use with caution
Application scenarios:
- prelude
- Testing: bring all the code under test into the tests module.
Split modules into different files
When defining a module, if the module name is followed by;instead of a code block,
- Rust will load the contents from a file with the same name as the module.
- The module tree will not change.
Example:
Filename: src/lib.rs
123456789 | mod front_of_house;pub use crate::front_of_house::hosting;pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist();} |
Example: Declaring frontof_house module, whose content will be located in _src/front_of_house.rs
src/front_of_house.rs will get front_of_the definition content of the house module, as shown in the example.
File name: src/front_of_house.rs
123 | pub mod hosting { pub fn add_to_waitlist() {}} |
Example:
Defining modulesfront_of_house
Method 1: Write module content directly in the file
file:src/front_of_house.rs
123 | pub mod hosting { pub fn add_to_waitlist() {}} |
- Here
front_of_houseThe module directly defines submoduleshosting。 - Advantage: Simple, and can be written this way when the module is small.
Method 2: Split the submodule into a separate file
- Before
front_of_house.rsOnly declare the submodule in it:
1 | pub mod hosting; |
- Create the directory and file structure:
12345 | src/├── lib.rs├── front_of_house.rs ← front_of_house 模块的主体└── front_of_house/ ← front_of_house 的子模块目录 └── hosting.rs ← 子模块 hosting 的实现 |
- Before
hosting.rsWrite the actual content in it:
1 | pub fn add_to_waitlist() {} |
- At this point,
hostingthe content of … is completely placed intohosting.rs。 - Advantage: When the module is large, splitting files is clearer and more maintainable.
Traditional style
Before Rust Edition 2018, the following style was used
1234 | src/└── front_of_house/ ├── mod.rs // front_of_house module body └── hosting.rs // front_of_house's submodule |
Common collections
Vector
Vec
Creating a vector
The Vec::new function
let v:Vec
Creating a Vec with initial values
let v = vec![1,2,3];
1234567891011121314 | let arr: [u8; 3] = [1, 2, 3]; let v = Vec::from(arr); is_vec(v); let v = vec![1, 2, 3]; is_vec(v); // vec!(..) and vec![..] are the same macro; the macro can use [], (), and {} forms, so... let v = vec!(1, 2, 3); is_vec(v); // ...in the code below, v is Vec<[u8; 3]>, not Vec<u8> let v1 = vec!(arr); |
Adding elements
12 | let mut v:Vec<i32>=Vec::new();v.push(1); |
Dropping a Vector
Like any other struct, a vector is freed when it goes out of scope.
12345 | { let v = vec![1, 2, 3, 4]; // Handling variable v} // <- here v goes out of scope and is dropped |
Reading values from a Vector
- Indexing
- get method
123456789 | let v = vec![1, 2, 3, 4, 5];let third: &i32 = &v[2];println!("The third element is {}", third);match v.get(2) { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."),} |
- When using the indexing method to access a value beyond the array elements, the program will panic.
- When using the get method to access, the program returns a None.
123456789101112131415161718 | fn main() { let mut v = Vec::from([1, 2, 3]); for i in 0..5 { println!("{:?}", v.get(i)) } for i in 0..5 { if let Some(x) = v.get(i) { v[i] = x + 1 } else { v.push(i + 2) } } assert_eq!(format!("{:?}",v), format!("{:?}", vec![2, 3, 4, 5, 6])); println!("Success!")} |
Reference borrowing rules
When we take an immutable reference to the first element of a vector and then try to add an element to the end of the vector, referencing that element later in the function will not work.
1234567 | let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0];//Immutable borrow v.push(6);//Mutable borrow println!("The first element is: {}", first);//Immutable borrow |
Why would a reference to the first element care about changes at the end of the vector? The reason this cannot be done is due to the way vectors work: when adding a new element at the end of the vector, if there is not enough space to store all elements contiguously,it may require allocating new memory and copying the old elements into the new space.. At this point,the reference to the first element would point to freed memory. The borrowing rules prevent the program from getting into this situation.。
Iterating over a Vector
1234 | let v = vec![100, 32, 57]; for i in &v { println!("{}", i); } |
We can also iterate over mutable references to each element of a mutable vector so that we can modify them.
1234 | let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } |
To modify the value pointed to by the mutable reference, you must use the dereference operator (*) to get the value in i before using the += operator.
Extending a Vector
A Vec can be extended using the extend method.
1234567891011121314 | fn main() { let mut v1 = Vec::from([1, 2, 4]); v1.pop(); v1.push(3); // v1 is [1,2,3] let mut v2 = Vec::new(); v2.extend([1, 2, 3]); // v2 is [1,2,3] assert_eq!(format!("{:?}",v1), format!("{:?}",v2)); println!("Success!")} |
Using an enum to make Vec store multiple data types
Define an enum so that different types of data can be stored in a vector.
1234567891011 | enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ]; |
Using trait objects to make Vec store multiple data types
12345678910111213141516171819202122232425262728293031 | trait IpAddr { fn display(&self);}struct V4(String);impl IpAddr for V4 { fn display(&self) { println!("ipv4: {:?}",self.0) }}struct V6(String);impl IpAddr for V6 { fn display(&self) { println!("ipv6: {:?}",self.0) }}fn main() { // Fill in the blanks let v: Vec<Box<dyn IpAddr>> = vec![ Box::new(V4("127.0.0.1".to_string())), Box::new(V6("::1".to_string())), ]; for ip in v { ip.display(); }} |
Convert type X into a Vec (using the From/Into traits)
1234567891011121314151617181920212223 | fn main() { // array -> Vec let arr = [1, 2, 3]; let v1 = Vec::from(arr); let v2: Vec<i32> = arr.into(); assert_eq!(v1, v2); // String -> Vec let s = "hello".to_string(); let v1: Vec<u8> = s.into(); let s = "hello".to_string(); let v2 = s.into_bytes(); assert_eq!(v1, v2); let s = "hello"; let v3 = Vec::from(s); assert_eq!(v2, v3); println!("Success!") } |
Slices
Similar to String slices, Vec can also be sliced. If a Vec is mutable, its slice is immutable or read-only; we can obtain a slice with &.
In Rust,**Passing slices as parameters is a more common usage.**For example, when a function only needs read-only access, passing a slice &[T] / &str of a Vec or String is more appropriate.
123456789101112131415161718192021 | fn main() { let mut v = vec![1, 2, 3]; let slice1 = &v[..]; // Out-of-bounds access will cause a panic. // When modifying, you must use `v.len` let slice2 = &v[0..v.len()]; assert_eq!(slice1, slice2); // Slices are read-only. // Note: slices and `&Vec` are different types; the latter is merely `Vec` a reference to, and can be directly obtained by dereferencing `Vec` let vec_ref: &mut Vec<i32> = &mut v; (*vec_ref).push(4); let slice3 = &mut v[0..]; // slice3.push(4); assert_eq!(slice3, &[1, 2, 3, 4]); println!("Success!")} |
Capacity
Capacity is the already allocated memory space used to store elements that will be added to the Vec in the future. Length, on the other hand, is the number of elements currently stored in the Vec. When adding a new element would cause the length to exceed the existing capacity, the capacity automatically grows: Rust reallocates a larger block of memory and copies the previous Vec over, so a new memory allocation occurs here.
If this code is executed frequently, frequent memory allocations will greatly affect the performance of our system. The best way is to allocate enough capacity in advance to minimize memory allocations.
123456789101112131415161718192021222324252627282930 | fn main() { let mut vec = Vec::with_capacity(10); assert_eq!(vec.len(), 0); assert_eq!(vec.capacity(), 10); // Since enough capacity was set in advance, the loop here will not cause any memory allocation... for i in 0..10 { vec.push(i); } assert_eq!(vec.len(), 10); assert_eq!(vec.capacity(), 10); // ...but the following code will cause new memory allocations. vec.push(11); assert_eq!(vec.len(), 11); assert!(vec.capacity() >= 11); // Fill in a suitable value, in `for` during the loop's execution, no memory allocation will occur let mut vec = Vec::with_capacity(100); for i in 0..100 { vec.push(i); } assert_eq!(vec.len(), 100); assert_eq!(vec.capacity(), 100); println!("Success!")} |
As long as From is implemented for Vec
String
- A string is a collection of bytes
- UTF-8 encoding
- Some methods can parse bytes into text
What is a string?
At Rust’s core language level, there is only one string type: the string slice str (or &str)
- String slice: a reference to a UTF-8 encoded string stored elsewhere
- String literals: stored in the binary, also string slices
123456789101112131415161718 | fn main() { let s = String::from("hello, 世界"); let slice1 = &s[0..1]; //Tip: `h` occupies only 1 byte in UTF-8 encoding assert_eq!(slice1, "h"); let slice2 = &s[7..10];// Tip: `in` occupies 3 bytes in UTF-8 encoding assert_eq!(slice2, "世"); // Iterate over all characters in s for (i, c) in s.chars().enumerate() { if i == 7 { assert_eq!(c, '世') } } println!("Success!")} |
In fact, String is asmart pointers, which is stored on the stack as a struct, and points to the underlying string data stored on the heap.
The smart pointer struct stored on the stack consists of three parts: a pointer to the byte array on the heap, the used length, and the allocated capacity (the used length is less than or equal to the allocated capacity; when the capacity is insufficient, memory space will be reallocated).
12345678910111213141516171819202122 | use std::mem;fn main() { let story = String::from("Rust By Practice"); // Prevent String's data from being automatically dropped let mut story = mem::ManuallyDrop::new(story); let ptr = story.as_mut_ptr(); let len = story.len(); let capacity = story.capacity(); assert_eq!(16, len); // We can reconstruct String based on the ptr pointer, length, and capacity. // This operation must be marked as unsafe, because we need to ensure that this operation is safe ourselves. let s = unsafe { String::from_raw_parts(ptr, len, capacity) }; assert_eq!(*story, s); println!("Success!")} |
Other string types
String type
From the standard library, also UTF-8 encoded.
String vs Str: Owned or Borrowed Variants
Other String Types
OsString / OsStr
12345 | use std::ffi::OsString;fn main() { let mut os_string = OsString::from("hello"); os_string.push(" world"); // Mutable} |
Mainly used for handling system strings such as file paths, command-line arguments, environment variables, etc.
CString / CStr
123 | use std::ffi::CString;let c_string = CString::new("hello").expect("NUL found!"); |
Fromstd::ffi, used for Interacting with C interfaces。
Features:
- With NUL-terminated (
\0a string of ) CStringOwns the data, guaranteeing no internal NUL.CStris an immutable borrow, similar to&str。
CStringdoes not provide methods likepush_stror index access to modify content; once created, itcannot modify the internal bytes。
Conversion between String and &str
Using to_string()
12345678 | fn main() { let s = "hello, world".to_string(); greetings(s)}fn greetings(s: String) { println!("{}",s)} |
Using String::from()
12345678 | fn main() { let s = String::from("hello, world"); greetings(s)}fn greetings(s: String) { println!("{}",s)} |
String escaping
1234567891011121314151617181920 | fn main() { // You can use escaping to output the characters you want. Here we use hexadecimal values; for example, \x73 is escaped to the lowercase letter 's'. // Fill in the blanks to output "I'm writing Rust" let byte_escape = "I'm writing Ru\x73__!"; println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape); // You can also use Unicode escape characters let unicode_codepoint = "\u{211D}"; let character_name = "\"DOUBLE-STRUCK CAPITAL R\""; println!("Unicode character {} (U+211D) is called {}", unicode_codepoint, character_name ); // You can also use \ to concatenate multi-line strings let long_string = "String literals can span multiple lines. The linebreak and indentation here \ can be escaped too!"; println!("{}", long_string);} |
Sometimes there are many characters that need escaping, and we want a more convenient way to write strings: raw strings.
1234567891011121314151617181920212223242526272829 | fn main() { let raw_str = r"Escapes don't work here: \x3F \u{211D}"; println!("{}", raw_str); // If the string contains double quotes, you can add # at the beginning and end let quotes = r#"And then I said: "There is no escape!""#; println!("{}", quotes); // If there is still ambiguity, you can keep adding more; there is no limit let longer_delimiter = r###"A string with "# in it. And even "##!"###; println!("{}", longer_delimiter);}fn main() { let raw_str = "Escapes don't work here: \x3F \u{211D}"; assert_eq!(raw_str, "Escapes don't work here: ? ℝ"); // If you need quotes in a raw string, add a pair of #s let quotes = r#"And then I said: "There is no escape!""#; println!("{}", quotes); // If you need "# in your string, just use more #s in the delimiter. // You can use up to 65535 #s. let delimiter = r###"A string with "# in it. And even "##!"###; println!("{}", delimiter); // Fill the blank let long_delimiter = r###"Hello, "##""###; assert_eq!(long_delimiter, "Hello, \"##\"")} |
Here r#" marks the beginning of a raw string, and "# marks the end of a string. If there is still ambiguity, you can keep adding #.
Create a new String
String::new()
let mut s = String::new();
Using to_string()
123456 | let data = "initial contents"; let s = data.to_string(); // This method can also be used directly on string literals: let s = "initial contents".to_string(); |
You can also use string::from()
let s = String::from(“initial contents”);
Updating a String
push_str()
12 | let mut s = String::from("foo"); s.push_str("bar"); |
The push_str() method does not take ownership of the parameter
123456789 | let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {}", s2);let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(&s2); println!("s2 is {}", s2); |
push()
The push method is defined to take a single character as a parameter and append it to the String
12 | let mut s = String::from("lo"); s.push('l'); |
Concatenating strings with +
You can onlyStringSame as&strtypes for concatenation, andStringthe ownership of … is moved in this process
123 | let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // Note that s1 has been moved and can no longer be used |
The + operator uses the add function, whose signature looks like this:
1 | fn add(self, s: &str) -> String { |
This is not the actual signature in the standard library;
But the type of &s2 is &String, not &str. So why does it still compile?
The reason &s2 can be used in the add call is that &String can be coerced(coerced) to &str. When the add function is called, Rust uses a technique called Deref coercion(deref coercion) technique, which you can think of as turning &s2 into &s2[…].
format!
12345678910 | let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = s1 + "-" + &s2 + "-" + &s3;let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3); |
The code generated by the format! macro uses references, soit does not take ownership of any parameters
s1,s2,s3they are all borrows.
Rust will Copy the contents to a new string(allocated on the heap):
- The original string is not affected.
- The copy here is necessary because a new independent string is generated.
Accessing by indexString
Rust’s stringsdoes not support indexing syntaxAccess
Internal representation
String is a Vec
let hello = String::from(“Hola”);
Here, the value of len is 4, which means the Vec storing the string “Hola” is four bytes long: each letter’s UTF-8 encoding takes up one byte.
(Note that the first letter in this string is the Cyrillic letter Ze, not the Arabic numeral 3.)
1 | let hello = String::from("Здравствуйте"); |
When asked how long this string is, someone might say 12. However, Rust’s answer is 24. This is the number of bytes needed to encode “Здравствуйте” in UTF-8, because each Unicode scalar value requires two bytes of storage.
Therefore, an index into a string’s bytes does not always correspond to a valid Unicode scalar value.
Byte strings
Byte strings, or byte arrays
1234567891011121314151617181920212223242526272829303132333435363738 | use std::str;fn main() { // Note that this is not `&str` type! let bytestring: &[u8; 21] = b"this is a byte string"; // Byte arrays do not implement `Display` trait, so only `Debug` way to print println!("A byte string: {:?}", bytestring); // Byte arrays can also use escapes let escaped = b"\x52\x75\x73\x74 as bytes"; // ...but do not support Unicode escapes // let escaped = b"\u{211D} is not allowed"; println!("Some escaped bytes: {:?}", escaped); // raw string let raw_bytestring = br"\u{211D} is not escaped here"; println!("{:?}", raw_bytestring); // Converting byte arrays to `str` type may fail if let Ok(my_str) = str::from_utf8(raw_bytestring) { println!("And the same as text: '{}'", my_str); } let _quotes = br#"You can also use "fancier" formatting, \ like with normal raw strings"#; // Byte arrays may not be in UTF-8 format let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82\xbb"; // "ようこそ" in SHIFT-JIS // but they may not necessarily be convertible to `str` Type match str::from_utf8(shift_jis) { Ok(my_str) => println!("Conversion successful: '{}'", my_str), Err(e) => println!("Conversion failed: {:?}", e), };} |
Bytes, scalar values, grapheme clusters
Rust has three ways of looking at strings:
- Bytes
123456 | fn main(){ let w = "नमस्ते"; for b in w.bytes(){ println!("{}",b); }} |
output
123456789101112131415161718 | 224164168224164174224164184224165141224164164224165135 |
- Scalar Values
123456 | fn main(){ let w = "नमस्ते"; for b in w.chars(){ println!("{}",b); }} |
output
123456 | नमस्ते |
- Grapheme Clusters (closest to what are called letters)
Obtaining them is more complex; the standard library does not provide this, so you need to rely on third-party libraries.
One reason Rust does not allow indexing into a String:
Indexing operations should take constant time O(1)
But String cannot guarantee this: it needs to traverse all the content to determine how many valid characters there are.
Slicing Strings
The type that string indexing should return is ambiguous: a byte value, a character, a grapheme cluster, or a string slice. Therefore, if you really want to use indexing to create a string slice, Rust requires you to be more explicit. To be more explicit about indexing and indicate that you need a string slice, instead of using [] with a single value, you can use [] with a range to create a string slice containing specific bytes:
123 | let hello = "Здравствуйте";let s = &hello[0..4]; |
s will be a &str that contains the first four bytes of the string. Earlier, we mentioned that these letters are each two bytes long, so this means s will be “Зд”.
What happens if you get &hello[0…1]? The answer is: Rust will panic at runtime.
Therefore,Slicing must not cross string boundaries.
Iterating over Strings
You cannot access a character in a string by indexing, but you can use slicing with &s1[start…end]; however, start and end must fall exactly on character boundaries.
- For scalar values: the chars() method
- For bytes: the bytes() method,
- For grapheme clusters: it’s very complex, and the standard library does not provide it. Neither Chinese nor English requires attention to grapheme clusters; you can use chars to iterate over Chinese and English.
12345678910111213141516171819 | fn main() { let s1 = String::from("hi,中国"); let h = &s1[0..1]; assert_eq!(h, "h"); let h1 = &s1[3..6]; assert_eq!(h1, "中");}fn main() { for c in "你好,世界".chars() { println!("{}", c) }}//output://you//good//,//world//world |
HashMap
HashMap uses the SipHash 1-3 hashing algorithm by default, which is very effective at resisting HashDoS attacks. In terms of performance, if your keys are medium-sized, this algorithm is very good, but if your keys are small (such as integers) or large (such as strings), you need to use other algorithms provided by the community to improve performance.
The hash table’s algorithm is based on Google’s SwissTable, you canHerefind the C++ implementation.
Creating HashMap<K,V>
Creating an empty HashMap: new() function
1234567 | use std::collections::HashMap; let mut scores = HashMap::new();//let mut scores:HashMap<String,i32> = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); |
- HashMap is used less often,not in the Prelude
- the standard library has less support for it,there is no built-in macro to create a HashMap
- data is stored on the heap
- homogeneous, i.e., K must be one type and V another type
12345678910111213141516171819 | use std::collections::HashMap;fn main() { let teams = [ ("Chinese Team", 100), ("American Team", 10), ("France Team", 50), ]; let mut teams_map1 = HashMap::new(); for team in &teams { teams_map1.insert(team.0, team.1); } let teams_map2: HashMap<_,_> = teams.into_iter().collect(); // let teams_map2 = HashMap::from(teams); assert_eq!(teams_map1, teams_map2); println!("Success!")} |
collect method creates a HashMap
The collect method can gather data into a variety of collection types
1234567 | use std::collections::HashMap; let teams = vec![String::from("Blue"), String::from("Yellow")]; let initial_scores = vec![10, 50]; let mut scores: HashMap<_, _> = teams.into_iter().zip(initial_scores.into_iter()).collect(); |
If the team names and initial scores are in two separate vectors, you can use zip method tocreate an iterator of tuples, where “Blue” and 10 are a pair, and so on. Then you can use the collect method to convert this iterator of tuples into a HashMap.
HashMap and Ownership
- For types that implement the Copy trait (such as i32), the values are copied into the HashMap.
- For values that own ownership (such as String), the values are moved, and ownership is transferred to the HashMap.
- If you insert references into a HashMap, the values themselves do not move, but the referenced values must remain valid for as long as the HashMap is valid.
Accessing values in a HashMap
get method
123456789 | use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name); |
for loop to iterate over a HashMap
12345678910 | use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{}: {}", key, value); } |
This will be inarbitrary orderPrint out each key-value pair:
12 | Yellow: 50Blue: 10 |
Indexing and the get Method
12345678910111213141516171819202122232425 | use std::collections::HashMap;fn main() { let mut scores = HashMap::new(); scores.insert("Sunface", 98); scores.insert("Daniel", 95); scores.insert("Ashley", 69); scores.insert("Katie", 58); // get returns an Option<&V> enum value let score = scores.get("Sunface"); assert_eq!(score, Some(&98)); if scores.contains_key("Daniel") { // Indexing returns a value V let score = scores["Daniel"]; assert_eq!(score, 95); scores.remove("Daniel"); } assert_eq!(scores.len(), 3); for (name, score) in scores { println!("The score of {} is {}", name, score) }} |
Updating a HashMap
Overwriting a value
12345678 | use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 25); println!("{:?}", scores); |
This will print {“Blue”: 25}. The original value 10 has been overwritten.
Inserting only if the key has no value
Using the entry methodInsert only if the key does not already have a value
1234567891011 | use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); let e=scores.entry(String::from("Yellow")); println!("{:?}",e); e.or_insert(50); scores.entry(String::from("Blue")).or_insert(50); println!("{:?}", scores); |
Output:
12 | Entry(VacantEntry("Yellow")){"Blue": 10, "Yellow": 50} |
The entry method
Checks whether the specified K corresponds to a V
Takes K as a parameter and returns an enum Entry: representing whether the value exists
Entry’sor_insert() method:
Returns:
- If K exists, returns a mutable reference to the corresponding V
- If K does not exist, inserts the method parameter as the new value for K, and returns a mutable reference to this value
Updating a value based on the old value
123456789101112 | use std::collections::HashMap; let text = "hello world wonderful world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } println!("{:?}", map); |
Here or_insert returns a mutable reference pointing to the value corresponding to the key just inserted by the entry method
Hash function
HashMap by default uses a hashing function called SipHash, which can resist hash table1 denial of service (DoS) attacks. However, this is not the fastest algorithm available, but it is worth paying some performance cost for higher security. If performance monitoring shows that this hashing function is very slow, so much so that you cannot accept it, you can specify a different hasher to switch to another function. A hasher is a type that implements the BuildHasher trait. We don’t need to implement your own hasher from scratch; there are many libraries of hashers for common hashing algorithms on crates.io.
HashMap key restrictions
Any type that implements the Eq and Hash traits can be used as a key in HashMap, including:
- bool (although rarely used, because it can only represent two kinds of keys)
- int, uint, and their variants, such as u8, i32, etc.
- String and &str (tip: when the key of a HashMap is of type String, you can actually use &str with the get method to query
It should be noted that f32 and f64 do not implement Hash, because Floating-point precision The problem would cause them to be unable to perform equality comparison.
If all fields of a collection type implement Eq and Hash, then the collection type will automatically implement Eq and Hash. For example, Vec
12345678910111213141516171819202122232425262728293031 | // Tip: `derive` is a good way to implement some common traits.use std::collections::HashMap;struct Viking { name: String, country: String,}impl Viking { fn new(name: &str, country: &str) -> Viking { Viking { name: name.to_string(), country: country.to_string(), } }}fn main() { // Use HashMap to store viking's health let vikings = HashMap::from([ (Viking::new("Einar", "Norway"), 25), (Viking::new("Olaf", "Denmark"), 24), (Viking::new("Harald", "Iceland"), 12), ]); // Use derive to print viking's current state for (viking, health) in &vikings { println!("{:?} has {} hp", viking, health); }} |
Capacity
Regarding capacity, we have already introduced it in detail in the previous Vector section, and HashMap can also adjust capacity: you can initialize it with a specified capacity via HashMap::with_capacity(uint), or use HashMap::new(), which provides a default initial capacity.
123456789101112131415161718 | use std::collections::HashMap;fn main() { let mut map: HashMap<i32, i32> = HashMap::with_capacity(100); map.insert(1, 2); map.insert(3, 4); // In fact, although we used a capacity of 100 to initialize, the map's capacity is likely to be more than 100. assert!(map.capacity() >= 100); // To shrink the capacity, the value you provide is only an allowed minimum. In fact, Rust will automatically set it based on the current amount of stored data. Of course, this value will be as close as possible to the value you provide, and it may also reserve some adjustment space. map.shrink_to(50); assert!(map.capacity() >= 50); // Let Rust adjust to an appropriate value by itself; the remaining strategy is the same as above. map.shrink_to_fit(); assert!(map.capacity() >= 2); println!("Success!")} |
Ownership
For types that implement the Copy trait, such as i32, the value of that type will be copied into the HashMap. For types with ownership, such as String, the ownership of their values will be transferred into the HashMap.
12345678910111213141516 | use std::collections::HashMap;fn main() { let v1 = 10; let mut m1 = HashMap::new(); m1.insert(v1, v1); println!("v1 is still usable after inserting to hashmap : {}", v1); let v2 = "hello".to_string(); let mut m2 = HashMap::new(); // Ownership is transferred here. m2.insert(v2, v1); // After this, v2 can no longer be used because it has been moved into the HashMap. // assert_eq!(v2, "hello"); // compile error: borrow of moved value println!("Success!")} |
Third-party Hash libraries
As mentioned earlier, if the performance of the existing SipHash 1-3 cannot meet the requirements, you can use alternative algorithms provided by the community.
For example, one of the community libraries is used as follows:
123456789 | use std::hash::BuildHasherDefault;use std::collections::HashMap;// Introduce a third-party hash functionuse twox_hash::XxHash64;let mut hash: HashMap<_, _, BuildHasherDefault<XxHash64>> = Default::default();hash.insert(42, "the answer");assert_eq!(hash.get(&42), Some(&"the answer")); |
Error handling
In most cases, errors are reported at compile time and handled.
Error classification
- Recoverable:
For example, file not found, can try again
- Unrecoverable
bug, e.g., accessing an index out of bounds
Rust does not have an exception mechanism like C++/Java
- Recoverable errors:Result<T,E>
- Unrecoverable: panic! macro
Unrecoverable errors and panic!
When the panic! macro executes
- The program prints an error message
- Unwind, cleaning up the call stack
- Exits the program
panic = ‘abort’
By default, when a panic occurs
- The program unwinds the call stack (large amount of work)
Rust walks back along the call stack, cleaning up data in each function it encounters
- Or immediately abort the call stack
Without cleaning up, directly stops the program; memory needs to be cleaned up by the OS
To make the binary smaller, change the setting from ‘unwind’ to ‘abort’
12 | [profile.release]panic = 'abort' |
Panic in your own code
123 | fn main(){ panic!("crash and burn");} |
So in dependent code: external panic
1234 | fn main(){ let vector=vec![1,2,3]; vector[100];} |
output
123456 | Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo) Finished dev [unoptimized + debuginfo] target(s) in 0.18s Running `target\debug\demo.exe`thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', src\main.rs:3:5note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceerror: process didn't exit successfully: `target\debug\demo.exe` (exit code: 101) |
Locate the code causing the problem through the backtrace information of the function that called panic!
Run the code with the following command (Windows cmd)
1 | set RUST_BACKTRACE=1 && cargo run |
Windows Poweshell
1 | $Env:RUST_BACKTRACE=1 -and (cargo run) |
On Linux
1 | export RUST_BACKTRACE=1 && cargo run |
More detailed information
RUST_BACKTRACE=full
To get a backtrace with debug information, debug symbols must be enabled (without --release).
Result enum and recoverable errors
1234 | enum Result<T, E> { Ok(T), Err(E),} |
T represents the type of the data in the Ok member returned on success,
and E represents the type of the error in the Err member returned on failure.
Result and its variants are also brought into scope by the prelude.
Opening a file
12345678910 | use std::fs::File;fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => panic!("Problem opening the file: {:?}", error), };} |
Matching different errors
12345678910111213141516171819 | use std::fs::File;use std::io::ErrorKind;fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => match error.kind() { ErrorKind::NotFound => match File::create("hello.txt") { Ok(fc) => fc, Err(e) => panic!("Problem creating the file: {:?}", e), }, other_error => { panic!("Problem opening the file: {:?}", other_error) } }, };} |
use unrap_or_else() and Closure
1234567891011121314 | use std::fs::File;use std::io::ErrorKind;fn main() { let f = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {:?}", error); }) } else { panic!("Problem opening the file: {:?}", error); } });} |
unwrap
A shortcut method for match expressions
12345 | use std::fs::File;fn main() { let f = File::open("hello.txt").unwrap();} |
equivalent to
12345678910 | use std::fs::File;fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => panic!("Problem opening the file: {:?}", error), };} |
- If the Result is Ok, return the value inside Ok.
- If the Result is Err, call the panic! macro.
expect
unwrap with a custom error message
12345 | use std::fs::File;fn main() { let f = File::open("hello.txt").expect("无法打开文件");} |
Propagating errors
Propagating errors to the caller
Custom implementation
1234567891011121314151617181920 | // src/main.rsuse std::fs::File;use std::io::{self, Read};fn read_username_from_file() -> Result<String, io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), }} |
The ? operator
12345678910 | use std::fs::File;use std::io;use std::io::Read;fn read_username_from_file() -> Result<String, io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s)} |
The ? operator is defined to work in exactly the same way as the match expression for handling Result values defined in the example of custom error propagation.
If the Result value is Ok, this expression will return the value inside Ok and the program will continue executing.
If the value is Err, the value inside Err will be the return value of the entire function,as if using thereturnkeyword, so the error value is propagated to the caller.
1234567891011121314151617181920212223242526272829 | use std::fs::File;use std::io::{self, Read};fn read_file1() -> Result<String, io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), }}fn read_file2() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s)}fn main() { assert_eq!(read_file1().unwrap_err().to_string(), read_file2().unwrap_err().to_string()); println!("Success!")} |
? and the from function
The from function on the trait std::convert::From
- Used for converting between errors.
- Errors to which ? is applied are implicitly handled by the from function.
- When ? calls the from function:
The error type it receives is converted to the error type defined by the current function’s return type.
Used for: Return the same error type for different error causes.
As long as each error type implements the from function that converts to the returned error type.
Can be used directly after ?Chained method callsto further shorten the code
File name: src/main.rs
1234567891011 | use std::fs::File;use std::io;use std::io::Read;fn read_username_from_file() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s)} |
Custom implementation
12345678910111213141516171819202122232425262728293031323334 | use std::fmt;use std::fs::File;use std::io::{self, Read};enum MyError { Io(io::Error), Parse,}// Implement Display for MyError (optional but common)impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { MyError::Io(e) => write!(f, "IO error: {}", e), MyError::Parse => write!(f, "Parse error"), } }}impl From<io::Error> for MyError { fn from(error: io::Error) -> MyError { MyError::Io(error) }}fn read_username_from_file() -> Result<String, MyError> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s)} |
? and the main function
The return type of the main function is ()
1234567 | use std::error::Error;use std::fs::File;fn main() -> Result<(), Box<dyn Error>> { let f = File::open("hello.txt")?; Ok(())} |
map,and_then
map example — modifyingOkthe value inside
Only handlesOkthe value and returns a newResult, without changing the error type
123 | let x: Result<i32, &str> = Ok(2);let y = x.map(|n| n * 3);assert_eq!(y, Ok(6)); |
What if it is an error?
123 | let x: Result<i32, &str> = Err("error");let y = x.map(|n| n * 3);assert_eq!(y, Err("error")); // The error is returned as is. |
and_then example — chained Result operations
and_thenHandlesOk, and continues to return aResult(chained logic), suitable for chaining multiple fallible steps:
12345678910 | fn sq_then_to_string(x: i32) -> Result<String, &'static str> { if x < 0 { Err("negative") } else { Ok((x * x).to_string()) }}let result = Ok(3).and_then(sq_then_to_string);assert_eq!(result, Ok("9".to_string())); |
If an error occurs, automatically stop the chained computation:
12 | let result = Err("bad").and_then(sq_then_to_string);assert_eq!(result, Err("bad")); |
Example:
123456789101112131415161718192021222324252627282930313233343536373839404142434445 | use std::num::ParseIntError;// With the return type rewritten, we use pattern matching without `unwrap()`.// But it's so Verbose..fn multiply(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> { match n1_str.parse::<i32>() { Ok(n1) => { match n2_str.parse::<i32>() { Ok(n2) => { Ok(n1 * n2) }, Err(e) => Err(e), } }, Err(e) => Err(e), }}// Rewriting `multiply` to make it succinct// You MUST USING `and_then` and `map` herefn multiply1(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> { // IMPLEMENT... n1_str.parse::<i32>().and_then(|n1| { n2_str.parse::<i32>().map(|n2| n1 * n2) })}fn print(result: Result<i32, ParseIntError>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), }}fn main() { // This still presents a reasonable answer. let twenty = multiply1("10", "2"); print(twenty); // The following now provides a much more helpful error message. let tt = multiply("t", "2"); print(tt); println!("Success!")} |
When to panic!
General Principles
- When defining afunction that might fail, prefer returning Result
- Otherwise, panic!
Scenario
- Demonstrating some concepts: unwrap
- Prototype code: unwrap, expect
- Tests: unwrap, expect
Sometimes you have more information than the compiler
- You can be sure that the Result is Ok:unwrap
12 | use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); |
Calling your code with meaningless argument values:panic!
Calling external code you don’t control, which returns an invalid state that you cannot fix:panic!
If failure is expected:Result
When your code operates on values, you should first validate those values:panic! (assert!)
Creating custom types for validation
One way to implement this is to parse the guess as an i32 instead of only a u32, to allow negative numbers, and then check whether the number is in range:
12345678910111213141516 | loop { // --snip-- let guess: i32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; if guess < 1 || guess > 100 { println!("The secret number will be between 1 and 100."); continue; } match guess.cmp(&secret_number) { // --snip-- } |
The if expression checks whether the value is out of range, tells the user what went wrong, and calls continue to start the next loop iteration, asking for another guess. After the if expression, you can compare the guess with the secret number, knowing that guess is between 1 and 100.
Instead, we can create a new type to put the validation in a function that creates an instance of it, rather than repeating these checks everywhere. This way, we can safely use the new type in function signatures and trust the values they receive. The example shows a way to define a Guess type that only creates an instance of Guess when the new function receives a value between 1 and 100:
1234567891011121314151617 | pub struct Guess { value: i32,}impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } pub fn value(&self) -> i32 { self.value }} |
We implemented a method named value that borrows self, takes no other parameters, and returns an i32. This kind of method is sometimes called _getter_because its purpose is to return the data of the corresponding field. Such a public method is necessary because the value field of the Guess struct is private. The private value field is important so that code using the Guess struct is not allowed to set value directly: callers Must must use the Guess::new method to create an instance of Guess, which ensures that there will not be a Guess whose value has not passed the condition check in the Guess::new function.
Thus, a function that takes (or returns) a number between 1 and 100 can be declared to take (or return) an instance of Guess instead of an i32, and its function body does not need to perform any additional checks.
Using Result in fn main
A typical main function looks like this:
123 | fn main() { println!("Hello World!");} |
In fact, the main function can also return a Result type: if an error occurs inside the main function, that error will be returned and a debug message about the error will be printed.
1234567891011 | use std::num::ParseIntError;fn main() -> Result<(), ParseIntError> { let number_str = "10"; let number = match number_str.parse::<i32>() { Ok(number) => number, Err(e) => return Err(e), }; println!("{}", number); Ok(())} |
Generics
Defining generics in functions
Finding the maximum value in a vec
12345678910111213141516171819202122232425262728 | fn largest_i32(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest}fn largest_char(list: &[char]) -> char { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest}fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!("The largest char is {}", result);} |
Using generics
123456789101112131415161718 | fn largest<T>(list: &[T]) -> T { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest}fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {}", result);} |
Run
12345678910111213141516 | Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo)error[E0369]: binary operation `>` cannot be applied to type `T` --> src\main.rs:5:17 |5 | if item > largest { | ---- ^ ------- T | | | T |help: consider restricting type parameter `T` |1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T { | ++++++++++++++++++++++For more information about this error, try `rustc --explain E0369`.error: could not compile `demo` due to previous error |
In simple terms, this error indicates that the body of largest cannot work for all possible types of T.
Because the function body needs to compare values of type T, but it can only be used for types that we know how to order. To enable comparison, the std::cmp::PartialOrd trait defined in the standard library can implement comparison for types.
Modification:
12345678910111213141516171819202122 | fn largest<T>(list: &[T]) -> &Twhere T: PartialOrd,{ let mut largest = &list[0]; for item in list { if *item > *largest {// Here, adding * is not necessary, because Rust implements PartialOrd for reference types, provided that the referenced type T: PartialOrd. largest = item; } } largest}fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {}", result);} |
Defining generics in structs
File name: src/main.rs
123456789 | struct Point<T> { x: T, y: T,}fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 };} |
Defining generics in enums
1234 | enum Option<T> { Some(T), None,} |
Enums can also have multiple generic types.
1234 | enum Result<T, E> { Ok(T), Err(E),} |
Generics in method definitions
File name: src/main.rs
1234567891011121314151617181920 | struct Point<T> { x: T, y: T,}impl<T> Point<T> { fn x(&self) -> &T { &self.x }}impl Point<i32> { fn x(&self) -> &i32 { &self.x }}fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x());} |
- Putting T after the impl keyword indicates implementing methods on type T: impl
Point - Implementing methods only for a concrete type: impl Point
123456789101112131415 | struct Point<T> { x: T, y: T,}impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() }}fn main() { let p = Point{x: 5.0_f32, y: 10.0_f32}; println!("{}",p.distance_from_origin())} |
- Generic type parameters in a struct can be different from the generic type parameters in its methods.
12345678910111213141516171819202122 | struct Point<X1, Y1> { x: X1, y: Y1,}impl<X1, Y1> Point<X1, Y1> { fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> { Point { x: self.x, y: other.y, } }}fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y);} |
Const generics
Generics are implemented for types; all generics are for abstracting over different types. But is there a generic for values? The answer is const generics.
1234567891011121314151617181920212223 | struct ArrayPair<T, const N: usize> { left: [T; N], right: [T; N],}impl<T: Debug, const N: usize> Debug for ArrayPair<T, N> { // ...}fn foo<const N: usize>() {}fn bar<T, const M: usize>() { foo::<M>(); // ok: matches the first kind foo::<2021>(); // ok: matches the second kind foo::<{20 * 100 + 20 * 10 + 1}>(); // ok: matches the third kind foo::<{ M + 1 }>(); // error: violates the third kind; const expressions cannot contain generic parameter M foo::<{ std::mem::size_of::<T>() }>(); // error: the generic expression contains generic parameter T let _: [u8; M]; // ok: matches the first kind let _: [u8; std::mem::size_of::<T>()]; // error: the generic expression contains generic parameter T}fn main() {} |
Currently, const generic parameters can only use arguments of the following forms:
a single const generic parameter
a literal (i.e. an integer, boolean, or character).
a concrete const expression (the expression cannot contain any generic parameters)
const generics can also help us avoid some runtime checks and improve performance
1234567891011121314151617181920 | pub struct MinSlice<T, const N: usize> { pub head: [T; N], pub tail: [T],}fn main() { let slice: &[u8] = b"Hello, world"; let reference: Option<&u8> = slice.get(6); // We know `.get` The returned value is `Some(b' ')` // but the compiler doesn't know assert!(reference.is_some()); let slice: &[u8] = b"Hello, world"; // When compiling and building MinSlice, a length check is performed, meaning we know at compile time that its length is 12. // At runtime, once `unwrap` succeeds, within `MinSlice` the scope of, no further checks are needed. let minslice = MinSlice::<u8, 12>::from_slice(slice).unwrap(); let value: u8 = minslice.head[6]; assert_eq!(value, b' ')} |
<T, const N: usize> is part of the struct type, just like array types, which means different lengths result in different types: Array<i32, 3> and Array<i32, 4> are different types
12345678910111213141516171819 | struct Array<T, const N: usize> { data : [T; N]}fn main() { let arrays = [ Array{ data: [1, 2, 3], }, Array { data: [1, 2, 3], }, Array { data: [4,5,6] } ];} |
Fill in the blanks
1234567891011 | // Fill in the blanksfn print_array<__>(__) { println!("{:?}", arr);}fn main() { let arr = [1, 2, 3]; print_array(arr); let arr = ["hello", "world"]; print_array(arr);} |
Answer:
12345678910 | fn print_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) { println!("{:?}", arr);}fn main() { let arr = [1, 2, 3]; print_array(arr); let arr = ["hello", "world"]; print_array(arr);} |
Sometimes we want tolimit the amount of memory a variable occupies, for example in embedded environments, the third form of const generic parameters — const expressions — is very suitable:
The following code uses a feature and requires a nightly compiler.
123456789101112131415161718192021222324 | fn check_size<T>(val: T)where Assert<{ core::mem::size_of::<T>() < 768 }>: IsTrue,{ //...}// fix the errors in mainfn main() { check_size([0u8; 767]); check_size([0i32; 191]); check_size(["hello你好"; 47]); // &str is a string reference, containing a pointer and string length in it, so it takes two word long, in x86-64, 1 word = 8 bytes check_size([(); 31].map(|_| "hello你好".to_string())); // String is a smart pointer struct, it has three fields: pointer, length and capacity, each takes 8 bytes check_size(['中'; 191]); // A char takes 4 bytes in Rust}pub enum Assert<const CHECK: bool> {}pub trait IsTrue {}impl IsTrue for Assert<true> {} |
Performance of generic code
Rust implements generics such that code using generic type parameters has no speed loss compared to using concrete types.
Rust, by performing at compile time the monomorphization of generic code(monomorphization) to ensure efficiency.Monomorphization is the process of converting generic code into specific code by filling in the concrete types used at compile time.
123456789101112131415161718 | //fn main(){// let integer = Some(5);// let float = Some(5.0);//}enum Option_i32 { Some(i32), None,}enum Option_f64 { Some(f64), None,}fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0);} |
At compile time, Rust expands the Option generic into Option
Trait: Defining Shared Behavior
A trait tells the Rust compiler what functionality a type has and can share with other types.
Defining a Trait
Putting method signatures together to define a set of behaviors required to accomplish a purpose.
- Keyword: trait
- Only method signatures, no concrete implementation.
Filename: src/lib.rs
123 | pub trait Summary { fn summarize(&self) -> String;} |
Implementing a Trait on a Type
Similar to implementing methods for a type.
Difference: impl Xxxx for Tweet
File: src/lib.rs
12345678910111213141516171819202122232425 | pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,}impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) }}pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,}impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) }} |
File: src/main.rs
12345678910 | use demo::{Summary,Tweet};fn main(){ let tweet = Tweet{ username: String::from("horse_ebook"), content: String::from("of course,sa you probably..."), reply:false, retweet:false, }; println!("1 new tweet:{}",tweet.summary());} |
demo is the name of the [package] entry in the Cargo.toml file.
Constraints for Implementing a Trait
The precondition for implementing a trait on a type is:
- The entire type or This trait is defined in the local crate (i.e., at least one of them is local).
Cannot implement an external trait on an external type.
This restriction is called coherence(coherence) part of the program property, or more specifically, orphan rule(orphan rule), named for the absence of a parent type. This rule ensures that code written by others cannot break your code, and vice versa. Without this rule, two crates could each implement the same trait for the same type, and Rust would have no way of knowing which implementation to use.
In short:If you didn’t write the type or the trait,then you are not allowed to add an impl for them.。
Default implementations of functions in a trait
Filename: src/lib.rs
12345678910111213141516171819202122232425262728293031 | pub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") }}pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,}impl Summary for NewsArticle { //fn summarize(&self) -> String { // format!("{}, by {} ({})", self.headline, self.author, self.location) //}}pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,}impl Summary for Tweet { //Overriding the default implementation fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) }} |
Default implementations can call other methods in the same trait,even if those methods don’t have default implementations
1234567 | pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) }} |
Note: you cannot call the default implementation from within an overriding implementation
Trait as a parameter
impl Trait syntax:
Applies to simple cases; it is syntactic sugar for trait bounds
123 | pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize());} |
trait bound syntax:
Applies to complex cases
123 | pub fn notify<T: Summary>(item: &T) { println!("Breaking news! {}", item.summarize());} |
Compare
1 | pub fn notify(item1: &impl Summary, item2: &impl Summary) {} |
This applies to cases where item1 and item2 are allowed to be different types (as long as they both implement Summary). But what if you want to force them to be the same type? That is only possible when using trait bounds:
1 | pub fn notify<T: Summary>(item1: &T, item2: &T) {} |
Use + to specify multiple trait bounds
123 | pub fn notify(item: &(impl Summary + Display)) {}pub fn notify<T: Summary + Display>(item: &T) {} |
Use where to simplify trait bounds
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
Use a where clause
1234 | fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug{} |
Returning a type that implements a trait
You can also use impl Trait syntax in the return value to return a type that implements a trait:
12345678910 | fn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, }} |
However, this only applies toreturning a single type. For example, this code specifies the return type as impl Summary, but returning NewsArticle or Tweet won’t work:
123456789101112131415161718192021222324 | fn returns_summarizable(switch: bool) -> impl Summary { if switch { NewsArticle { headline: String::from( "Penguins win the Stanley Cup Championship!", ), location: String::from("Pittsburgh, PA, USA"), author: String::from("Iceburgh"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), } } else { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } }} |
Here it tries to return NewsArticle or Tweet. This won’t compile because of the limitations of how impl Trait works.
You can use dyn trait objects
12345678910111213141516171819202122232425262728293031323334 | struct Sheep {}struct Cow {}trait Animal { fn noise(&self) -> String;}impl Animal for Sheep { fn noise(&self) -> String { "baaaaah!".to_string() }}impl Animal for Cow { fn noise(&self) -> String { "moooooo!".to_string() }}// Return a type that implements the Animal trait, but we cannot know at compile time which specific type is returned.// To fix the error here, you can use a fake random, or you can use trait objects.fn random_animal(random_number: f64) -> Box<dyn Animal> { if random_number < 0.5 { Box::new(Sheep {}) } else { Box::new(Cow {}) }}fn main() { let random_number = 0.234; let animal = random_animal(random_number); println!("You've randomly chosen an animal, and it says {}", animal.noise());} |
Trait objects, using trait objects in arrays
12345678910111213141516171819202122232425262728293031323334353637383940 | trait Bird { fn quack(&self);}struct Duck;impl Duck { fn fly(&self) { println!("Look, the duck is flying") }}struct Swan;impl Swan { fn fly(&self) { println!("Look, the duck.. oh sorry, the swan is flying") }}impl Bird for Duck { fn quack(&self) { println!("{}", "duck duck"); }}impl Bird for Swan { fn quack(&self) { println!("{}", "swan swan"); }}fn main() { // Fill in the blanks let birds :[Box<dyn Bird>;2]=[Box::new(Duck{}),Box::new(Swan{})]; for bird in birds { bird.quack(); // When duck and swan become birds, they both forget how to soar in the sky, and only remember how to call. // Therefore, the following code will cause an error. // bird.fly(); }} |
&dyn and Box
12345678910111213141516171819202122232425262728293031323334 | trait Draw { fn draw(&self) -> String;}impl Draw for u8 { fn draw(&self) -> String { format!("u8: {}", *self) }}impl Draw for f64 { fn draw(&self) -> String { format!("f64: {}", *self) }}fn main() { let x = 1.1f64; let y = 8u8; // draw x draw_with_box(Box::new(x)); // draw y draw_with_ref(&y);}fn draw_with_box(x: Box<dyn Draw>) { x.draw();}fn draw_with_ref(x: &dyn Draw) { x.draw();} |
Static and Dynamic dispatch
12345678910111213141516171819202122232425262728293031 | trait Foo { fn method(&self) -> String;}impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) }}impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) }}// implement below with genericsfn static_dispatch<T: Foo>(x: T) { x.method();}// implement below with trait objectsfn dynamic_dispatch(x: &dyn Foo) { x.method();}fn main() { let x = 5u8; let y = "Hello".to_string(); static_dispatch(x); dynamic_dispatch(&y); println!("Success!")} |
Use trait bounds to fix the largest function
File name: src/main.rs
1234567891011121314151617181920212223 | fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest}fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {}", result);} |
If you do not want to restrict the largest function to only types that implement the Copy trait, we can specify Clone instead of Copy in the trait bounds of T. And clone each value of the slice so that the largest function takes ownership of them. Using the clone function means that for types that own data on the heap, such as String, it will potentially allocate more heap space, and heap allocation can be quite slow when dealing with large amounts of data.
1234567891011121314151617 | fn largest<T: PartialOrd + Clone>(list: &[T]) -> T { let mut largest = list[0].clone(); for item in list { if item > &largest { largest = item.clone(); } } largest}fn main() { let str_list = vec![String::from("hello"),String::from("world")]; let result = largest(&str_list) println!("The largest word is {}", result);} |
Or have largest return a reference directly
1234567891011121314151617 | fn largest<T: PartialOrd + Clone>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > &largest { largest = item; } } largest}fn main() { let str_list = vec![String::from("hello"),String::from("world")]; let result = largest(&str_list) println!("The largest word is {}", result);} |
Using trait bounds to conditionally implement methods
12345678910111213141516171819202122 | use std::fmt::Display;struct Pair<T> { x: T, y: T,}impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } }}impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } }} |
Implementing a trait for all types that satisfy a trait bound is called blanket implementation (full coverage implementation)
For example:
123 | impl<T: Display> ToString for T { // --snip--} |
Because the standard library has these blanket implementations, we can call the to_string method defined by ToString on any type that implements the Display trait. For example, we can convert an integer to its corresponding String value, because integers implement Display:
let s = 3.to_string();
Derive macro derived implementations
We can use the #[derive] attribute to derive some traits, and the compiler will automatically provide default implementations for these traits. This is very convenient for everyday code development. For example, the Debug trait, which everyone often uses, is directly obtained through derivation, without us having to do this work manually.
12345678910111213141516171819202122232425262728293031323334353637383940414243 | // `Centimeters`, a tuple struct that can be compared for sizestruct Centimeters(f64);// `Inches`, a tuple struct that can be printedstruct Inches(i32);impl Inches { fn to_centimeters(&self) -> Centimeters { let &Inches(inches) = self; Centimeters(inches as f64 * 2.54) }}// Add some attributes to make the code work// Do not modify other code!struct Seconds(i32);fn main() { let _one_second = Seconds(1); println!("One second looks like: {:?}", _one_second); let _this_is_true = _one_second == _one_second; let _this_is_true = _one_second > _one_second; let foot = Inches(12); println!("One foot equals {:?}", foot); let meter = Centimeters(100.0); let cmp = if foot.to_centimeters() < meter { "smaller" } else { "bigger" }; println!("One foot is {} than one meter.", cmp);} |
Lifecycle
- Every reference in Rust has its own lifetime
- Lifetimes: the scope in which references remain valid
- In most cases: lifetimes are implicit and can be inferred
- When the lifetimes of references may be related in different ways: manually annotate lifetimes
The goal of lifetimes is:Avoid dangling references
Attempting to use a reference to a value that has gone out of scope will fail.
12345678910 | { let r; { let x = 5; r = &x; } println!("r: {}", r); } |
borrow checker
The Rust compiler has a borrow checker(borrow checker), which compares scopes to ensure that all borrows are valid.
12345678910 | { let r; // ---------+-- 'a // | { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!("r: {}", r); // | } |
Here, the lifetime of r is marked as 'a and the lifetime of x as 'b. As you can see, the inner 'b block is much smaller than the outer lifetime 'a. At compile time, Rust compares the sizes of these two lifetimes and finds that r has lifetime 'a, but it references an object with lifetime 'b. The program is rejected for compilation because lifetime 'b is smaller than lifetime 'a: the referenced object exists for a shorter time than the reference to it.
Generic Lifetimes in Functions
1234567891011121314 | fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y }}fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result);} |
Execute
1234567891011121314 | error[E0106]: missing lifetime specifier --> src\main.rs:1:33 |1 | fn longest(x: &str, y: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`help: consider introducing a named lifetime parameter |1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ++++ ++ ++ ++For more information about this error, try `rustc --explain E0106`.error: could not compile `demo` due to previous error |
Lifetime annotations
1234567891011121314 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result);} |
It indicates that the lifetime of the return value is the overlap of the lifetimes of the two references passed in.
Lifetime annotation syntax
Lifetime annotations do not change the length of a reference’s lifetime.
When generic lifetime parameters are specified, the function can accept references with any lifetime.
Lifetime annotations:Describes the relationships among the lifetimes of multiple references, but does not affect the lifetimes.
Lifetime parameter names: start with an apostrophe (') and are usually all lowercase and very short; many people use 'a.
Placement of lifetime annotations
After the & symbol of a referenceuse a space to separate the annotation from the reference type.
123 | &i32 // Reference&'a i32 // A reference with an explicit lifetime&'a mut i32 // A mutable reference with an explicit lifetime |
Generic lifetime parameters are declared in:the <> between the function name and the parameter list.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
1234567 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }} |
Its actual meaning is that the lifetime of the reference returned by the longest function is the same as the shorter of the lifetimes of the references passed in.
Remember that by specifying lifetime parameters in the function signature, we are not changing the lifetimes of any values passed in or returned. Rather, we are stating that any value that does not satisfy this constraint will be rejected by the borrow checker. Note that the longest function does not need to know exactly how long x and y will exist, but only that some scope can be substituted for 'a that will satisfy this signature.
When concrete references are passed to longest, the concrete lifetime substituted for 'a is the scope of x and the scope of y.the overlapping partIn other words, the concrete lifetime of the generic lifetime 'a is the one of the lifetimes of x and y that is**the smaller one.**Because we annotated the returned reference with the same lifetime parameter 'a, the returned reference is guaranteed to remain valid until the end of the shorter of the lifetimes of x and y.
use
123456789 | fn main() { let string1 = String::from("long string is long"); { let string2 = String::from("xyz"); let result = longest(string1.as_str(), string2.as_str()); println!("The longest string is {}", result); }} |
In this example, string1 is valid until the end of the outer scope, string2 is valid in the inner scope, and result references something that is valid until the end of the inner scope. The borrow checker accepts this code; it compiles and runs, and prints The longest string is long string is long.
Modify
123456789 | fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); } println!("The longest string is {}", result);} |
The program fails to run.
The error indicates that for result to be valid in println!, string2 would need to be valid until the end of the outer scope. Rust knows this because the parameters and return value of the longest function all use the same lifetime parameter 'a.
Understanding Lifetimes in Depth
- The correct way to specify lifetime parameters depends on the specific functionality of the function implementation.
If we change the implementation of the longest function to always return the first parameter rather than the longest string slice, we don’t need to specify a lifetime for parameter y. The following code will compile:
File name: src/main.rs
123 | fn longest<'a>(x: &'a str, y: &str) -> &'a str { x} |
- When returning a reference from a function, the lifetime parameter of the return value needs to match the lifetime parameter of one of the parameters.
If the returned reference does not does not point to any parameter, then the only possibility is that it points to a value created inside the function, which will be a dangling reference because it will go out of scope when the function ends.
1234 | fn longest<'a>(x: &str, y: &str) -> &'a str { let result = String::from("really long string"); result.as_str()} |
Compilation error
In summary, lifetime syntax is used to**associate the lifetimes of the function’s parameters with the lifetime of its return value.**Once they form such an association, Rust has enough information to allow memory-safe operations and prevent behaviors that would create dangling pointers or violate memory safety.
Lifetime Annotations in Struct Definitions
A struct can include:
- owned types
- References: you need to add a lifetime annotation to each reference.
File name: src/main.rs
1234567891011 | struct ImportantExcerpt<'a> { part: &'a str,}fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, };} |
This indicates that the lifetime of the struct is the same as the lifetime of its member part.
Lifetime Elision
Lifetime Elision
The patterns encoded into Rust’s reference analysis are calledlifetime elision rules.(lifetime elision rules)
These are not rules that programmers need to follow; they are a set of specific scenarios that the compiler considers. If the code matches these scenarios, there is no need to explicitly specify lifetimes.
The lifetime of a function or method’s parameters is called input lifetime(input lifetimes), and the lifetime of the return value is called output lifetime(output lifetimes)。
The compiler usesthree rulesto determine when references do not require explicit annotations. The first rule applies to input lifetimes, and the latter two rules apply to output lifetimes. If after checking these three rules there are still references whose lifetimes have not been determined, the compiler will stop and generate an error. These rulesApplicable tofndefinitions, andimplblocks。
- Rule 1:Each parameter of a reference type has its own lifetime.
- Rule 2:If there is only one input lifetime parameter, that lifetime is assigned to all output lifetime parameters.
- Rule 3:If there are multiple input lifetime parameters, but one of them is &self or &mut self (because it is a method), then the lifetime of self is assigned to all output lifetime parameters.
Example:
Let’s assume we are the compiler.
1 | fn first_word(s: &str) -> &str { |
Then the compiler applies the first rule, which iseach reference parameter has its own lifetime。
1 | fn first_word<'a>(s: &'a str) -> &str { |
For the second rule, it applies because there is exactly one input lifetime parameter here. The second rule states that the input parameter’s lifetime will be assigned to the output lifetime parameters, so now the signature looks like this:
1 | fn first_word<'a>(s: &'a str) -> &'a str { |
Now all references in this function signature have lifetimes, so the compiler can continue its analysis without the programmer needing to annotate the lifetimes in this function signature.
1 | fn longest(x:&str,y:&str)->&str{ |
Apply the first rule
1 | fn longest<'a,'b>(x:&'a str,y:&'b str)->&str{ |
The second rule does not apply, and the third rule does not apply, so the compiler reports an error.
Lifetime annotations in method definitions
When implementing methods on a struct with lifetimes, the syntax is still similar to that of generic type parameters.
Where to declare lifetime parameters depends on:
- Whether lifetime parameters are related to struct fields or to method parameters and return values.。
Lifetime names for struct fields:
- Declared after impl
- Used after the struct name
- These lifetimes are part of the struct type.
In method signatures within the impl block:
- References must be bound to the lifetime of the struct field references, or they can be independent references.
- Lifetime elision rules often make lifetime annotations in methods unnecessary.
12345678910111213141516171819202122 | struct ImportantExcerpt<'a>{ part: &'a str,}impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } //Here is an example that applies the third lifetime elision rule. fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {}", announcement); self.part }}fn main(){ let novel = String::from("Call me Ishmael. Some year ago..."); let first_sentence = novel.split('.').next().expect("Could not found a '.'"); let i = ImportantExcerpt{ part: first_sentence, };} |
announce_and_In return_part, there are two input lifetimes, so Rust applies the first lifetime elision rule and gives &self and announcement their own lifetimes. Then, because one of the parameters is &self, the return type is assigned the lifetime of &self, so all lifetimes have been worked out.
Static lifetime
'static, whose lifetimecanlive for the entire duration of the program.
All string literals have'staticLifecycle
let s: &'static str = “I have a static lifetime.”;
The text of this string is stored directly in the program’s binary file, which is always available. Therefore, all string literals are 'static.
Combining generic type parameters, trait bounds, and lifetimes
1234567891011121314151617 | use std::fmt::Display;fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T,) -> &'a strwhere T: Display,{ println!("Announcement! {}", ann); if x.len() > y.len() { x } else { y }} |
Writing Automated Tests
Test functions in Rust are used to verify that non-test code is behaving as expected. The bodies of test functions typically perform the following three actions:
- Set up any needed data or state (Arrange)
- Run the code that needs to be tested (Act)
- Assert that the results are what we expect
The Anatomy of a Test Function
Writing test functions
A test in Rust is a function annotated with the test attribute. Attributes are metadata about pieces of Rust code.
To turn a function into a test function, add #[test] before the fn line. #[test]
Running tests
- Use the cargo test command to run all test functions.
Rust will build a test runner executable that runs functions annotated with test and reports whether they succeed.
- When you create a library project with cargo, a test module is generated with a test function inside.
You can add any number of test modules or functions.
1 | cargo new 项目名 --lib |
src/lib.rs
1234567891011121314 | pub fn add(left: usize, right: usize) -> usize { left + right}mod tests { use super::*; fn it_works() { let result = add(2, 2); assert_eq!(result, 4); }} |
Run
1 | cargo test |
Test failure
- A test function panicking means failure.
- Each test runs in a new thread.
- When the main thread sees that a test thread has died, that test is marked as failed.
Assertions
Use the assert! macro to check test results.
The assert! macro, from the standard library, is used to determine whether a condition is true.
- true: the test passes.
- false: it calls panic! and the test fails.
123456789101112131415161718192021222324252627282930 | struct Rectangle { width: u32, height: u32,}impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height }}mod tests { use super::*; fn larger_can_hold_smaller() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); }} |
Use assert_eq! and assert_ne! macro to test equality
- Both come from the standard library.
- Determine whether two arguments are equal or not equal.
- In fact, they are assert! macros that use the == and != operators.
- When the assertion fails, it automatically prints the values of the two arguments.
It prints the arguments using the debug format: the arguments are required to implement the PartialEq and Debug traits (all primitive types and most standard library types implement them).
12345678910111213 | pub fn add_two(a: i32) -> i32 { a + 2}mod tests { use super::*; fn it_adds_two() { assert_eq!(4, add_two(2)); }} |
Custom error message
You can pass to assert!, assert_eq! and assert_The ne! macro accepts an optional failure message parameter, which can print the custom failure message along with the test failure.
- The first parameter of the assert! macro is required, and the custom message is the second parameter.
- assert_eq! and assert_The first two parameters of ne! are required, and the custom message is the third parameter.
- The custom message parameter is passed to the format! macro, and {} placeholders can be used.
1234567891011121314 | pub fn greeting(name: &str) -> String { format!("Hello {}!", name)}mod tests { use super::*; fn greeting_contains_name() { let result = greeting("Carol"); assert!(result.contains("Carol")); }} |
Custom error message
123456789 | fn greeting_contains_name() { let result = greeting("Carol"); assert!( result.contains("Carol"), "Greeting did not contain name, value was `{}`", result ); } |
Verify error handling situations
Can verify whether the code panics under specific circumstances.
should_panic Attribute:
- Function panics: test passes
- Function does not panic: test fails
123456789101112131415161718192021222324 | pub struct Guess { value: i32,}impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } }}mod tests { use super::*; fn greater_than_100() { Guess::new(200); }} |
Making should_panic more precise
You can add an optional expected parameter to the should_panic attribute. The test harness will ensure that the error message contains the provided text.
1234567891011121314151617181920212223242526272829 | // --snip--impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!( "Guess value must be greater than or equal to 1, got {}.", value ); } else if value > 100 { panic!( "Guess value must be less than or equal to 100, got {}.", value ); } Guess { value } }}mod tests { use super::*; fn greater_than_100() { Guess::new(200); }} |
Using Result<T, E> in tests
Without needing to panic, you can write tests using Result<T, E> as the return type.
- Returning Ok: test passes
- Returning Err: test fails
1234567891011 | mod tests { fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("two plus two does not equal four")) } }} |
Note:You cannot use the #[should_panic] annotation on these tests that use Result<T, E>.。
Because when they fail, they return Err instead of panicking.
Controlling test execution
Changing the behavior of cargo test: adding command-line arguments
Default behavior:
- Run in parallel
- All tests
- Capture (do not display) all standard output, making it easier to read output related to test results.
Command-line arguments:
- Arguments for cargo test: immediately after cargo test
cargo test --help
- Arguments for the test executable: after –
cargo test – --help
Running tests in parallel
By default, runs in parallel using multiple threads.
To ensure that tests:
- do not depend on each other
- do not depend on shared state (environment, working directory, environment variables, etc.)
–test-threads argument
If you do not want tests to run in parallel, or want more precise control over the number of threads, you can pass the --test-threads argument and the number of threads you want to use to the test binary. For example:
1 | cargo test -- --test-threads=1 |
Here we set the test threads to 1, telling the program not to use any parallelism. This will also take more time than running in parallel, but when there is shared state, tests will not potentially interfere with each other.
Showing function output
By default, if a test passes, Rust’s test library captures all output printed to standard output.
For example, println!:
- If the test succeeds, we will not see the println! output in the terminal.: we will only see the line indicating that the test passed.
- ifIf the test fails, you will see all standard output and other error messages.。
If you also want to see the values printed in passing tests, you can add --show-output at the end to tell Rust to show the output of successful tests.
1 | cargo test -- --show-output |
Running a subset of tests by name
If you run tests without passing any arguments, all tests will run in parallel:
Running a single test
You can pass the name of any test to cargo test to run only that test:
1 | cargo test one_hundred |
Filtering to Run Multiple Tests
We can specify the name of a subset of tests, and any test whose name matches this name will be run. For example, because the first two tests’ names contain add, we can run these two tests with cargo test add:
12345678910 | $ cargo test add Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests (target/debug/deps/adder-92948b65e88960b4)running 2 teststest tests::add_three_and_two ... oktest tests::add_two_and_two ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s |
This runs all tests with add in their names and also filters out the test named one_hundred.
Ignoring Some Tests
Sometimes certain specific tests are very time-consuming to execute, so you want to exclude them during most runs of cargo test.
You can use the ignore attribute to mark time-consuming tests and exclude them, as shown below:
Filename: src/lib.rs
12345678910 | fn it_works() { assert_eq!(2 + 2, 4);}fn expensive_test() { // code that takes an hour to run} |
For the tests you want to exclude, we add the #[ignore] line after #[test]. Now if you run the tests, you’ll see that it_works runs, while expensive_test does not run:
If we only want to run the ignored tests, we can use cargo test – --ignored
1 | $ cargo test -- --ignored |
Test Organization
The Rust community tends to think about tests in terms of two main categories:
Unit Tests(unit tests) and Integration Tests(integration tests)
- Unit tests tend to be smaller and more focused, testing one module at a time in isolation, or testing private interfaces.
- Integration tests, on the other hand, are entirely external to your library. They use your code in the same way as any other external code, testing only public interfaces, and each test may potentially test multiple modules.
Unit Tests
Annotating the test module with #[cfg(test)]
- The code is only compiled and run when you run cargo test
- Running cargo build will not.
- Integration tests are in a different directory, and they do not need the #[cfg(test)] annotation.
cfg: configuration
tells Rust that the following item is only included under the specified configuration option.
Testing Private Functions
There has always been a debate in the testing community about whether private functions should be tested directly, and in other languages testing private functions is difficult or even impossible. However, no matter which testing ideology you adhere to,Rust’s privacy rules do allow you to test private functions。
Filename: src/lib.rs
1234567891011121314151617 | pub fn add_two(a: i32) -> i32 { internal_adder(a, 2)}fn internal_adder(a: i32, b: i32) -> i32 { a + b}#[cfg(test)]mod tests { use super::*; #[test] fn internal() { assert_eq!(4, internal_adder(2, 2)); }} |
Listing 11-12: Testing a private function
Note the internal_adder function is not marked as pub. Also, tests are just another module. As mentioned in the ‘Paths for Referring to an Item in the Module Tree’ section, items in child modules can use items in their ancestor modules. In the test, we bring all items from the test module’s parent module into scope with use super:😗, and then the test calls internal._adder。
Integration Tests
In Rust, integration tests are entirely external to your library.
They use your library in the same way any other code would, which means they can only call functions that are part of your library’s public API. The purpose of integration tests is to test whether many parts of your library work together correctly. Units of code that work correctly on their own could have problems when integrated, soIntegration test coverageis also important.
The tests Directory
- Creating Integration Tests: The tests Directory
- Each test file in the tests directory is a separate crate.
Create an integration test. Keep the src/lib.rs code in the adder example. Create a tests tests directory, and create a new file _tests/integration_integration_test.rs, and enter the code from the example.
123456 | use adder;fn it_adds_two() { assert_eq!(4, adder::add_two(2));} |
- You don’t need to _tests/integration_annotate any code in integration_test.rs with #[cfg(test)]. **The tests directory is a special directory in Cargo.**Cargo will only compile files in this directory when you run cargo test.
- You need to import the library under test.
Running a Specific Integration Test
Running a Specific Integration Test
1 | $ cargo test 函数名 |
Run all tests in a test file
1 | $ cargo test --test 文件名 |
Submodule in Integration Testing
As integration tests increase, you may want to add more files in the tests directory to better organize them, for example, grouping tests by their functionality. As we mentioned earlier, each tests Files in the directory are compiled as separate crates.
Treat each integration test file as its own crate, which helps create separate scopes that provide an environment more similar to how end users use the crate.
For example, if we can create atests/common.rs file and create a function named setup, which we want to be callable by test functions in multiple test files:
File name: tests/common.rs
123 | pub fn setup() { // setup code specific to your library's tests would go here} |
If you run the test again, you will see a new corresponding entry in the test results. common.rs The test result section of the file, even if this file does not contain any test functions and the setup function is not called anywhere:
12345678910111213141516171819202122232425262728 | $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.89s Running unittests (target/debug/deps/adder-92948b65e88960b4)running 1 testtest tests::internal ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)running 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)running 1 testtest it_adds_two ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s |
We don’t want common to appear in the test results showing ‘running 0 tests’. We just want it to be callable from other integration test files.
To prevent ‘common’ from appearing in the test output, we willCreatetests/common/mod.rs ,rather than creatingtests/common.rs . This is a Rust naming convention, and it is named this way.Tell Rust not tocommonTreat as an integration test file. Move the setup function code to tests/common/mod.rs and delete tests/common.rs After the file, this part will not appear in the test output.Subdirectories in the tests directory will not be compiled as separate crates or appear as a test result section in the test output.。
Once you have tests/common/mod.rs, you can use it as a module to be used in any integration test file. Here is a tests/integration_the it in test.rs that calls the setup function_adds_two test examples:
File name: tests/integration_test.rs
123456789 | use adder;mod common;fn it_adds_two() { common::setup(); assert_eq!(4, adder::add_two(2));}//Then in the test function you can call `common::setup()` it. |
Integration Tests for Binary Crates
If the project is a binary crate that only contains src/main.rs and no src/lib.rs:
You cannot create integration tests in the tests directory
Cannot bring the functions in main.rs into scope
Only library crates can expose functions for other crates to use.
A binary crate is meant to run independently.
I/O Project: Building a Command Line Program
Accepting Command Line Arguments
123456 | use std::env;fn main() { let args: Vec<String> = env::args().collect(); println!("{:?}", args);} |
Note:
std::env::args will panic if any of its arguments contain invalid Unicode characters.
If you need to accept arguments containing invalid Unicode characters, use std::env::args_os instead. This functionreturns OsString valuesrather than String values. Here, for simplicity, we use std::env::args because OsString values are different on each platform and are more complex to handle than String values.
Separation of Concerns for Binary Projects
The problem of the main function being responsible for multiple tasks is common in many binary projects. So the Rust community has developed a set of guidelines for separating concerns in binary programs when the main function starts to grow large. These steps are as follows:
- Split the program into main.rs and lib.rs and put the program’s logic into lib.rs。
- When the command line parsing logic is small, it can be kept in main.rs Middle.
- When command line parsing starts to become complex, also extract it from main.rs to lib.rs Middle.
After these steps, the responsibilities remaining in the main function should be limited to:
- Call the command line parsing logic with the argument values
- Set up any other configuration
- Call lib.rs the run function in
- If run returns an error, handle this error
The whole point of this pattern is separation of concerns:main.rs The handler runs, and lib.rs handles all the real task logic. Because the main function cannot be tested directly, this structure moves all the program logic into lib.rs functions so that we can test them. Only the code left in main.rs will be small enough to verify its correctness by reading.
TDD (Test-Driven Development)
Following the Test-Driven Development (TDD) pattern, we will incrementally add the search logic to minigrep. This is a software development technique that follows these steps:
- Write a failing test and run it to make sure it fails for the reason you expect.
- Write or modify enough code to make the new test pass.
- Refactor the code you just added or changed, and make sure the tests still pass.
- Repeat from step 1!
This is just one of many ways to write software, but TDD helps drive the design of the code. Writing tests before writing the code that makes them pass helps maintain high test coverage during development.
Writing minigrep code
src/lib.rs
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 | use std::env;use std::error::Error;use std::fs;pub fn run(config: Config) -> Result<(), Box<dyn Error>> { //Reading a file let contents = fs::read_to_string(&config.filename)?; // println!("With text:\n{}", contents); let results = if config.case_sensitive { search(&config.query, &contents) } else { search_case_insensitive(&config.query, &contents) }; for line in results { println!("{}", line); } Ok(())}pub struct Config { pub query: String, pub filename: String, pub case_sensitive: bool,}impl Config { pub fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let filename = args[2].clone(); // println!("Search for {}", query); // println!("In file {}", filename); let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive, }) }}pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut result = Vec::new(); for line in contents.lines() { if line.contains(query) { result.push(line); } } result}pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut result = Vec::new(); let query = query.to_lowercase(); for line in contents.lines() { if line.to_lowercase().contains(&query) { result.push(line); } } result}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
12345678910111213141516 | use std::env;use std::process;use minigrep::Config;fn main() { let args: Vec<String> = env::args().collect(); // println!("{:?}",args); let config = Config::new(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments:{}", err); process::exit(1); }); if let Err(e) = minigrep::run(config){ eprintln!("Application error: {}",e); process::exit(1); };} |

