The Rust Programming Language - HsuJv/Note GitHub Wiki

Rust

Hello, World

Installation

sudo pacman -S rustup
rustup default nightly
cargo install racer

echo "export CARGO_HOME=\"$HOME/.cargo\"" >> ~/.bashrc
echo "export RUSTBINPATH=\"$CARGO_HOME/bin\"" >> ~/.bashrc
echo "export RUST=\"$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu\"" >> ~/.bashrc
echo "export RUST_SRC_PATH=\"$RUST/lib/rustlib/src/rust/src\"" >> ~/.bashrc
echo "export PATH=$PATH:$RUSTBINPATH" >> ~/.bashrc
source ~/.bashrc

Hello, World

cargo new hello_world --bin
cd hello_world
cargo build
cargo run
  • You will get a main.rs in ./hello_world/src, codes in it are:
fn main() {
    println!("Hello, world!");
}

Anatomy of a Rust Program

fn main() {

}
  • These lines define a function in Rust.
  • The main function is the entry of the whole program
  • The () has the parameter tables in it
  • The whole function body is enclosed in a pair of curly bracket {}
  • println!(...);:
    • println! calls a macro while println calls a function. Using a ! in rust means that a macro is called instead of a normal function
    • rust ends the line with a semicolon(;)

Cargo

  • Cargo is Rust's build system and package manager.

  • In the hello_world directory, we've also got Cargo.lock and Cargo.toml

  • Cargo.toml:

    [package]
    name = "hello_world"
    version = "0.1.0"
    authors = ["********"]
    edition = "2018"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [dependencies]
    • This file is in the TOML (Tom's Obvious, Minimal Language) format.
    • [package] section is the package configuring.
    • [dependencies] section is a section that used to list any of this project's dependencies. In Rust, packages of code are refferred to as creates.
  • cargo build: build a binary in ./target/debug/

  • cargo run: run the binary built before

  • cargo check: check the code but do not build, which is much faster than cargo build

  • cargo build --release: instead build in ./target/debug with debug symbols, this will build the target in ./target/release with optimization and no symbols in.

Programming a Guessing Game

use std::io;

fn main() {
    println!("Guess the number!");
    println!("Please input your guess.");

    let mut guess = String::new();
    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");
    println!("You guessed: {}", guess);
}

Anatomy of the Guessing Game

  • use std::io;: First bing the io library into scope, which comes from the standard library (aka std)
  • fn main() { : the entry of the whole program
  • The following two println! print the hints on the screen
  • let mut guess = String::new():
    • let foo = bar;: creates a new variable named foo and binds it to the value of bar. In Rust, variable are immutable by default. So mut is needed for vars that mutable.
    • String::new(): a function that returns a new instance of a String (a string type provided by the standard library that is a growable, UTF-8 encoded bit of text.)
    • ::: calls an associated function (aka static method) of a type.
  • io::stdin().read_line(&mut guess).expect("Failed to read line");:
    • io::stdin(): calls the stdin function from the io module. The stdin function returns an instance of std::io::Stdin, which is a type that represents a handle to the standard input for the terminal.
    • .read_line(&mut guess): calls the read_line method on the standard input handle to get input from the user. We're also passing one argument to read_line: &mut guess. The argument guess needs to be mutable so the method can change its value.
    • &: indicates that this argument is a reference. One of Rust’s major advantages is how safe and easy it is to use references.
    • .expect(): read_line returns a value which is the type of io::Result. Rust has a number of types named Result in its standard library: a generic Result as well as specific versions for submodules, such as io::Result. The Result types are enums, consists of Ok or Err. An instance of io::Result has an except method. If this instance of io::Result is an Err value, except will cause the program to crash and display the message that passed as an argument. If this instance of io::Result is an Ok value, expect will take the return value that Ok is holding and return just that value to you so you can use it.
    • println!("You guessed: {}", guess);: The {} are known as placeholders

Generating a Secret Number

Using a Create to Get More Functionality

  • A create is a collection of Rust source code files. The project we've built before is a binary create, which is an executable.
  • The rand create is a library create, which contains code intended to be used in other programs.
  • Before rand create is used, it should be declared as a dependency in Cargo.toml
[dependencies]
rand = "0.3.14"
  • Cargo understands Semantic Versioning, which is a standard for writing version numbers. The number 0.3.14 is actually shorthand for ^0.3.14, which means 'any version that has a public API compatible with version 0.3.14'
  • Now that we have an external dependency, Cargo fetches the latest versions of everything from the registry, which is a copy of data from Crates.io. Crates.io is where people in the Rust ecosystem post their open source Rust projects for others to use.
  • After updating the registry, Cargo checks the [dependencies] section and downloads any crates you don’t have yet.

Ensuring Reproducible Builds with the Cargo.lock file

  • When first run cargo build, Cargo figures out all the versions of the dependencies that fit the criteria and then writes them to the Cargo.lock file.

Updating a Crate to Get a New Version

  • Cargo provides a command cargo update, which will ignore the Cargo.lock file and figure out all the latest versions that fits in Cargo.toml.
  • Another Cargo.lock will be created if the update works.

View all APIs in a crate

  • There's no need to remember which traits to use and which methods and functions to call from a crate.
  • cargo doc --opne will help create a document that contains all APIs from the crates specified in Cargo.toml

Generate the Secret Number

use rand::Rng;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_num = rand::thread_rng().gen_range(1, 100);
    println!("The secret number is: {}", secret_num);

    println!("Please input your guess.");

    let mut guess = String::new();
    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");
    println!("You guessed: {}", guess);
}
  • rand::thread_rng() returns a particular randon number generator that is local to the current thread of excution and seeded by the OS.
  • gen_range(start, end) returns a random integer that in [start, end).

Comparing the Guess to the Secret Number

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_num = rand::thread_rng().gen_range(1, 100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Input a number please.");
                continue;
            }
        };
        println!("You guessed: {}", guess);
        match guess.cmp(&secret_num) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big"),
            Ordering::Equal => {
                println!("You win");
                break;
            }
        }
    }
}
  • A match expression is made up of arms. An arm consists of a pattern and the code that should be run if the value given to the beginning of the match expression fits that arm's pattern.
  • Number in Rust defaults to an i32, which is also the type of secret_num. So we should parse the variable guess from String to integer (i32, u32, i64, u64, etc.) first.
  • Rust allows us to shadow the previous value of guess with a new one. This feature is used in situations in which a value should be converted from one type to another type.
  • In this case, we convert the variable guess from String to integer without create another unique variables.
  • The colon(:) after guess tells Rust the variable's type.
  • If the parse is not able to turn the string into a number, it will return an Err value that contains mor information about the error, which will match the pattern Err(_).
    • The underscore, _, is a catchall value.

Common Programming Concepts

Variables and Mutability

  • In Rust, the compiler guarantees that when you state that a value won’t change, it really won’t change.
  • Variables are immutable only by default; mut can be annotated to allow this variable to be changed.

Differeces Between Variables and Constants

  • Being unable to change the value of a variable is similar to another concept that known as constants. But there are a few differences between constants and variables.
  • First, a constant cannot be annotated with mnt, it is always immutable.
  • A constant is declared with keyword const instead of let, and the type of the value must be annotated.
  • Constants can be declared in any scope, including the global scope.
  • Constants may be set only to a constant expression, not the result of a function call or any other value that could only be computed at runtime.

Shadowing

  • As is shown in Comparing the Guess to the Secret Number, a new variable can be declared with the same name as a previous variable, and the tnew one shadows the previous one.
  • Shadowing is different from marking a variable as mut, because a compile-time error will occurs if we accidentally try to reassign to this variable without using let keyword.
  • The other difference between mut and shadowing is that a brand new variable is created when let is used and the type of the variable can be changed.

Data Types

Scalar Type

  • A scalar type represents a single value

  • Rust has for primary scalar types: integers, floating-point numbers, Booleans, and characters.

Inter Types

Integer Types in Rust
Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch (32 for 32-bits OS, and 64 for 64-bits OS) isize usize
Integer Literals in Rust
Number literals Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte b'A'

Floating-Point Types

Floating-Point Types in Rust
Length Types
32-bit(float) f32
64-bit(double) f64

The Boolean Type

  • a Boolean type in Rust has two possible values: true and flase.

  • Booleans are one byte in size. Specified using bool

The Character Type

  • Rust's char type is the language's most primitive alphabetic type.
  • char literals are specified with single quotes, as opposed to string literals, which use double.
  • Rust's char type is four bytes in size, representing a Unicode Scalar Value.

Compound Types

  • Compound types can group multiple values into one type.
  • Rust has two primitive compound types: tuples and arrays.

Tuple Type

  • A tuple has fixed length: once declared, it cannot grow or shrink in size.
  • A tuple is created like following: let tup: (i32, f64, u8) = (600, 1.1, 243);
  • Pattern matching is used to destructure a tuple value, like following: let (x, y, z) = tup; // x = 600, y = 1.1, z = 243
  • Access to tuple elements needs a period ., like following:let x1 = tup.0; // x1 = 600

The Array Type

  • Every element of an array must have the same type.
  • Arrays in Rust have a fixed length, like tuples
  • An array is created like following:
let a = [1, 2, 3, 4, 5];
let b = ["12", "34", "56"];
let c: [i32, 5] = [1, 2, 3, 4, 5];
let d = [3; 5]; // [3, 3, 3, 3, 3]
  • Acess to array elements needs a bracket [], like following: let first = a[0];
  • A variable in the bracket is also allowed: let one_element = a[index]

Functions

  • Rust defines a function with keyword fn
fn main() {
    println!("Hello, world!");
}

Function Parameters

  • Functions can take parameters, which should be specified its type while defining.
fn foo(x: i32) {
    println!("x is {}", x);
}

Function Returns

  • The return value is declared after an arrow(->)

  • A return can be used anywhere in a function to return the value.

  • But most functions return the last expression implicitly.

fn five() -> i32 {
    5
}

fn six() -> i32 {
    return 6;
}
  • Especially, if the last line of the function is a statement rather than expression, an empty tupe will be returned.
fn foo() -> i32 {
    6;
}

error[E0308]: mismatched types
 --> src/main.rs:5:13
  |
5 | fn foo() -> i32 {
  |    ---      ^^^ expected i32, found ()
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
6 |     6;
  |      - help: consider removing this semicolon
  |
  = note: expected type `i32`
             found type `()`

Control Flow

if Expressions

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}
  • Blocks of code associated with the conditions in if expressions are sometimes called arms, just like the arms in match expressions.

  • The condition in if expressions must be a bool.

if - else if

  • Just like the other programming language

if in a let

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    println!("The value of number is: {}", number);
}
  • Like the ternary expression ?: in C/CPP

  • the two operands must be in the same type

Repetition with Loops

  • Rust has three kinds of loops: loop, while, for

loop

  • The loop keyword just goes into an infinite loop to execute a block until a break

  • The break keyword can return values from a loop

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

while

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

for

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

fn main() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

Understanding Ownership

  • Ownership enables Rust to make memory safety guarantees without needing a garbage collecto (aka GC).

What is Ownership

  • Memory is managed through a system of ownership with a set of rules that the compiler checks at compile time. It won't slow down the program.

Ownership Rules

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Ways Variables and Data Interact: Move

let s1 = String::from("hello");
let s2 = s1;
  • To ensure memory safety, there’s one more detail to what happens in this situation in Rust. Instead of trying to copy the allocated memory, Rust considers s1 to no longer be valid and, therefore, Rust doesn’t need to free anything when s1 goes out of scope. Check out what happens when you try to use s1 after s2 is created; it won’t work:
println!("{}, world!", s1);



error[E0382]: use of moved value: `s1`
 --> src/main.rs:5:28
  |
3 |     let s2 = s1;
  |         -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`, which does
  not implement the `Copy` trait

Ways Variables and Data Interact: Clone

  • Simply like the deep clone.
let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

Stack-Only Data: Copy

  • Rust has a special annotation called the Copy trait that we can place on types like integers that are stored on the stack. If a type has the Copy trait, an older variable is still usable after assignment. Rust won’t let us annotate a type with the Copy trait if the type, or any of its parts, has implemented the Drop trait. If the type needs something special to happen when the value goes out of scope and we add the Copy annotation to that type, we’ll get a compile-time error.

  • Types with Copy trait: simply think all groups of simple scalar values can be Copy

    • All the integer types, such as u32.
    • The Boolean type, bool, with values true and false.
    • All the floating point types, such as f64.
    • The character type, char.
    • Tuples, if they only contain types that are also Copy. For example, (i32, i32) is Copy, but (i32, String) is not.

Onwership and Funcitons

  • Passing a variable to a function will move or copy, just as assignment does.
fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it’s okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
  • Here s cannot be use after takes_ownership is called

Return Values and Scope

  • Return values can also transfer ownership.
fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
  // moved, so nothing happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("hello"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

// takes_and_gives_back will take a String and return one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}
  • Taking ownership and then returning ownership with every function is a bit tedious. In this case, references are using.

References and Borrowing

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()
}
  • &s1 is passed into calculate_length and, in its definition, &String is taken rather than String

  • These ampersands are references, which allow some one to refer to some value without taking ownership of it.

  • We don’t drop what the reference points to when it goes out of scope because we don’t have ownership.

  • If we want to change the value of reference, &mut must be declared

Mutable References

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
  • This way, the reference to s has be mut, so it can be changed in the function change

  • But you can have only one mutable reference to a particular piece of data in a particular scope. This code will fail:

  • These codes will fail:

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

error[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 here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here
  • This restriction has earned Rust the benefit of preventing data races at compile time.

  • A similar rule exists for combining mutable and immutable references. We also cannot have a mutable reference while we have an immutable one.

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);



error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here
  • But these codes do work, because a reference's scope starts from where it is introduced and continues through the last time that is used.
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

Dangling References

  • There's no danging pointer in Rust!
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

The Rules of References

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

Slices

  • Another data type that does not have ownership is the slice.

  • Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection.

String Slice

  • The type that signifies "string slice" is write as &str
fn main() {
let s = String::from("hello world");

let hello = &s[0..5];
let hello1 = &s[..5];
let world = &s[6..11];
let world1 = &s[6..];
let hello_world = &s[..];
let 1st_world = first_world(&s);
}


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[..]
}
  • Also It's impossible to keep the slice of a string while clearing the string itself.
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!
    // clear is declared as :
    // fn clear(&mut self) {
    //     self.vec.clear()
    // }

    println!("the first word is: {}", word);
}


error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {}", word);
   |                                       ---- immutable borrow later used here

String Literals Are Slices

  • String literals being stored inside the binary, rather than alloced in the heap.

  • The type of string literals are &str. It also explains why the string literals cannot be modified

String Slices as Parameters

  • fn first_word(s: &String) -> &str { is better to be written as fn first_word(s: &str) -> &str {

  • If we have a string slice, we can pass it directly. If we have a String, we can pass a slice of the entire String. This will make our API more general and useful.

Summary

  • The concepts of ownership, borrowing, and slices ensure memory safety in Rust programs at compile time.

Using Structs to STructure Related Date

Defining and Instantiating Struct

struct User {
 username: String,
 email: String,
 sign_in_count: u64,
 active: bool,
}

fn main() {
    let mut user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
        };
    user1.email = String::from("[email protected]");
}
  • Note that the entire instance must be mutable -- Rust doesn't allow us to mark only sertain fields as mutable.

Using the Field Init Shorthand when Variables and Fields Have the Same Name

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

Create Instances From Other Instances

let user2 = User {
    email: String::from("[email protected]"),
    username: String::from("anotherusername567"),
    ..user1
};

Using Tuple Structs without Named Fields

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

Unit-LIke Structs Without Any Fields

  • You can also define structs that don’t have any fields! These are called unit-like structs because they behave similarly to (), the unit type.

  • Unit-like structs can be useful in situations in which you need to implement a trait on some type but don’t have any data that you want to store in the type itself.

Ownership of Struct Data

  • In the User struct definition, we used the owned String type rather than the &str string slice type. This is a deliberate choice because we want instances of this struct to own all of its data and for that data to be valid for as long as the entire struct is valid.

  • In Generic Types, Traits, and Lifetimes we will fix these errors with the lifetime feature.

Method Syntax

  • Methods are similar to functions, but methods are always defined within the context of a struct(or enum, tarit object), and the first of their parameter is always self

Defining Methods

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

-> Operator

  • In C and C++, two different operators are used for calling methods: you use . if you’re calling a method on the object directly and -> if you’re calling the method on a pointer to the object and need to dereference the pointer first. In other words, if object is a pointer, object->something() is similar to (*object).something().

  • Rust has a feature called automatic referencing and dereferencing instead of -> operator

  • when you call a method with object.something() , Rust automatically adds in & , &mut , or * so object matches the signature of the method, so the following codes are the same: `p1.distance(&p2); (&p1).distance(&p2);

  • The fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice.

Associated Functions

  • We're allowed to define functions within impl blocks that don't take self as a parameter

  • That is called associated functions (aka, static functions). :: is used to call them.

Enums and Pattern Matching

  • Rust's enums are most similar to algebraic data types in functional languages

Defining an Enum

Enum Values

enum IpAddrKind {
    v4,
    v6,
}

struct IpAddr {
    IpAddrKind,
    address: String,
}

fn main() {
    let home = IpAddr {
        kind: IpAddrKind::V4,
        address: String::from("127.0.0.1"),
    }
}
enum IpAddr {
    v4(u8, u8, u8, u8),
    v6(String),
}

impl IpAddr {
    fn disp(&self) {
        println!("Called here");
    }
}

fn main() {
    let home = IpAddr::v4(127, 0, 0, 1);
    let loopback: IpAddr = IpAddr::v6(String::from("::1"));
    home.disp();
}

The Option Enum and Its Advantages Over Null Values

  • The Option type is used in many places because it encodes the very common scenario in which a value could be something or it could be nothing.

  • In languages with null, variables can always be in one of two states: null or not-null. But Rust doesn't have athe null feature.

  • Rust uses Options instead.

enum Option<T> {
    Some(T),
    None,
}
#![allow(unused_variables)]
fn main() {
let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;
}
  • The Option<T> enum is so useful that it’s even included in the prelude; you don’t need to bring it into scope explicitly. In addition, so are its variants: you can use Some and None directly without the Option:: prefix. The Option<T> enum is still just a regular enum, and Some(T) and None are still variants of type Option<T>.

The match Control Flow Operator

  • Rust has an extremely powerful control flow oeprator called match that allows you to comare a value against a series of patterns and then execute code based on matches.

  • Patterns can be made up of literal values, variable names, wildcards, and many other thins, see Patterns and Matching

Patterns that Bind to Values

#[derive(Debug)] // so we can inspect the state in a minute
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
        },
    }
}

Matching with Option<T>

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

THe _ Placeholder

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

Concise Control Flow with if let

  • The if let syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest.
let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}


if let Some(3) = some_u8_value {
    println!("three");
}
let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

Managing Growing Projects with Packages, Crates, and Modules

-Packages: A Cargo feature that lets you build, test, and share crates -Crates: A tree of modules that produces a library or executable -Modules and use: Let you control the organization, scope, and privacy of paths -Paths: A way of naming an item, such as a struct, function, or module

Packages and Crates

  • A crate is a binary of library.
  • The crate root is a source file that the Rust compiler starts from and makes up the root module of this crate section
  • A package is one or more crates that provide a set of functionality.
  • A package contains a Cargo.toml file that describe s how to build those crates
  • Cargo follows a convention that src/main.rs is the crate root of a binary crate with the same name as the package.
  • Likewise, Cargo knows that if the package directory contains src/lib.rs, the package contains a library crate with the same name as the package, and src/lib.rs is its crate root.

Defining Modules to Control Scope and Privacy

  • Modules let us organize code within a crate into groupes.
  • Modules also control the privacy of items, which is whether an item can be used by outside code(public) or is an internal implementation detail and not available for outside use(private)
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:
crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

Paths for Referring to an Item in the Module Tree

  • A path can take two forms:
    • An absolute path starts from a crate root by using a crate name or a literal crate
    • A relative path starts from the current module and uses self, super or an identifier in the current module.
  • Both of them are followed by one or more identifiers separated by ::
  • Our preference is to specify absolute paths because it’s more likely to move code definitions and item calls independently of each other.
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Starting Relative Paths with super

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }

    fn cook_order() {}
}

Making Structs and Enums Public

  • Structs
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() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // meal.seasonal_fruit = String::from("blueberries");
}
  • Enum
mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Bringing Paths into Scope with the use Keyword

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

use front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Providing New Names with the as Keyword

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
}

fn function2() -> IoResult<()> {
    // --snip--
}

Re-exporting Names with pub use

  • When we bring a name into scope with the use keyword, the name available in the new scope is private. To enable the code that calls our code to refer to that name as if it had been defined in that code’s scope, we can combine pub and use. This technique is called re-exporting because we’re bringing an item into scope but also making that item available for others to bring into their scope.

Using Nested Paths to Clean Up Large use Lists

use std::{cmp::Ordering, io};
use std::io::{self, Write};

The Glob Operator

use std::collections::*;

Common Collections

  • Unlike the built-in array and tuple types, the data collections point to is stored on the heap.

Vectors

let mut v: Vec<i32> = Vec::new();
let mut v = vec![1, 2, 3];
v.push(4);
v.push(5);
let third: &i32 = &v[2];
match v.get(2) {
    Some(third) => println!("The third elements is {}", third),
    None => println!("There is no third element."),
}

for i in &v {
    println!("{}", i);
}
for i in &mut v{
    *i += 50; // To change the value that the mutable reference refers to, we have to use the dereference operator (*)
}

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];
  • When the program has a valid reference, the borrow checker enforces the ownership and borrowing rules to ensure this reference and any other references to the contents of the vector remain valid. Recall the rule that states you can't have mutable and immutable references in the same scope.
let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6);


error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {}", first);
  |                                          ----- immutable borrow later used here

String

let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");


let mut s = String::from("foo");
s.push_str("bar");

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used


let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{}-{}-{}", s1, s2, s3);

Hash Maps

Create

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

use std::collections::HashMap;

let teams  = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];

let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();

Hash Maps and Ownership

use std::collections::HashMap;

let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point

Accessing Values in a Hash Map

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 (key, value) in &scores {
    println!("{}: {}", key, value);
}

Update Hash Map

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
// Overwriting
scores.insert(String::from("Blue"), 25);

// insert if key has no value
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50); // do nothing

// based on the old
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;
}

Error Handling

  • Errors are grouped into two major categories: recoverable and unrecoverable

  • Rust has the type Result<T, E> for recoverable errors and the panic! macro that stops execution when the program encounters an unrecoverable error.

Unrecoverable Errors with panic!

fn main() {
    panic!("crash and burn");
}
  • To switch from unwinding the stack to just aborting, add `[profile.release] \n panic = 'abort' to Cargo.toml

Recoverable Errors with Result

Prototype

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Usage

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),
        },
    };
}
  • There's a more clear writting using closure

Shortcuts for Pacnic on Error

  • unwrap is a shortcut method:
use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

// ---------- the same as ----------
use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!(error)
        },
    };
}
  • except used in the same way as unwarp, but we can pass arguments to the except to pass to panic!.

Propagating Errors

  • It's better to return an error in a function rather than just handle it, since we don't have enough information on what the calling code is actually trying to do.

  • Use ? operator as a shortcut for propagating errors.

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}

// ------- Use API

use std::io;
use std::fs;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
  • NOTE, the ? operator can only be used in functions that return Result

To panic! or Not to panci!

  • It's appropriate to panic in examples, prototype code, and tests

  • Cases in which you have more information than the compiler: It would also be appropriate to call unwrap when you have some other logic that ensures the Result will have an Ok value, but the logic isn’t something the compiler understands.

Guidelines for Error Handling

  • It’s advisable to have your code panic when it’s possible that your code could end up in a bad state. In this context, a bad state is when some assumption, guarantee, contract, or invariant has been broken, such as when invalid values, contradictory values, or missing values are passed to your code—plus one or more of the following:

    • The bad state is not something that’s expected to happen occasionally.
    • Your code after this point needs to rely on not being in this bad state.
    • There’s not a good way to encode this information in the types you use.
  • If someone calls your code and passes in values that don’t make sense, the best choice might be to call panic! and alert the person using your library to the bug in their code so they can fix it during development. Similarly, panic! is often appropriate if you're calling external code that is out of your control and it returns an invalid state that you have no way of fixing.

  • However, when failure is expected, it's more appropriate to return a Result than to make a panic! call.

  • When your code performs operations on values, your code should verify the values are valid first and panic if the values aren’t valid. This is mostly for safety reasons: attempting to operate on invalid data can expose your code to vulnerabilities.

Generic Types, Traits, and Lifetimes

Generic

In Struct Definitions

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 };
}


// -----------------------
struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

In Enum Definitions

enum Result<T, E> {
    Ok(T),
    Err(E),
}

In Method Definitions

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}
struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        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);
}

Traits

  • Traits are similar to a feature often called interfaces in other languages, although with some differences.

Define a Trait

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }

    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

Traits as Parameters

pub fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

pub fn notify(item: impl Summary + Display) { }

pub fn notify<T: Summary + Display>(item: T) { }

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{ }

Traits as Returns

fn returns_summarizable() -> impl Summary {}

Using Trait Bounds to Conditionally Implement Functions

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);
        }
    }
}
  • The type Pair<T> always implements the new function. But Pair<T> only implements the cmp_display method if its inner type T implements the PartialOrd trait that enables comparison and the Display trait that enables printing.

  • We can also conditionally implement a trait for any type that implements another trait. Implementations of a trait on any type that satisfies the trait bounds are called blanket implementations

impl<T: Display> ToString for T {
    // --snip--
}

Validating References with lifetimes

The Borrow Checker

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+
  • The program is rejected because 'b is shorter than 'a: the subject of the reference doesn't live as long as the reference
{
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+
  • Here, x has the lifetime 'b, which is in this case larger than 'a

Lifetime Annotation Syntax

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

Lifetime Annotations in Function Signatures

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
  • The function signature tells Rust that the string slice returned from the function will live at least as long as lifetime 'a.

  • In practice, it means that the lifetime of the reference returned by the longest function is the same as the smaller of the lifetimes of the references passed in.

Lifetime Annotations in Struct Definitions

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 annotation means an instance of ImportantExcerpt can’t outlive the reference it holds in its part field.

Lifetime Annotations in Method Definitions

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}
  • Lifetime names for struct fields always need to be declared after the impl keyword and then used after the struct’s name, because those lifetimes are part of the struct’s type.

Lifetime Elision

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
  • The compiler uses three rules to figure out what lifetimes references have when there aren’t explicit annotations.
    • each parameter that is a reference gets its own lifetime parameter.
    • if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
    • if there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters.

The Static Lifetime

let s: &'static str = "I have a static lifetime.";

Generic Type Parameters, Trait Bounds, and Lifetimes Together

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Writing Automated Tests

How to Write Tests

  • Set up needed data or state
  • Run the code you want to test
  • Assert the results are what you expected
cargo new adder --lib


#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}
cargo test
running 2 tests
test tests::exploration ... ok
test tests::another ... FAILED

failures:

---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:10:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.

failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed
  • Checking for Panics with should_panic
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
        }
    }
}

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

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}


// -----cargo test
running 1 test
test tests::greater_than_100 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  • Using Result<T, E> in Tests
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

// ----- cargo test
running 1 test
test tests::greater_than_100 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  • The attribute cfg stands for configuration and tells Rust that the following item should only be included given a certain configuration option. In this case, the configuration option is test, which is provided by Rust for compiling and running tests.

Controlling How Tests Are Run

Running test in Parallel or Consecutively

$cargo test -- --test-threads=1

Showing Function Output

fn prints_and_returns_10(a: i32) -> i32 {
    println!("I got the value {}", a);
    10
}

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

    #[test]
    fn this_test_will_pass() {
        let value = prints_and_returns_10(4);
        assert_eq!(10, value);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(5, value);
    }
}


// ------- cargo test
cargo test
running 2 tests
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:19:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.

failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

// -------- cargo test -- --nocapture
cargo test -- --nocapture --test-threads=1
running 2 tests
test tests::this_test_will_fail ... I got the value 8
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
FAILED
test tests::this_test_will_pass ... I got the value 4
ok

failures:

failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

Running a Subtest by name

pub fn add_two(a: i32) -> i32 {
    a + 2
}

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

    #[test]
    fn add_two_and_two() {
        assert_eq!(4, add_two(2));
    }

    #[test]
    fn add_three_and_two() {
        assert_eq!(5, add_two(3));
    }

    #[test]
    fn one_hundred() {
        assert_eq!(102, add_two(100));
    }
}

// ------- cargo test one_hundred
running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out


// ------ cargo test add
running 2 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out

Ignoring Some Tests Unless Specifically Requested

#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
    // code that takes an hour to run
}

Integration Tests

  • Integration tests are entirely external to your library
  • Integration tests 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 tests directory at the top level tells Cargo that it contains integration tests.
  • Each file in the tests directory is compiled as its own separate crate.
  • Files in subdirectories of the tests directory don’t get compiled as separate crates or have sections in the test output.

Functional Language Features: Iterators and Closures

Closueres: Anonymous Functions that Can Capture Their Environment

  • The same as lambdas in python.
fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

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

Closure Type Inference and Annotation

  • Closures don’t require you to annotate the types of the parameters or the return value like fn functions do.

  • But we can do this annotation explicitly

let expensive_closure = |num: u32| -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

// a vertical comparison of the syntax for
// the definition of a function that adds 1 to its parameter
// and a closure that has the same behavior.

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

Storing Closures Using Generic Parameters and the Fn Traits

  • All closures implement at least one of the traits: Fn, FnMut, or FnOnce.

  • So we can add the closure to a struct and the implement a cache for it.

struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    calculation: T,
    pub value: HashMap<u32, u32>,
}

impl<T> Cacher<T>
where
    T: Fn(u32) -> u32,
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: HashMap::new(),
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        // to use or_insert api, the lambda function will be called first
        // self.value.entry(arg).or_insert((self.calculation)(arg))
        if let Entry::Vacant(_) = self.value.entry(arg) {
            self.value.insert(arg, (self.calculation)(arg));
        }
        self.value[&arg]
    }
}

fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num + 1
    });

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

Capturing the Enviroment with Closures

fn main() {
    let x = 4;
    let equal_to_x = |z| z == x;
    let y = 4;
    assert!(equal_to_x(y));
}

// -----------------------------

fn main() {
    let x = 4;
    fn equal_to_x(z: i32) -> bool { z == x }
    let y = 4;
    assert!(equal_to_x(y));
}

error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
} closure form instead
 --> src/main.rs
  |
4 |     fn equal_to_x(z: i32) -> bool { z == x }
  |  
  • Closures can capture values from their environment in three ways, which directly map to the three ways a function can take a parameter: taking ownership, borrowing mutably, and borrowing immutably. These are encoded in the three Fn traits as follows:

    • FnOnce: consumes the variables it captures from its enclosing scope, known as the closure’s environment. To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined. The Once part of the name represents the fact that the closure can’t take ownership of the same variables more than once, so it can be called only once.
    • FnMut: can change the environment because it mutably borrows values.
    • Fn: borrows values from the environment immutably.
  • If you want to force the closure to take ownership of the values it uses in the environment, you can use the move keyword before the parameter list.

Processing a Series of Items with Iterators

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

// --------------------------
let v1_iter = v1.iter();
for val in v1_iter {
    println!("Got: {}", val);
}

// --------------------------
let mut v1_iter = v1.iter();

assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);

// ---------------------------
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum(); // ownership passed
assert_eq!(total, 6);

// ---------------------------

let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); // the comsuming(collect) is necessary
assert_eq!(v2, vec![2, 3, 4]);


// ----------------------------
#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter()
        .filter(|s| s.size == shoe_size)
        .collect()
}

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

    let in_my_size = shoes_in_my_size(shoes, 10); // ownership passed

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

Comparing Performance: Loop vs. Iterators

  • Conclusion first: The iterator version is slightly faster.

  • The point is this: iterators, although a high-level abstraction, get compiled down to roughly the same code as if you’d written the lower-level code yourself. Iterators are one of Rust’s zero-cost abstractions, by which we mean using the abstraction imposes no additional runtime overhead.

  • This is analogous to how Bjarne Stroustrup, the original designer and implementor of C++, defines zero-overhead in “Foundations of C++” (2012):

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

More about Cargo and Crates.io

Release Publish

  • First, Distinguishing production and debug, with the optimize level.
[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3
  • Second, making useful doc commnet: /// and the markdown.
    • follow the convention, sections should be list as
      • # Examples
      • # Panics: The scenarios in which the function being documented could panic. Callers of the function who don’t want their programs to panic should make sure they don’t call the function in these situations.
      • # Errors: If the function returns a Result, describing the kinds of errors that might occur and what conditions might cause those errors to be returned can be helpful to callers so they can write code to handle the different kinds of errors in different ways.
      • # Safety: If the function is unsafe to call, there should be a section explaining why the function is unsafe and covering the invariants that the function expects callers to uphold.
    • use //! instead of /// to add doc to items which contains the them.
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
// ------------------------------------

/// Adds one to the number given.
///
/// # Examples
///
/// ```Rust
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}
  • Third, using pub use to re-export the APIs

  • Forth, setting up a crates.io Account:

    • Before you can publish any crates, you need to create an account on crates.io and get an API token. To do so, visit the home page at crates.io and log in via a GitHub account. (The GitHub account is currently a requirement, but the site might support other ways of creating an account in the future.) Once you’re logged in, visit your account settings at profile and retrieve your API key. Then run the cargo login command with your API key, like this: cargo login *****
    • a login info will be stored @ ~/.cargo/credentials
  • Fifth, adding metadata to a new crate

[package]
name = "********"
version = "0.1.0"
authors = ["Your Name <[email protected]>"]
edition = "2018"
description = ""
license = "****"
  • Sixth, publish it with : cargo publish

  • To publish a new version of an existing crate, just change the [version] value and re-publish

  • To remove versions from crates.io, use cargo yank --vers a.b.c

    • undo it: cargo yank --vers a.b.c --undo

Cargo Workspaces

  • A workspace is a set of packages that share the same Cargo.lock, and output directory.
$mkdir add
$cd add
$touch Cargo.toml

; ----------Cargo.toml
[workspace]

members = [
    "adder",
    "add-one",
]
; --------------------

$cargo new adder
$cargo new add-one --lib

; ----------tree
├── Cargo.lock
├── Cargo.toml
├── add-one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target
; --------------

; ---------adder/Cargo.toml
[dependencies]

add-one = { path = "../add-one" }
; -------------------------

Installing Binaries from Crates.io

$ cargo install ripgrep
Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading ripgrep v0.3.2
 --snip--
   Compiling ripgrep v0.3.2
    Finished release [optimized + debuginfo] target(s) in 97.91 secs
  Installing ~/.cargo/bin/rg

Smart Pointers

  • A difference between references and smart pointers is that references are pointers that only borrow data; in contrast, in many cases, smart pointers own the data they point to.

Using Box<T> to Point to Data on the Heap

  • Boxes don’t have performance overhead, other than storing their data on the heap instead of on the stack. But they don’t have many extra capabilities either. You’ll use them most often in these situations:
    • When you have a type whose size can’t be known at compile time and you want to use a value of that type in a context that requires an exact size
    • When you have a large amount of data and you want to transfer ownership but ensure the data won’t be copied when you do so
    • When you want to own a value and you care only that it’s a type that implements a particular trait rather than being of a specific type

Usage of Box<T>

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

Enabing Recursive Type with Boxes

  • cons list
    • A cons list is a data structure that comes from Lisp
    • The cons function (short for “construct function”) constructs a new pair from its two arguments, which usually are a single value and another pair. These pairs containing pairs form a list.
    • The cons function concept has made its way into more general functional programming jargon: “to cons x onto y” informally means to construct a new container instance by putting the element x at the start of this new container, followed by the container y.
enum List {
    Cons(i32, Box<List>),
    Nil,
}

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

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

Treating Smart Pointer Like Regular References with the Deref Trait

  • Implementing the Deref trait allows you to customize the behavior of the dereference operator, * (as opposed to the multiplication or glob operator).
use std::ops::Deref;

struct MyBox<T>(T);

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

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

    fn deref(&self) -> &T {
        &self.0
    }
}

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

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
  • The type Target = T; syntax defines an associated type for the Deref trait to use.

  • We fill in the body of the deref method with &self.0 so deref returns a reference to the value we want to access with the * operator.

  • interacts with mutability:

    • Rust does deref coercion when it finds types and trait implementations in three cases:
      • From &T to &U when T: Deref<Target=U>
      • From &mut T to &mut U when T: DerefMut<Target=U>
      • From &mut T to &U when T: Deref<Target=U>
    • The first two cases are the same except for mutability.
    • For the third case: Rust will also coerce a mutable reference to an immutable one. But the reverse is not possible.

Running Code on Cleanup with the Drop Trait

struct CustomSmartPointer {
    data: String,
}

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

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

// ---------------------------------
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
  • Rust doesn’t let you call the Drop trait’s drop method manually; instead you have to call the std::mem::drop function provided by the standard library if you want to force a value to be dropped before the end of its scope.

    • For example, drop a lock.
    fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
    }

Rc<T>, the Reference Counted Smart Pointer

  • To enable multiple ownership, Rc<T> is used, which is an abbreviation for reference counting.
    • The Rc<T> type keeps track of the number of references to a value which determines whether or not a value is still in use.
    • If there are zero references to a value, the value can be cleaned up without any references becoming invalid.
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

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

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}
  • The implementation of Rc::clone doesn’t make a deep copy of all the data like most types’ implementations of clone do.
    • The call to Rc::clone only increments the reference count, which doesn’t take much time.
    • We could have called a.clone(), but just use Rc::clone following the convention to make a difference

RefCell<T> and the Interior Mutability Pattern

  • Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data; normally, this action is disallowed by the borrowing rules.
    • To mutate data, the pattern uses unsafe code inside a data structure to bend Rust’s usual rules that govern mutation and borrowing.
    • The unsafe code involved is then wrapped in a safe API, and the outer type is still immutable.
  • Unlike Rc<T>, the RefCell<T> type represents single ownership over the data it holds.
    • With references and Box<T>, the borrowing rules’ invariants are enforced at compile time. With RefCell<T>, these invariants are enforced at runtime.
    • The advantage of checking the borrowing rules at runtime instead is that certain memory-safe scenarios are then allowed, whereas they are disallowed by the compile-time checks. Static analysis, like the Rust compiler, is inherently conservative.
    • Similar to Rc<T>, RefCell<T> is only for use in single-threaded scenarios and will give you a compile-time error if you try using it in a multithreaded context.
  • Box<T>, Rc<T>, or RefCell<T>:
    • Rc<T> enables multiple owners of the same data; Box<T> and RefCell<T> have single owners.
    • Box<T> allows immutable or mutable borrows checked at compile time; Rc<T> allows only immutable borrows checked at compile time; RefCell<T> allows immutable or mutable borrows checked at runtime.
    • Because RefCell<T> allows mutable borrows checked at runtime, you can mutate the value inside the RefCell<T> even when the RefCell<T> is immutable.

A Use Case for Interior Mutability: Mock Objects

  • A test double is the general programming concept for a type used in place of another type during testing. Mock objects are specific types of test doubles that record what happens during a test so you can assert that the correct actions took place.

Having Multiple Owners of Mutable Data by Rc<T> and RefCell<T>

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

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

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

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

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

    *value.borrow_mut() += 10;

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

Reference Cycles

  • Rust’s memory safety guarantees make it difficult, but not impossible, to accidentally create memory that is never cleaned up (aka memory leak).
use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

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

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

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

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

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

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

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


// -------------------------------
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

Preventing Reference Cycles

  • Turning Rc<T> into a Weak<T>, which is cleaned up with no guarantee that the weak_count is zero by calling Rc::downgrade.

  • Strong references are how you can share ownership of an Rc<T> instance. Weak references don’t express an ownership relationship. They won’t cause a reference cycle because any cycle involving some weak references will be broken once the strong reference count of values involved is 0.

  • Use upgrade on a Weak<T> to ensure that the reference not dropped.

Fearless Concurrency

  • Handling concurrent programming safely and efficiently is another of Rust’s major goals.

Using Threads to Run Code Simultaneously

  • Rust standard library only provides an implementation of 1:1 threading (which means, one operating system thread per one language thread)

  • Creating a New Thread with spawn

use std::thread;
use std::time::Duration;

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

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}
  • Waiting Thread with join
use std::thread;
use std::time::Duration;

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

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

    handle.join().unwrap();
}
  • IPC with channel
use std::thread;
use std::sync::mpsc;

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

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap(); // pass the ownership
    });

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

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

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

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

    for received in rx {
        println!("Got: {}", received);
    }
}
  • IPC with Mutex<T>
use std::sync::Mutex;

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

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

    println!("m = {:?}", m);
}
  • Mutex<T> cannot be store in a Rc<T> and passed to different threads as it is unsafe.

  • Arc<T> is a type like Rc<T> that is safe to use as it is atomic.

use std::sync::{Mutex, Arc};
use std::thread;

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

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

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

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

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

Extensible Concurrency

  • Transfer ownership between threads with Send
    • The Send marker trait indicates that ownership of the type implementing Send can be transferred between threads.
    • Almost every Rust type is Send, but there are some exceptions, including Rc<T>
  • Access from multiple threads with Sync
    • The Sync marker trait indicates that it is safe for the type implementing Sync to be referenced from multiple threads.
    • If some type T have its reference &T be Send, the T should be Sync
    • Rc<T> is not Sync, RefCell<T> and its family Cell<T> neither.
  • Types made up of Send and Sync are also Send and Sync
    • do not implement those trait manuallay.

OOP

  • Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data.

  • If a language must have inheritance to be an object-oriented language, then Rust is not one. There is no way to define a struct that inherits the parent struct’s fields and method implementations.

  • You can share Rust code using default trait method implementations instead.

    • Trait objects are more like objects in other languages in the sense that they combine data and behavior. But trait objects differ from traditional objects in that we can’t add data to a trait object.
  • Object Safety Is Required for Trait Objects

    • The return type isn’t Self.
    • There are no generic type parameters.

Patterns and Matching

All the Places Patterns Can Be Used

  • if let
fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

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

stack.push(1);
stack.push(2);
stack.push(3);

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

Refutability: Whether a Pattern Might Fail to Match

  • Patterns come in two forms: refutable and irrefutable.

    • Patterns that will match for any possible value passed are irrefutable.
    • Patterns that can fail to match for some possible value are refutable.
  • Function parameters, let statements, and for loops can only accept irrefutable patterns, because the program cannot do anything meaningful when values don’t match.

  • The if let and while let expressions only accept refutable patterns, because by definition they’re intended to handle possible failure: the functionality of a conditional is in its ability to perform differently depending on success or failure.

Pattern Syntax

let x = 1;
match x {
    Some(0) => println!("Got 0"),
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    4..=7 => println!("four through seven"),
    _ => println!("anything"),
}

// -------------------------------
struct Point {
    x: i32,
    y: i32,
}

let p = Point { x: 0, y: 7 };

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

// -------------------------------
let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

// -------------------------------
let numbers = (2, 4, 8, 16, 32);

match numbers {
    (first, .., last) => {
        println!("Some numbers: {}, {}", first, last);
    },
}

// -------------------------------
enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello { id: id_variable @ 3..=7 } => {
        println!("Found an id in range: {}", id_variable)
    },
    Message::Hello { id: 10..=12 } => {
        println!("Found an id in another range")
    },
    Message::Hello { id } => {
        println!("Found some other id: {}", id)
    },
}

Advanced

Unsafe

  • Unsafe Rust exists because, by nature, static analysis is conservative.

  • To switch to unsafe Rust, use the unsafe keyword and then start a new block that holds the unsafe code. You can take four actions in unsafe Rust, called unsafe superpowers, that you can’t in safe Rust. Those superpowers include the ability to:

    • Dereference a raw pointer
    • Call an unsafe function or method
    • Access or modify a mutable static variable
    • Implement an unsafe trait
    • Access fields of unions

Dereferencing a Raw Pointer

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;


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


let address = 0x012345usize;
let r3 = address as *const i32;
let r4 = address as *mut i32;

let slice: &[i32] = unsafe {
    slice::from_raw_parts_mut(r4, 10000)
};

extern "C" {
    fn abs(input: i32) -> i32;
}

unsafe {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}

// on the contrary, extern a c function
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

// change a const variable
static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}
  • Equivalent Syntax in C language
const int num1 = 5;
int num2 = 5;
uintptr_t address = 0x012345;

int &r1 = num1;
int &r2 = num2;
int r3 = *static_cast<const int*>(address);
int r4 = *static_cast<int*>(address);

extern int32_t abs(int32_t);
printf("Absolute value of -3 according to C: %d", abs(-3));

extern "C" {
    void call_from_c() {
        printf("Just called a C++ function from C!");
    }
}

const int COUNTER = 0;

void add_to_count(uint32_t inc) {
    int *p = const_cast<int*>(&COUNTER);
    *p += inc;
}

Advanced Types

type Kilometers = i32;

let x: i32 = 5;
let y: Kilometers = 5;


type Thunk = Box<dyn Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));


type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: Arguments) -> Result<()>;
}

Advanced Function and Closures

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {}", answer);
}

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

Macros

  • The term macro refers to a family of features in Rust: declarative macros with macro_rules! and three kinds of procedural macros
  • Three kinds of procedural macros:
    • Custom #[derive] macros that specify code added with the derive attribute used on structs and enums
    • Attribute-like macros that define custom attributes usable on any item
    • Function-like macros that look like function calls but operate on the tokens specified as their argument
  • Difference between Macros and Functions
    • A function signature must declare the number and type of parameters the function has.
    • Macros, on the other hand, can take a variable number of parameters
    • Also, macros are expanded before the compiler interprets the meaning of the code, so a macro can, for example, implement a trait on a given type. A function can’t, because it gets called at runtime and a trait needs to be implemented at compile time.
    • you must define macros or bring them into scope before you call them in a file, as opposed to functions you can define anywhere and call anywhere.

Declarative Macros

  • The most widely used form of macros in Rust is declarative macros. At their core, declarative macros allow you to write something similar to a Rust match expression.

  • Definition for vec!

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

Procedural Macros

  • procedural macros, which act more like functions (and are a type of procedure). Procedural macros accept some code as an input, operate on that code, and produce some code as an output rather than matching against patterns and replacing the code with other code as declarative macros do.

  • The three kinds of procedural macros (custom derive, attribute-like, and function-like) all work in a similar fashion.

  • When creating procedural macros, the definitions must reside in their own crate with a special crate type.

  • derive Macro

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}


// ----- hello_macro.lib

pub trait HelloMacro {
    fn hello_macro();
}

// ----- hello_macro_derive.lib
// Cargo.toml
[lib]
proc-macro = true

[dependencies]
syn = "0.14.4"
quote = "0.6.3"

// lib.rs
extern crate proc_macro;

use crate::proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}", stringify!(#name));
            }
        }
    };
    gen.into()
}
  • Attribute-like macros
#[route(GET, "/")]
fn index() {


#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
  • Function-like macros
let sql = sql!(SELECT * FROM posts WHERE id=1);

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

Appendix

A-Keywords

B-Operators and Symbols

C-Derivable Traits

D-Useful Development Tools

⚠️ **GitHub.com Fallback** ⚠️