Introduction to the

Rust has the same performance as C/C++, but many common bugs can be eliminated at compile time.

Rust is a general-purpose programming language that is good at:

  • High-performance scenario
  • Memory Security Scenario
  • Take advantage of multiprocessor scenarios

While C/C++ performance is very good, the type system and memory are not very safe.

Java/C# has a good GC, is memory safe, and has a lot of good features, but relatively low performance.

Rust’s areas of expertise:

  • High-performance Web Services
  • Webassembly
  • Command line tool
  • Network programming
  • Embedded programming
  • System programming

Installation and Development Environment Configuration (Mac)

Visit the official website, click “Install”, and then run the command given on the website:

curl https://sh.rustup.rs -sSf | sh
Copy the code

However, the command may be unavailable on a domestic network. Therefore, configure the domestic source first.

export RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static
export RUSTUP_UPDATE_ROOT=https://mirrors.sjtug.sjtu.edu.cn/rust-static/rustup
Copy the code

However, there are also offline installation solutions available.

On the Install Rust screen, click Other Installation Methods and select x86_64-apple-Darwin. After downloading it, you can install it directly.

Verify installation:

rustc --version
Copy the code

Once installed, develop with VSCode, and then install a plug-in “rust-analyzer” for VSCode.

Hello World

Create file main.rs and write:

fn main() {
    println!("Hello World");
}
Copy the code

Compile (XCode must be installed before compiling) :

rustc main.rs
Copy the code

Run:

./main
Copy the code

Cargo

Rustc is good enough for small projects, but for big projects we need Cargo. Rust is a build system and package management tool for building code, downloading dependency libraries, and building code.

Cargo is usually installed automatically when Rust is installed. You can check if Cargo is installed by typing some commands:

cargo --version
Copy the code

Create projects using Cargo

cargo new hello_cargo
Copy the code

After execution, a hello_cargo directory is created with the SRC directory, Cargo. Toml,.gitignore, etc.

The TOML (Tom’s Obvious, Minimal Language) format is Cargo’s configuration format.

In Rust, the package for the code is called Crate, the official address.

Build the Cargo project

Build the project using the following command:

cargo build
Copy the code

This will generate an executable file: target/debug/hello_cargo. Run the following command:

./target/debug/hello_cargo
Copy the code

The first run of the build generates a cargo. Lock file in the top-level directory, which is responsible for keeping track of project dependencies and exact versions, and generally does not require manual modification.

To build for publication, add the –release argument:

cargo build --release
Copy the code

Optimized at compile time, the code runs faster, but it takes longer to compile. Once built, the executable will be generated in the: target/release/ directory.

Running Cargo project

The + project can be compiled and executed using the following command:

cargo run
Copy the code

If it has been compiled before and the source code has not changed, it will run directly.

Check the code

You can use the following command to check the code without compiling. The code that passes the check is guaranteed to compile, but does not produce an executable, and is much faster than building:

cargo check
Copy the code

Upgrading dependency packages

The following command will update our dependencies, ignore the configuration of Cargo. Lock, and look for the specified version from the dependencies of Cargo.

cargo update
Copy the code

Guess the number of games

use rand::Rng;
use std::cmp::Ordering;
use std::io; // prelude; trait

fn main() {
    println!("Guessing game!");
    let secret_number = rand::thread_rng().gen_range(1.101); // i32 u32 i64
    // println! (" Secret number is: {}", secret_number);
    
    loop {
        println!("Guess a number.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess).expect("Unable to read rows");

        // shadow
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) = >continue};println!("Your guess is: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break; }}}}Copy the code

Variables and variability

let

Declare variables. By default, declared variables are Immutable.

If you want a variable to be mutable, you need to define it with the muT keyword in front of it.

fn main() {
    println!("Hello World");
    
    let x = 5;
    // The following code will not fail after this declaration
    // let mut x = 5;
    println!("The value of x is {}", x);
    
    // The following code will report an error
    // x = 6;
}
Copy the code

const

Constant (constant), a constant is immutable after binding to its value, but it differs from immutable variables in many ways:

  • Do not usemutConstants are always immutable
  • Declare constants using the const keyword, whose type must be annotated
  • Constants can be declared in any scope, including global scope
  • Constants can only be bound to constant expressions, not to the result of a function call or to a value that can only be evaluated at run time

Constants remain valid in their declared scope as long as the program is running.

Naming conventions: all caps, separated by underscores, e.g. MAX_POINTS, const MAX_POINTS: u32 = 100_000;

const MAX_POINTS: u32 = 100 _000;

fn main() {
    println!("Max points is {}", MAX_POINTS);
}
Copy the code

Shadowing (hide)

You can declare a new variable with the same name, and the new variable will shadow the previously declared variable with the same name.

fn main() {
    let x = 5;
    // The bottom x will hide the top x
    let x = x + 1;
    println!("The value of x is {}", x);
}
Copy the code

Shadowing types are mutable, muT cannot do this:

fn main() {
    let spaces = "";
    let spaces = spaces.len();
    println!("{}", spaces); / / 4
}
Copy the code

The data type

scalar

Integer types

Unsigned starts with u and signed starts with I.

For example, u32: an unsigned integer ranging from 0 to 2 to the 32 power minus 1, occupying a 32-bit space.

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 number of isize and USize bits depends on the architecture of the computer, which is 64-bit for a 64-bit computer and 32-bit for a 32-bit computer, and is rarely used.

Integer literals:

Number literals Example
Decimal 98 _222
Hex 0xff
Octal 0o77
Binary 0b111_0000
Byte(u8 only) b’A’

All numeric literals except byte allow type suffixes, such as 57u8.

The default integer type is i32.

Integer overflow:

Such as: The u8 variable ranges from 0 to 255. If you set a U8 variable to 256 and compile in debug mode, it may be detected that the program will panic at run time. If you compile in release mode (–release), it will not detect the possible panic errors. After an overflow occurs Rust performs a “wrap” operation: 256 becomes 0, 257 becomes 1, and so on.

Floating point types

Rust has two basic floating point types: F32 (single precision) and F64 (double precision). Floating point types use the IEEE-754 standard and are f64 by default.

fn main() {
    let x = 2.0;
    let y: f32 = 3.0;
}
Copy the code

Boolean type

The size of one byte is false and true only.

fn main() {
    let x = 2.0;
    let y: f32 = 3.0;
}
Copy the code

Character types

The char type is used to describe the most basic single character. Literals of character types use single quotes, take up four bytes, and are Unicode scalar values.

fn main() {
    let x = 'x';
    let y: char = 'y';
    letZ = '😂';// win +. Enter emoticons
}
Copy the code

The compound type

The Tuple tuples

You can put values of multiple types into a single type.

fn main() {
    let tup: (i32.f64.u8) = (500.2.0.1);
    
    let (x, y, z) = tup;
    
    println!("{}, {}, {}", tup.0, tup.1, tup.2);
    
    println!("{}, {}, {}", x, y, z);
}
Copy the code

An array of

Multiple values can be placed in a single type, but the values must be of the same type, and the array length is fixed at declaration time.

fn main() {
    let arr = [1.2.3.4.5];
    let a: [i32.5] = [1.2.3.4.5];
    let b = [3; 5]; // equivalent to [3, 3, 3, 3, 3]
    
    println!("{}", arr[0]);
}
Copy the code

Arrays are better if you want your data to be stored in stack memory rather than heap memory, or if you want to have a fixed number of elements.

function

Functions are declared using the fn keyword, and Rust uses the Snake Case naming convention for functions and variable names, where all letters are lowercase and words are separated by underscores.

fn main() {
    println!("hello world");
    another_function();
}

// Another_function does not have to be defined before it is called. This is different from C/C++, which is closer to JS
fn another_function() {
    println!("Another function");
}
Copy the code

Function parameters

fn main() {
    another_function(5.6); // argument
}

// Another_function does not have to be defined before it is called. This is different from C/C++, which is closer to JS
fn another_function(x: i32, y: i32) { // parameter
    println!("{}", x, y);
}
Copy the code

The difference between statements and expressions:

fn main() {
	let x = 5;
	let y = {
        let x = 1;
        x + 1 // y = x + 1
        x + 1; // The semicolon is a statement that returns an error
    }
    
    println!("{}", y);
}
Copy the code

Function return value:

Return if you want to return early, or return the last expression if you don’t.

fn five -> i32{5}fn plus_five(x: i32) - >i32 {
    x + 5 // No semicolons
}
fn main() {
	let x = five();
	let y = plus_five(6);
    
    println!("{}, {}", x, y);
}
Copy the code

annotation

// Single-line comment
/* Multiline comment */
Copy the code

The control flow

If expression

fn main() {
	let x = 3;
	if number < 5 {
        println!("true");
    } else {
         println!("false"); }}Copy the code
Else if = match
fn main() {
	let x = 6;
	if number % 4= =0 {
        println!("a");
    } else if number % 3= =0 {
         println!("b");
    } else {
        println!("c"); }}Copy the code
fn main() {
	let condition = true;
    
    let number = if condition { 5 } else { 6 };
    
    let number = if condition { 5 } else { "6" }; // Error reported, type incompatible
    
    println!("{}", number);
}
Copy the code

cycle

loop

Run a piece of code over and over again until you say “break”.

fn main() {
	let mut counter = 0;
    
    let result = loop {
        counter += 1;
        
        if counter == 10 {
            break counter * 2; }}println!("{}", result);
}
Copy the code

while

fn main() {
	let mut number = 3;
    
    whilenumber ! =0 {
        println!("{}", number);
        
        number = number - 1; }}Copy the code

for

fn main() {
    let a = [10.20.30.40.50];
    for element in a.iter() { / / the iterator
        println!("{}", element); }}Copy the code

For loops are safer and faster than while and loop.

Range

fn main() {
    for number in (1.4).rev() { / / (1.. 4) is a Range, representing 1, 2, 3 (note: 4 is not included), and rev reverses the Range
        println!("{}", element); }}Copy the code

The ownership of

The core feature of Rust is ownership. All programs must manage how they use the computer’s memory at run time.

Some languages, such as Java, are their own GC, constantly looking for memory they no longer use while the program is running, while others, such as C++, require the programmer to explicitly allocate and free memory.

Rust takes a new approach: memory is managed through an ownership system with a set of rules that the compiler checks at compile time. The ownership feature does not slow down the application when it is running.

Stack vs Heap

Stack memory versus heap memory. In a system-level programming language like Rust, whether a value is on the stack or on the heap has a much greater impact on the behavior of the language and why you make certain decisions.

Store the data

Stack is last in, first out. All data stored on the Stack must have a known fixed size. Data whose size is unknown at compile time or whose size may change at run time must be stored on the heap.

Heap memory is less organized. When you put data into the Heap, you request a certain amount of space. When the operating system finds a large enough space in the Heap, marks it as in use, and returns a pointer to the address of that space, this process is called allocating on the Heap. Sometimes it’s just called “distribution.”

Pushing a value onto the stack is not allocation, because Pointers are known to be of fixed size and can be placed on the stack. But if you want the actual data, you have to use Pointers to locate it.

Pushing data onto the stack is much faster than allocating it on the heap, because the operating system doesn’t have to find space to store new data, which is always at the top of the stack.

Allocating space on the heap takes more work. The operating system first has to find a space large enough to store the data, and then records it for next allocation.

To access the data

Accessing the data in the heap is slower than accessing the data in the stack because you need a pointer to find the data in the heap. With modern processors, the fewer times an instruction jumps in memory, the faster it will be because of the cache.

If the data is stored close together, the processor will process it faster (on the stack);

If the data is stored far away, the processor will process it slower (on the heap); It also takes time to allocate a large amount of space on the heap.

A function call

When your code calls a function, the value is passed to the function (including a pointer to the heap). The local variables of the function are pushed onto the stack. When the function ends, these values pop up from the stack.

Why ownership exists

Problems solved by ownership:

  • Track which parts of the code are using which heap data
  • Minimize the amount of duplicate data on the heap
  • Clean up unused data on the heap to avoid running out of space

Once you understand ownership, you don’t need to think about stack or heap as much.

But knowing that managing heap data is why ownership exists helps explain why it works the way it does.

Ownership rule

  • Each value has a variable that is the owner of the value

  • Each value can have only one owner at a time

  • This value is removed when the owner is out of scope

fn main() {
    // Unlike char, a scalar, String is the data type allocated to the heap
    let mut s = String::from("hello");
    
    s.push_str(", world");
    
    println!("{}", s);
}
// When out of scope, Rust automatically calls the drop function, and s is reclaimed
Copy the code
fn main() {
    let mut s1 = String::from("hello");
    
    let s2 = s1;
    
    println!("{}", s1); // An error is reported because in Rust, s1 is invalidated after assignment (the moved value s1 is borrowed) and only s2 can be used
}
Copy the code

The above assignment of S1 to S2 is different from the traditional shallow copy and deep copy, so we use a new term to express it: Move.

fn main() {
    let mut s1 = String::from("hello");
    
    let s2 = s1.clone;
    
    println!("{}, {}", s1, s2);
}
Copy the code

Both s1 and S2 are now available.

Quote and borrow

The & symbol represents References, allowing you to refer to values without taking ownership of them.

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("{}, {}", s1, len);
}
// This function uses a reference to s1, which does not transfer ownership of S1 and does not affect ownership of S1
fn calculate_length(s: &String) - >usize {
    s.len()
}
Copy the code

The act of taking a reference as a function parameter is called borrowing.

Borrowed things, if the referenced variable is mutable, then borrowed variable is mutable, otherwise immutable.

fn main() {
    let mut s1 = String::from("hello");
    let len = calculate_length(&mut s1);
    println!("{}, {}", s1, len);
}

fn calculate_length(s: &mut String) - >usize {
    s.len()
}
Copy the code

In addition, there is only one mutable reference for a piece of data (there can be many immutable references). This prevents data contention at compile time (two or more Pointers accessing the same data at the same time; At least one pointer is used to write data; No mechanism is used to synchronize access to the data.

fn main() {
    let mut s = String::from("hello");
    let s1 = &mut s;
    let s2 = &mut s; / / complains
    println!("{}, {}", s1, s2);
}
Copy the code
// So there is no error
fn main() {
    let mut s = String::from("hello");
    { let s1 = &mut s; }
    let s2 = &mut s;
    println!("{}, {}", s1, s2);
}
Copy the code

You cannot have both a mutable reference and an immutable reference.

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    let s1 = &mut s; / / complains
    println!("{}, {}", r1, r2, s1);
}
Copy the code

Dangling References A pointer that refers to an address in memory that may have been freed and assigned to someone else.

fn main() {
    let r = dangle();
}

fn dangle() - > &String {
    let s = String::from("hello"); // s is released after the function completes execution, but this function returns a reference to s
    &s
}
Copy the code

Rust generates an error at compile time to prevent the dangling pointer bug.

The ownership of

The core feature of Rust is ownership. All programs must manage how they use the computer’s memory at run time.

Some languages, such as Java, are their own GC, constantly looking for memory they no longer use while the program is running, while others, such as C++, require the programmer to explicitly allocate and free memory.

Rust takes a new approach: memory is managed through an ownership system with a set of rules that the compiler checks at compile time. The ownership feature does not slow down the application when it is running.

Stack vs Heap

Stack memory versus heap memory. In a system-level programming language like Rust, whether a value is on the stack or on the heap has a much greater impact on the behavior of the language and why you make certain decisions.

Store the data

Stack is last in, first out. All data stored on the Stack must have a known fixed size. Data whose size is unknown at compile time or whose size may change at run time must be stored on the heap.

Heap memory is less organized. When you put data into the Heap, you request a certain amount of space. When the operating system finds a large enough space in the Heap, marks it as in use, and returns a pointer to the address of that space, this process is called allocating on the Heap. Sometimes it’s just called “distribution.”

Pushing a value onto the stack is not allocation, because Pointers are known to be of fixed size and can be placed on the stack. But if you want the actual data, you have to use Pointers to locate it.

Pushing data onto the stack is much faster than allocating it on the heap, because the operating system doesn’t have to find space to store new data, which is always at the top of the stack.

Allocating space on the heap takes more work. The operating system first has to find a space large enough to store the data, and then records it for next allocation.

To access the data

Accessing the data in the heap is slower than accessing the data in the stack because you need a pointer to find the data in the heap. With modern processors, the fewer times an instruction jumps in memory, the faster it will be because of the cache.

If the data is stored close together, the processor will process it faster (on the stack);

If the data is stored far away, the processor will process it slower (on the heap); It also takes time to allocate a large amount of space on the heap.

A function call

When your code calls a function, the value is passed to the function (including a pointer to the heap). The local variables of the function are pushed onto the stack. When the function ends, these values pop up from the stack.

Why ownership exists

Problems solved by ownership:

  • Track which parts of the code are using which heap data
  • Minimize the amount of duplicate data on the heap
  • Clean up unused data on the heap to avoid running out of space

Once you understand ownership, you don’t need to think about stack or heap as much.

But knowing that managing heap data is why ownership exists helps explain why it works the way it does.

Ownership rule

  • Each value has a variable that is the owner of the value

  • Each value can have only one owner at a time

  • This value is removed when the owner is out of scope

fn main() {
    // Unlike char, a scalar, String is the data type allocated to the heap
    let mut s = String::from("hello");
    
    s.push_str(", world");
    
    println!("{}", s);
}
// When out of scope, Rust automatically calls the drop function, and s is reclaimed
Copy the code
fn main() {
    let mut s1 = String::from("hello");
    
    let s2 = s1;
    
    println!("{}", s1); // An error is reported because in Rust, s1 is invalidated after assignment (the moved value s1 is borrowed) and only s2 can be used
}
Copy the code

The above assignment of S1 to S2 is different from the traditional shallow copy and deep copy, so we use a new term to express it: Move.

fn main() {
    let mut s1 = String::from("hello");
    
    let s2 = s1.clone;
    
    println!("{}, {}", s1, s2);
}
Copy the code

Both s1 and S2 are now available.

Quote and borrow

The & symbol represents References, allowing you to refer to values without taking ownership of them.

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("{}, {}", s1, len);
}
// This function uses a reference to s1, which does not transfer ownership of S1 and does not affect ownership of S1
fn calculate_length(s: &String) - >usize {
    s.len()
}
Copy the code

The act of taking a reference as a function parameter is called borrowing.

Borrowed things, if the referenced variable is mutable, then borrowed variable is mutable, otherwise immutable.

fn main() {
    let mut s1 = String::from("hello");
    let len = calculate_length(&mut s1);
    println!("{}, {}", s1, len);
}

fn calculate_length(s: &mut String) - >usize {
    s.len()
}
Copy the code

In addition, there is only one mutable reference for a piece of data (there can be many immutable references). This prevents data contention at compile time (two or more Pointers accessing the same data at the same time; At least one pointer is used to write data; No mechanism is used to synchronize access to the data.

fn main() {
    let mut s = String::from("hello");
    let s1 = &mut s;
    let s2 = &mut s; / / complains
    println!("{}, {}", s1, s2);
}
Copy the code
// So there is no error
fn main() {
    let mut s = String::from("hello");
    { let s1 = &mut s; }
    let s2 = &mut s;
    println!("{}, {}", s1, s2);
}
Copy the code

You cannot have both a mutable reference and an immutable reference.

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    let s1 = &mut s; / / complains
    println!("{}, {}", r1, r2, s1);
}
Copy the code

Dangling References A pointer that refers to an address in memory that may have been freed and assigned to someone else.

fn main() {
    let r = dangle();
}

fn dangle() - > &String {
    let s = String::from("hello"); // s is released after the function completes execution, but this function returns a reference to s
    &s
}
Copy the code

Rust generates an error at compile time to prevent the dangling pointer bug.

slice

Another data type of Rust that does not hold ownership is the slice.

The following code returns the position of a space in a string.

fn main() {
	let mut s = String::from("hello world");
    let wordIndex = first_word(&s);
    
    // s.clear(); // If you add this wordIndex, the validity is not guaranteed. If you use it later, you may get a bug, but you will not get an error when compiling
    println!("{}", wordIndex);
}

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()
}
Copy the code

However, there is a fatal problem with this code, which is that wordIndex validity is not guaranteed, and is prone to bugs.

However, string slicing solves this problem.

fn main() {
	let mut s = String::from("hello world");
    let hello = &s[0.5]; / / section
    let world = &s[6.11];
    
    /* let hello = &s[..5]; Let world = &s[6..] ; Index let whole = &s[..] ; // The entire string can start or end without */
    
    println!("{}", wordIndex);
}
Copy the code

Transform the above function:

fn main() {
	let mut s = String::from("hello world");
    let wordIndex = first_word(&s);
    
    s.clear(); // An error will be reported
    println!("{}", wordIndex);
}

fn first_word(s: &String) - > &str {
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}
Copy the code

The string literal is a slice of type & STR.

fn main() {
	let s = String::from("hello world");
    letwordIndex = first_word(&s[..] );let my_str = "hello world";
    let wordIndex2 = first_word(my_str);
}

fn first_word(s: &str) - > &str { // This is a better way to make our function more generic
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}
Copy the code

Other types of slices:

fn main() {
    let a = [1.2.3.4.5];
    let slice = &a[1.3];
}
Copy the code