Rust

The Rust programming language is a modern compiled programming language similar to C/C++.

The best way to learn Rust from scratch is via the book!

Async

Attributes

Cargo

Cargo is Rust's build system and package manager.

Publicly available crates can be found at crates.io!

• Build

$ cargo build compiles your code in ./srs and saves the results to ./target/debug by default. This command also creates a Cargo.lock file in the base directory. This file is used to keep track of the exact versions of dependencies in the project.

Flags:

  • --release: Compiles the source code with optimizations. Saves the results to target/release

• Check

$ cargo check makes sure your project contains valid Rust code (parses the code and checks for syntax errors).

• Clippy

Clippy is a linter for Rust code. Use $ cargo clippy to lint your code. Use $ rustup component add clippy to install Clippy first.

• Doc

Rust comes with documentation comments. To use documentation comments, start a line with ///. These comments support markdown notation and can be converted to HTML using cargo doc. The documents will be saved to target/doc

The other documentation comment is //!. This type of comment is used to add comments to what it is in (like crates) as opposed to what is after the comment (a function). These are common at the beginning of a file.

Example (in math/src/lib.rs):

//! # Math
//!
//! `math` is a collection of mathematical functions

/// Squares the given number
///
/// # Examples
///
/// ```
/// let square = math::square(5);
///
/// assert_eq!(25, square);
/// ```
/// ```
/// let square1 = math::square(1);
///
/// asswert_eq!(1, square1);
/// ```
pub fn square(x: i32) -> i32 {
    x * x
}
Flags
  • --open: Build and open the HTML documentation
Sections
  • Errors: If a Result is returned, what kind of errors might occur and how are they caused
  • Examples: Example usage
  • Panics: What scenarios causes the function to panic
  • Safety: If unsafe is used, explain why and the variants the caller must uphold
Tests

cargo test actually tests the code left in documentation comments!

• Lock

Cargo.lock is used in conjunction with your dependences in Cargo.toml. The Cargo.lock file locks in the version of your dependences when $ cargo build was first executed with the dependency. With this lock, if a newer minor release or other release for the dependency is added, your project will continue to use the old release.

• New

Use $ cargo new <project_name> to create a new Rust project. This command will create a nested directory containing a Cargo.toml file and a srs/ directory containing a basic source file. In addition, it will initialize a git repository for the project.

The Cargo.toml file is Cargo's configuration file for packages. In Rust, packages of code are called "crates".

Flags:

  • --lib: Initialize a library (make src/lib.rs)

• Run

$ cargo run both compiles your rust code and runs the resulting executable.

• Rustfix

Fix rust code (i.e. compiler warnings) using $ cargo fix.

• Rustfmt

You can automatically format rust code using $ cargo fmt. You will first need to install rustfmt using $ rustup component add rustfmt.

• Tests

Cargo has a tests suit. To run, use cargo test.

cargo test accepts two types of parameters separated by --. The flags that go before the -- are for cargo test while the flags that go after -- are for your test suit. For help you can either do cargo test --help for help with cargo test or cargo test -- --help for help with what you can pass to your test suit.

Parallel Tests

By default, tests run in parallel, so it can be dangerous if they use the same resources, like an opened file. To make them run in sequential order, use cargo test -- --test-threads=1.

Show Output

By default, output to stdout from your code for tests will not be shown. To show test output do cargo test -- --show.

Running a Subset of Tests

To run a subset of the test suit, use cargo test <function name match> where cargo will run any tests that have your inputted string in the test function name.

Ignore Tests

To ignore some tests add #[ignore] before the test function but after #[test]. These tests will be ignored unless you do cargo test -- --ignored.

• Toml

Cargo.toml is Cargo's configuration file for packages. It contains several sections.

Dependencies

The dependencies section ([dependencies]) contains the dependencies for the package. For example:

[dependencies]
rand = "0.8.3"

This will import the rand external crate with the version number of 0.8.X where "X" is 3 or greater. The last number if considered a minor release. Any release with the same first and secondary number will have the same public API and will be compatible.

Profiles

Profiles allows you to customize how your code is compiled during development and release times. Example:

[profile.dev]
opt-level = 0

• Update

$ cargo update is used to update the crates in the Cargo.lock file. It uses the dependencies found in the Cargo.toml file and updates the Cargo.lock file to the newest appropriate dependency.

Closures

Closures are similar to functions except they can be stored in a variable and can capture the surrounding variables.

Example:

fn main() {
    // Normal Closure
    let add_one = |x| {
        x + 1
    };

    // Closures are of a single concrete type
    let y = add_one(1); // The type x is deduced to be an i32
    //let y = add_one(1.0); // Does not compile because x should be an i32, not f32

    // Type information optional
    let verbose_add_one = |x: i32| -> i32 {
        x + 1
    };

    // {} optional if single expression
    let one_linner = |x| x + 1;
    let z = one_linner(2);
}

• Returning Closures

Closures can be returned from functions via a pointer.

fn returns_closure() -> Box<dyn Fn(u32) -> u32> {
    Box::new(|x| x * x)
}

• Traits

Closures can implement some or all of three traits:

  • Fn: Borrows the values from the environment immutably
  • FnMut: Mutably borrow values
  • FnOnce: Consumes the variables it captures from its environment. This closure can be called only once because it consumes the variables.
Fn
struct Cacher<T>
where
    T: Fn(i32) -> i32,
{
    func: T,
    value: Option<i32>,
}

impl<T> Cacher<T>
where
    T: Fn(i32) -> i32,
{
    fn new(func: T) -> Cacher<T> {
        Cacher{
            func,
            value: None,
        }
    }

    fn value(&mut self, arg: i32) -> i32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.func)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

fn main() {
    let add = 5;
    let mut saved_value = Cacher::new(|x| x + add);
    println!("{}", saved_value.value(2));   // 7
    println!("{}", saved_value.value(738)); // 7, because cached
}

Collections

• Hashmaps

HashMaps are like unordered maps in C++. Docs.

use std::collections::HashMap;

fn main() {
    let mut occur = HashMap::new();

    // All keys must be of the same type and all values must be of the same type
    occur.insert(String::from("Blue"), 12);
    occur.insert(String::from("Red"), 5);

    let teams = vec![String::from("Blue"), String::from("Pink")];
    let scores = vec![5, 78];

    let mut scores: HashMap<_, _> =
        teams.into_iter().zip(scores.into_iter()).collect();
        // Teams and scores becoming iterators. Zip combines them into _, _ iterator
        // Collect transforms the iterator into a container, in this case, a HashMap

    // Hash maps copy for copy traited types, moved for others
    let key = String::from("Moved Key");
    let value = String::from("Moved Value");

    let mut map = HashMap::new();
    map.insert(key, value); // key and value are now invalid identifiers

    // --- Accessing Elements --- //
    let blue_score = scores.get("Blue");  // Wrapped in Some -> Some(&7)

    let team_name = String::from("Pink");
    let pink_score = scores.get(&team_name);  // Some(&78)

    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }

    // --- Updating --- //
    // Overwriting
    scores.insert(String::from("Blue"), 12);
    // Update if key does not exist
    scores.entry(String::from("Purple")).or_insert(42);
    // Update an existing value
    let mut map = HashMap::new();
    let text = "apples pies are full of pies";
    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);  // count is a mutable reference
        *count += 1
    }
    println!("{:?}", map); // HashMaps do NOT retain order

}

• String

Strings are encoded as UTF-8 characters in Rust, thus they are a bit weird. They cannot be indexed into since UTF-8 is variable lengthen. Docs.

fn main() {
    // Initialize
    let mut s = String::new();
    let const_str = "str, not String";
    s = const_str.to_string();
    let mut s = String::from("hello");
    let s2 = String::from(" This is Daltie!");

    // Append
    s.push_str(" world");
    s.push('!');
    let s3 = s + &s2; // s has been moved and can no longer be used
    let s1 = String::from("Hello World!");
    let s3 = format!("{} {}", s1, s2);

    // Iterating Over
    for c in s3.chars() {
        println!("{}" ,c);
    }
}

• Vector

Just like a C++ vector. Docs.

fn main() {
    // With type annotation
    let _v: Vec = Vec::new();
    // With type deduction (vec! macro)
    let mut v = vec![1, 2, 3];
    for i in 5..8 {
        v.push(i);
        
    }

    // --- Read element --- //
    // & [index], causes a panic if accessing memory outside of vector
    let _third: &i32 = &v[2];
    let fourth = &v[3];
    let _fourth_copy = v[3]; // Copy of the fourth element
    println!("Forth: {}", fourth);
    // .get() returns Option<&T>
    match v.get(4) {
        Some(fifth) => println!("The fifth element is: {}", fifth),
        None => println!("There is no fifth element!"),
    }

    // -- Iterating -- //
    for i in &v {
        println!("{}", i);
    }
    // Mutable
    for i in &mut v {
        *i += 1; // Dereference operator required to update

    }

    // --- Multi-Type Vector Hack --- //
    enum Colors {
        Rgb(i32),
        Grayscale(f32),
    }
    let _pixels = vec![
        Colors::Rgb(255),
        Colors::Grayscale(3.5),
    ];

    // --- Useful Methods --- //
    // Length
    let _len = v.len();
    // Clear
    v.clear();
    // Insert
    v.insert(0, 4); // Index, value
    // Remove
    let _value = v.remove(0); // Index
    // Push/Pop back
    v.push(5);
    let _value = v.pop();
}

Concurrency

One of the goals of Rust is to make concurrency as safe as can be.

Example:

use std::sync::mpsc; // Multiple producer, single consumer (mpsc)
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

fn main() {
    // Transmitter and Receiver
    let (tx, rx) = mpsc::channel();

    // Mutex must be stored an atomic shared pointer to share between threads
    let counter = Arc::new(Mutex::new(0));

    // Threads list
    let mut handles = vec![];

    for i in 0..=10 {
        let tx2 = tx.clone();
        let counter = Arc::clone(&counter);

        let handle = thread::spawn(move || {
            // Sleep for 1 millisecond
            thread::sleep(Duration::from_millis(100));

            // Will release the lock at the end of the scope
            let mut num = counter.lock().unwrap();
            *num += 1;

            // Transmit string to reciever
            tx2.send(format!("Thread number: {}, Mut number: {}", i, num)).unwrap();
        });
        handles.push(handle);
    }

    // Received will end when all tx go out of scope
    // tx acts like a shared pointer, keeping track of references
    drop(tx);
    for received in rx {
        println!("{}", received);
    }

    // Wait for threads to stop
    for handle in handles {
        handle.join().unwrap();
    }

    println!("DONE!");
}

• Channel

Channels allow threads to communicate with one another. A channel consists of a transmitter and a receiver. Given a channel, there can be any number of transmitters but only one receiver.

• Move

move allows you to move local variable's ownership to a closure.

use std::thread;

fn main() {
    let v = vec![0, 1, 2, 3];

    let handle = thread::spawn(move || {
        println!("{:?}", v);
    });

    // Cannot use v in main thread anymore

    handle.join().unwrap();
}

• Mutex

Mutexes allows thread safe access to memory. They should be pared with Arc<T> (atomic Rc<T>) to be shared amount threads.

• Traits

Any type must implment the std::marker traits Sync and Send to be passed into a thread. Any type that does not use non-thread safe pointers automatically implements these traits.

Crates

• Binary

A package can have any number of binary crates

src/main.rs is the default crate root for a binary crate with the same name as the package. To add multiple binary crates, place files in the src/bin/ directory. Each file will be a separate binary crate.

• Library

A package can only have one (or none) library crates.

If a package directory contains src/lib.rs, the package contains a library crate with the same name as the package with src/lib.rs being the crate root. If both src/lib.rs and src/main.rs exist, then the package has two crates

• Module

Modules are used to organize code within a crate into groups and control the privacy of items (public vs private). The C++ counterpart would be a namespace, except Rust takes it a step further.

Modules are created by using the mod keyword.

The modules in src/main.rs and src/lib.rs are called "crate roots" and is nested under crate.

Modules are private by default (like C++ private methods in a class). To make them public, you have to add the pub keyword. All parents of that module are now public, but children are still private unless specified otherwise.pub can be used on structs, enums, functions, and methods as well.

The keyword super allows you to refer to something in the parent's scope.

Example:

mod garden {
    // Nest a module inside of another
    pub mod food {
        // Add structs, enums, constants, traits, modules, functions, etc. here
        pub fn harvest() {
            super::soil::amount();
            super::super::plant_garden();
        }
        fn water() {}
    }

    pub mod soil {
        pub fn quality() {}
    
        pub fn amount() { quality(); }
    }
}

pub fn plant_garden() {
    // Relative path
    garden::soil::quality();
    // Absolute path
    crate::garden::soil::amount();
}

fn main() {
    // To access harvest, you'll have to do:
    crate::garden::food::harvest();
    // Both food and harvest must be marked as pub
}

• Multiple Files

Multiple files example:

// src/garden.rs
pub mod food;

// src/garden/food.rs
pub fn harvest() {}

// src/main.rs
mod garden;

pub use crate::garden::food;

fn main() {
    food::harvest();
}

• Package

A package is one or more crates that provide a some functionality. A package contains a Cargo.toml file which describes how to build the crates.

A package can contain as many binary crates as desired but can only contain a maximum of one library crate. A package must contain at least one crate (either library or binary).

• Paths

To use a module tree, you'll need the path of the module. The path can be:

  • Absolute path: Starts at the crate root
  • Relative path: Starts from the current module. Uses self, super, or an identifier in the current module.

Which path method used in a module depends on how the module will be used. Absolute paths are generally recommended.

:: separate each identifier in a path (like C++ namespaces).

• Public

The keyword pub makes something public, like a function, module, enum, struct etc.

However, structs are special. pub in front of them just makes them public, not the members inside. The members will not be read nor writable. To make them read/writable, add the pub keyword to the variable itself.

Example:

mod house {
    pub struct Kitchen {
        pub plates: i32,
        sinks: i8,
    }

    impl Kitchen {
        pub fn duel_sink(num_plates: i32) -> Kitchen {
            Kitchen {
                plates: num_plates,
                sinks: 2,
            }
        }
    }
}

pub fn build_home() {
    let mut home = house::Kitchen::duel_sink(8);
    home.plates += 1;

    // Cannot read nor write to private struct field
    //home.sinks += 1;
    //println!("Num sinks: {}", home.sinks);
}

• Use

The keyword use can be used to import code and to shorten absolute/relative paths. It is recommended to import the module and not the specific function so when used, it is obvious that the call is not to a local function. When importing anything other than a function (structs, enums, etc), use the full path.

Example:

mod garden {
    // Nest a module inside of another
    pub mod food {
        // Add structs, enums, constants, traits, modules, functions, etc. here
        pub fn harvest() {}
        fn water() {}
    }

    pub mod soil {
        pub fn quality() {}
    
        pub fn amount() { quality(); }
    }
}

// Absolute path
use crate::garden::food;
// Relative path
use self::garden::soil;

// Full path for structs
use std::collections::HashMap;


fn main() {
    food::harvest();
    soil::quality();
	let mut map = HashMap::new();
}
As

When there is a name collision with use, the keyword as can be used in conjunction. Example:

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

fn func1() -> Result { // ...  }
fn func2() -> IoResult<()> { // ... }
Pub Use

pub use can be used to re-export your code. For example, if you import A into B then import B into C, C would not have access to A because A would be private. If A was imported to B using pub use, then C would have access to A.

Re-exporting is especially useful when creating an API. Inside of src/lib.rs, you can do pub use self::path::to::thing to make others be able to use thing via use my_crate::thing in their code. This allows your API to be more strait-forward than your internal structure.

Nested Paths

Example:

use std::{cmp::Ordering, io}; // Bring in both std::cmp::Ordering and std::io
use std::io::{self, Write}; // std::io and std::io::Write
Glob

Brings in all public items from a path into scope. Example:

use std::collections::*;

• Workspaces

A workspace is a set of packages that work together. To make a workspace, create a directory with a Cargo.toml file with a [workspace] section. You can then add packages to this workspace via cargo new and adding the name of the crate to the workspace section.

Debug

• Println

The println! macro offers two different ways to print debugging information: {:?} and {:#?}. The {:#?} option is a bit prettier.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle {
        width: 25,
        height: 40,
    };

    // {:?}
    println!("rect is {:?}", rect);
    // Output:
    // rect is Rectangle { width: 25, height: 40  }

    // {:#?}
    println!("rect is {:#?}", rect);
    // Output:
    // rect is Rectangle {
    //     width: 25,
    //     height: 40,      
    // }
}

Dyn

The dyn keyword allows Rust to vaguely have the concept of inheritance in the sense that a pointer can dynamically point to more than one type.

dyn allows a smart pointer like a Box point to anything that implements a trait.

Rules:

  • Trait function cannot return self because a concrete self type would not be known at compile time and thus not object safe.

• Object Oriented Programming

Rust is not a true OOP language as it does not have inheritance. Rust does allow for a polymorphic like interface via dyn though. Example:

// Trait for dyn
trait Animal {
    // The use of self instead of &self allows the function to take
    // ownership, and thus invalidating the old state
    fn grow_up(self: Box<Self>) -> Box<dyn Animal>;
    fn age(&self);
}

// Container. Similar to parent class
pub struct DogHouse {
    pub residence: Option<Box<dyn Animal>>
}

impl DogHouse {
    pub fn new() -> DogHouse {
        DogHouse { residence: None }
    }

    pub fn get_puppy(&mut self, s: &str) {
        self.residence = Some(Box::new(Puppy::new(s)));
    }

    pub fn grow_up(&mut self) {
        if let Some(s) = self.residence.take() {
            self.residence = Some(s.grow_up())
        }
    }

    pub fn residence_age(&self) {
        if let Some(s) = &self.residence {
            s.age()
        }
    }
}

pub struct Puppy {}

pub struct Dog {}

impl Puppy {
    pub fn new(name: &str) -> Puppy {
        Puppy{}
    }
}

impl Animal for Puppy {
    fn grow_up(self: Box<Self>) -> Box<dyn Animal> {
        Box::new(Dog {})
    }

    fn age(&self) {
        println!("Puppy!");
    }
}

impl Animal for Dog {
    fn grow_up(self: Box<Self>) -> Box<dyn Animal> {
        self
    }

    fn age(&self) {
        println!("Doggo!");
    }
}

fn main() {
    let mut red_dog_house = DogHouse::new();
    red_dog_house.get_puppy("Spike");
    red_dog_house.residence_age(); // Puppy
    red_dog_house.grow_up();
    red_dog_house.residence_age(); // Doggo
}

Enumerations

Enums are great for categorical data, where something, like an IP address, can belong to one category, such as IPv4 or IPv6. Enums are heavily used in Rust, unlike in C++. Example:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

// Function that uses an enum as a parameter
fn coin_value(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

// Enums can contain data
enum Message {
    Move { x: i32, y: i32 }, // Anonymous struct
    Write(String),
    ChangeColor(i32, i32, i32), // three i32 values
    Quit,
}

// Method of Message
impl Message {
    fn call(&self) {
        // Body
    }
}

fn main() {
    // Use the namespace syntax to choose a category
    let c = Coin::Dime;
    println!("Coin value: {}", coin_value(c));

    let m = Message::Write(String::from("hello"));
    m.call();
}

• Option

Null references have caused lots of problems in other languages. To solve this, Rust created the Option enum. In the Option enum, there are two possibilities: Some (not null) and None (null). The Option enum will not interact with none Option data, thus you have to be aware of when using types that might be null, unlike C++ pointers 😁.

This enum is so popular in Rust that it is automatically loaded, Some and None can be used without namespacing them with Option.

The Option enum works well with the match expression, verifying that you cover both the null and not null cases.

Basic definition:

enum Option<T> {
    Some(T),
    None,
}

Example use:

// With namespaceing
let some_number = Option::Some(2);
// Without namespacing
let another_number = Some(3);
let some_string = Some("Daltie");

// "Null"
let absent_number: Option<i32> = None; // Need to specify the type that Null would be otherwise

// Options and none options do not interact with each other natively
let x: i8 = 3;
let y: Option<i8> = Some(5);

//let sum = x + y; // Breaks

Function example:

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


fn main() {
    let three = Some(3);
    let four = add_one(three);
    let none = add_one(None);
}

Error Handling

There are two types of errors in rust, recoverable errors and unrecoverable errors.

• Propagating Errors

To proprogate and error, return the error.

The ? operator

The ? operator can be used to immediately return an error from a function! The ? operator is only valid with functions that return Result or Option (or any type that implements Try). Example:

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

fn read_from_file(f_name: &str) -> Result<String, io::Error> {
    let f = File::open(f_name);

    let mut opened_file = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match opened_file.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

fn shortcut_read_from_file(f_name: &str) -> Result<String, io::Error> {
    let mut f = File::open(f_name)?;  // Return Err if error occurred
    let mut s = String::new();
    f.read_to_string(&mut s)?; // Return Err if error occurred
    Ok(s)
}

fn super_short_read_from_file(f_name: &str) -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}

use std::fs;
fn the_shortest_read_from_file(f_name: &str) -> Result<String, io::Error> {
    fs::read_to_string(f_name)
}

fn main() {
    let f_name = String::from("file.txt");
    let s1 = read_from_file(&f_name).unwrap();
    println!("{}", s1);
}

• Recoverable

Rust uses a Result<T, E> enum to catch errors. Functions that have the possibility of erroring returns the Result type. Example:

use std::fs::File;
use std::io::ErrorKind;

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

    let f = match f {
        Ok(file) => file,  // Return the file
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("file.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Cannot create the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error)
            }
        },
    };
}
Unwrap

unwrap can be used as a short cut if you want to panic! on error. If an Ok is returned, then unwrap will return the value inside of the OK.

let f = File::open("file.txt").unwrap();
Unwrap-Or-Else

Like unwrap but executes closure if error occured.

let f = File::open("file.txt").unwrap_or_else(|err| {
    println!("Error: {}", err);
    // Do something else
});
Expect

Similar to unwrap, expect can be used to give your own error message in the case of an error.

let f = File::open("file.txt").expect("Failed to open file.txt");
Is Error

Returns true if an error occurred:

let debug: bool = env::var("DEBUG").is_err();

• Unrecoverable

panic! is how to raise an unrecoverable error.

By default, when a panic occurs, the program starts unwinding by walking up the stack and cleaning up the data for each function. Alternatively, you can abort, which ends the program without cleaning up. Aborting leads to a smaller binary.

In order to make panics abort instead of unwind in production code (to create smaller binaries), you must add the following to your Cargo.toml file:

[profile.release]
panic = 'abort'
Backtrace

Rust provides backtracing. To use do:

$ RUST_BACKTRACE=1 cargo run

Expressions

Rust is an expression-based language. Unlike statements:

  • Expressions do not end in a semi-colon
  • Expressions evaluate to a resulting value (returns something)

For example:

fn main() {
    let x = 1;

    // The {} block is an expression. The entire line is a statement.
    let y = {
        let x = 7; // Different x
        x + 1 // Notice how there is no semi-colon here.
    };

    println!("y = {}", y);
}

• For

For loops are used to iterate over collections. Don't worry, a normal C++ for loop is still applicable. You just need to make it more like Python and iterate through some numbers!

fn main() {
    let arr = [1, 2, 3, 4, 5];

    // Iterate through a collection
    for element in arr.iter() {
        println!("The value is: {}", element);
    }

    // Iterate through a range of values
    for number in 1..6 {
        println!("The value is: {}", number);
    }

    // Iterate through a range, backwards
    for number in (1..6).rev() {
        println!("The value is: {}", number);
    }

    // Inclusive end
    for _ in 1..=9 {}

    // Use vars
    let a = 0;
    let b = 10;
    for i in a..b {
        println!("{}", i)  # println! requires a string literal, not just a variable
    }
}

• If Expression

Rust is a standard if, else if, else language, however, only boolean values are allowed for the conditional. Integers will cause a mismatched types error.

fn main() {
    let number = 12;

    // Standard if, else if, else expression
    if number % 3 == 0 {
        println!("Number is divisible by 3");
    } else if number % 2 == 0 {
        println!("Number is divisible by 2");
    } else {
        println!("Math hard, I gave up.");
    }

    // Using "if" in a let statement (because it's an expression, not a statement)
    let number = if number > 0 { number } else { 0 }; // Return types must match
    println!("number is now: {}", number);
}

If there are many else if expressions, Rust recommends using match instead.

• If Let

The if let is the little sibling to match. if let allows you to match only one arm of a match expression. The if let expression takes a pattern on the left hand side of an EQUAL SIGN and an expression on the right hand side of an EQUAL SIGN.

if let can also be combined with normal if-else statements.

Example:

#[derive(Debug)]
enum Breed {
    Husky,
    Poodle,
    Lab,
    // Etc.
}

enum Animal {
    Cat,
    Giraffe,
    Dog(Breed), // This Animal::Dog type contains Breed data
}

fn main() {
    // Example A
    let some_i32 = Some(17i32); // Make 17 an i32 type
    if let Some(3) = some_i32 { // Notice the "=" sign
        println!("Three!");
    } else {
        println!("The number was not three :(");
    }

    // Example B
    let dog = Animal::Dog(Breed::Husky);
    if let Animal::Dog(breed) = dog {
        println!("We found a {:?} dog!", breed);
    }
}

• Loop

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

There also exists the break statement to break out of a loop and the continue statement to immediately go to the next iteration of the loop.

Values can be returned from loops. For example:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 5 {
            break counter * 7;
        }
    };

    println!("result = {}", result);
}
Loop

The loop loop creates an infinite loop.

fn main() {
    loop {
        println!("Never ending!");
    }
}

• Match

match is like a switch-case in most other languages. It consists of the match keyword followed by an expression (generally a single variable). In the curly brackets comes the arms. Each arm consists of a pattern and some code.

Matches are exhaustive. Every case for an enum must be handled. match does provide a catch all via the _ placeholder.

A | can be used to match multiple patterns.

Each arm of a match expression must return the same type (the "never type" is an exception).

Example:

#[derive(Debug)]
enum Breed {
    Husky,
    Poodle,
    Lab,
    // Etc.
}

enum Animal {
    Cat,
    Giraffe,
    Dog(Breed), // This Animal::Dog type contains Breed data
    Fox,
    Hamster,
    Snake,
    Daltie,
}

fn sound(animal: Animal) -> String {
    match animal {
        Animal::Cat => String::from("Meow"),
        Animal::Giraffe => String::from("Hello good Sir"),
        Animal::Dog(breed) => { // Function like arm block
            println!("I am a {:?}", breed);
            String::from("Woof")
        },
        Animal::Snake | Animal::Daltie => String::from("I'm a sssnake!"),
        _ => String::from("??"), // Catches the Fox and Hamster cases
    }
}

fn main() {
    let husky = Animal::Dog(Breed::Husky);
    println!("{}", sound(husky));

    // Match range
    let x = 2;
    
    match x {
        1..=7 => println!("Small num"),
        _ => println!("Invalid"),
    }
}
Destructing

Structs, enums, tuples, and references can be destructed to their individual parts.

struct Point{ x: u32, y: u32 }

fn main() {
    let p = Point { x: 0, y: 7 };
    let Point{ x: a, y } = p; // a and y are now valid variables in this scope

    // Match destructured values
    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!("Somewhere, over a rainbow? ({}, {})", x, y),
    }
}
Ignoring Parts
struct Point{ x: u32, y: u32 , z: u32}

fn main() {
    let p = Point { x: 0, y: 7, z: 14 };

    // Ignore everything other than y
    match p {
        Point { y, .. } => println!("Only care about y: {}", y), // 7
    }

    let numbers = (1, 2, 3, 4, 5);

    // Only match first and last values
    match numbers {
        (first, .., last) => println!("{}, {}", first, last), // 1, 5
    }
}
Match Guards
fn main() {
    let num = Some(7);

    match num {
        Some(x) if x < 5 => println!("Less than 5"),
        // Match expressions much be exhaustive, so not including
        // this line would cause an complication error
        Some(x) => println!("{}", x),
        None => (),
    }
}
@

@ allows you to both test a pattern and bind a variable.

enum Height {
    Meters { m: u32 },
}

fn main() {
    let tall = Height::Meters{ m: 7 };

    match tall {
        Height::Meters {
            m: meters_var @ 0..=9,
        } => println!("You're only {} meters tall! You short!", meters_var),
        Height::Meters { m: 10..=99 } => {
            println!("You fit") // variable m is not available, need @ if want variable binding
        }
        Height::Meters { m } => println!("{} is so tall", m), // Since there is no testing, m is available
    }
}

• While

Conditional looping!

fn main() {
    let mut number = 0;

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

        number += 1;
    }
}

• While Let

Similar to if let, while let allows you to loop until the "if let" is false.

let mut stack = vec![1, 2, 3];

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

Functions

A function in Rust starts with fn. For example:

fn main() {
    // Body
}

main() is the most important function and is where the Rust program starts.

Rust does not care where a function is defined, it just needs to be defined. For example:

fn main() {
    other_function();
}

fn other_function() {
    println!("This function is declared after main. It could have been declared before main.");
}

• Function Pointers

You can pass functions as parameters to functions as well! The fn type is a function pointer.

Function pointers implement all three closure traits Fn, FnMut, and FnOnce).

fn double(x: u32) -> u32 {
    x * 2
}

fn do_twice(f: fn(u32) -> u32, arg: u32) -> u32 {
    f(arg) * f(arg)
}

fn main() {
    assert_eq!(
        16, 
        do_twice(double, 2)
    )
}

• Paramaters

Type annotations for parameters are required in Rust.

fn main() {
    other_function(12, 'a');
}

fn other_function(x: i32, c: char) {
    println!("The value of x is: {}", x);
    println!("The value of c is: {}", c);
}

• Return Values

Rust requires you to declare a return type for a function if it returns something.

The last line in a function is returned implicitly as long as it is an expression (no semi-colon). A return statement can be used to return prior to the last line.

fn three() -> i32 {
    3 // Notice the lack of a semi-colon. Expressions return something, statements do not.
}

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

fn minus_one(x: i32) -> i32 {
    // Because we are explicitly using "return" we can either make this
    // a statement or an expression.
    return x - 1; 
}

fn no_return(x: i32) {
    println!("{}, x");
}

fn main() {
    let x = three();
    println!("x is: {}", x);

    let x = plus_one(x);
    println!("x is now: {}", x);

    let x = minus_one(x);
    println!("x is finally: {}", x);
}

Generics

Generics are Rust's version of templates. Examples:

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

impl<T, U> Point<T, U> {
    fn swap<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 43, y: 3.14 };
    let p2 = Point { x: "Hello", y: "Daltie" };
    let p3 = p1.swap(p2);
}

Iterators

Iterators allow you to perform some task on a sequence of items.

Rust iterators are lazy, they do not do anything until a method consumes them.

• Collect

Converts an iterator into a collection. The type of collection must be known. Example:

let add_one: Vec<_> = vec![1,2,3].iter().map(|x| x + 1).collect();
let add_one_more = add_one.iter().map(|x| x + 1).collect::<Vec<i32>>(); // turbofish type specification

• Filter

Uses a closure to filter results from an iterator. Only results that return true are added to the resulting iterator.

let even: Vec<u32> = vec![1,2,3,4].into_iter().filter(|x| x % 2 == 0).collect();

• Iter

There are three common types of iter functions:

  • iter: Iterator over immutable references
  • into_iter: Takes ownership of collection and returns owned values
  • iter_mut: Iterator over mutable references
fn main() {
    let v = vec![1, 2, 3, 4];
    // iter
    let sum: i32 = v.iter().sum(); // 10
    // into_iter
    let even: Vec<i32> = v.into_iter().filter(|x| x % 2 == 0).collect(); // [2, 4]
    // iter_mut
    let mut v = &mut vec![1, 2, 3, 4];
    for x in v.iter_mut() {
        *x += 1;
    } // [2, 3, 4, 5]
}

• Map

Applies a closure to an iterator and returns an iterator. Example:

let add_one: Vec<_> = vec![1,2,3].iter().map(|x| x + 1).collect();

• Sum

Adds together all elements in the collection:

let total: i32 = vec![1, 2, 3].iter().sum(); // 6

• Traits

All iterators implement a trait named Iterator, which is defined in the standard library. The definition follows:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>; // Go to next item
}

• Zip

Iterates over two iterators simultaneously.

let x = vec![1, 2, 3, 4];
    let y = vec![9, 8, 7, 6];

    let z: Vec<i32> = x.iter().zip(y).map(|(a, b)| a + b).collect();

    assert_eq!(z, vec![10, 10, 10, 10]);

Lifetimes

A lifetime is how long an object exists, i.e. the scope for which an object's reference is valid. Generally lifetimes are inferred in Rust, but sometimes annotations must be applied so the compile knows how long an object will be around for.

The syntax for explicit lifetimes are: 'a, where the 'a' can be any char (well variable string, but who needs more than 26 lifetimes?).

The lifetime annotations live in the same area as templates, and share the same < >.

• Functions

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s;
    let x = String::from("Apple");
    {
        let y = String::from("Pie");
        s = longest(&x, &y);
        println!("{}", s);
    }
    // print!("{}", s);  // y does not live long enough, therefor s cannot be used here
}

• Rules

Lifetimes have three shortcut rules to make it easier for developers:

  1. Each parameter that is a reference gets its own lifetime
    • fn foo<'a, 'b>(x: &'a str, y: &'b str)
  2. If there is exactly one input lifetime parameter, that lifetime is applied to all output lifetime parameters
    • fn foo<'a>(x: &'a str) -> &'a str
  3. If an input lifetime parameter is &self or &mut self, the lifetime of self is automatically to all output lifetime parameters

• Static Lifetimes

Static lifetimes live for the entire duration of the program. They have the 'static lifetime annotation.

fn main() {
    let s;
    {
        let x: &'static str = "LONG LIVE THE STATIC!";
        s = x;
    }
    println!("{}", s); // Static variable is still alive despite leaving scope
}

• Structs

struct StringWrapper<'a> {
    s: &'a str,
}

impl<'a> StringWrapper<'a> {
    fn print(&self) {
        println!("{}", self.s);
    }

    fn get_s(&self) -> &str { // &str does not need a lifetime annotation because of the 3rd shortcut rule
        self.s
    }
}

fn main() {
    let wrapper;
    {
        let sentence = String::from("Apple Pie");
        let first_word = sentence.split(' ').next().expect("Need more than 1 word"); 
        wrapper = StringWrapper {
            s: first_word,
        };
        wrapper.print();
    };
    // wrapper.print();  // first_word, who's lifetime is on sentence, does not live long enough
}

Macros

Macros are a form of metaprogramming in Rust. Macros are able to take in a variable number of parameters (like println!) and generate Rust code to satisfy the listed parameters.

• Eprintln

eprintln! does the same thing as println!, but prints to stderr instead.

Operator Overloading

Some operators may be overloaded in rust by implementing the corresponding trait in std::ops

• Add

use std::ops::Add;

#[derive(Debug, PartialEq, Copy, Clone)]
struct Point{ x: i32, y: i32 }

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point{ x: 3, y: -2 } + Point{ x: 2, y: -3 },
        Point{ x: 5, y: -5 }
    );
}

Ownership

Ownership is a central part of Rust. Ownership is similar to a unique_ptr in C++, but for all variables, including referenced variables.

  • Each value 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

• Clone

To create a deep copy of a variable, using the clone method is required.

let s1 = String::from("Daltie");
let s2 = s1.clone();

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

• Copy

The Copy trait copies a variable onto the stack. Scalar types and tuples implement the Copy trait by default.

Copy VS Drop

A type may either have a Copy trait or a Drop trait. If a type or any part of a type has implemented the Drop trait then it cannot have the Copy trait. This is to make it very obvious if the type is to be placed on the stack or the heap.

• Functions

Similar to assignment, variables are either moved or copied into a function. Same for return values. For example:

fn main() {
    let s1 = String::from("Daltie");
    takes_ownership(s1); // s1 is no longer valid

    let x = 3;
    makes_copy(x); // x is still valid

    let s2 = gives_ownership(); // The return value from gives_ownership() is moved

    let s3 = takes_and_gives_ownership(s2); // s2 is no longer valid
} 
// s3 is dropped. 
// s1 and s2 were already moved, so nothing happens. 
// x goes out of scope.

fn takes_ownership(some_str: String) {
    println!("{}", some_str);
}

fn makes_copy(some_int: i32) {
    println!("{}", some_int);
}

fn gives_ownership() -> String {
    let some_str = String::from("Daltie");
    some_str
}

fn takes_and_gives_ownership(some_str: String) -> String {
    some_str
}

• Move

In Rust, you need to know what variables will be put on the stack and which variables will be added to the heap. Heap and stack variables are treated differently.

Stack
Stack variables consist of basic types with a known size at compile time. Stack variables are valid until they go out of scope. For example:
let x = 3;
let y = x;

Both x and y are valid and contain the value 3 until they go out of scope.

Heap

Heap variables follow similar but more strict rules to shallow copies in Python in the sense that the new variable will point to the data, however, in Rust, the old variable becomes invalid. For example:

let s1 = String::from("hello");
let s2 = s1;

Line 1 initializes a String variable by creating a string of size 5(ish) on the heap and a pointer to said string on the stack. Line 2 then takes ownership of this heap string. s1 now no longer points to the string and s1 is no longer valid and cannot be used until it is re-assigned. The data is NEVER copied, instead it behaves similarly to C++'s move semantics.

See the Rust Book for fantastic diagrams of this process, specifically Figure 4-4.

• References

Functions

Rust lets a function borrow a variable using references.

fn main() {
    let s1 = String::from("Daltie");
    let len = find_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn find_length(s: &String) -> usize {
    s.len()
}
// s is never actually given ownership, it is simply borrowing the String,
// thus nothing happens when s goes out of scope
Mutable References

Rust allows for exactly one mutable reference per scope (as long as the variable is active).

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

    change(&mut s);

    let r1 = &mut s;
    r1.push_str(" is ");
    println!("{}", r1); // "Daltie Cole is"
    
    let r2 = &mut s; // Okay because r1 is never used after r2 creation
    r2.push_str(" cool!");
    println!("{}", r2); // "Daltie Cole is cool!"

    // Either unlimited immutable references are allowed at once OR one mutable reference is allowed per scope
    let r3 = &s;
    let r4 = &s;
    //let r5 = &mut s; // BREAKS

    println!("{}, {}", r3, r4);
}

fn change(some_str: &mut String) {
    some_str.push_str(" Cole");
}

Rustc

To compile rust code, you use rustc

For example, to compile the file main.rs you would do $ rustc main.rs. This would then create the executable main

Smart Pointers

Smart pointers in Rust are like smart pointers in C++. Unlike references which only borrow data, smart pointers can own the data they point to.

• Box

A Box<T> stores data on the heap rather than the stack. A pointer to that heap data remains on the stack. They are useful when:

  • Don't know the size of the type at compile time but will need to know the exact size during run time
  • Want to transfer ownership of a large amount of data without copying.
  • When you own a value and only care about its implemented traits, not its type (polymorphism)

Immutable or mutable borrow checking is done at compile time.

Example:

pub enum LinkedList {
    Con(i32, Box<LinkedList>),
    Nil,
}


use crate::LinkedList::{Con, Nil};

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

    assert_eq!(1, match list {
        Con(value, next) => value,
        Nil => -1,
    });
}

• Deref Coercion

Deref coercion allows a referenced type to another type, for example, &String can be converted to &str because String implements the Deref trait and returns &str.

The DerefMut trait can be used to override the * operator on mutable references.

In addition, mutable references can be coerced to become immutable if the Deref trait is implemented.

Example:

use std::ops::Deref;
use std::ops::DerefMut;

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) -> &Self::Target {
        &self.0
    }
}

impl<T> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

fn deref_coercion(name: &str) {
    println!("{}", name);
}

fn deref_mut_coercion(num: &mut f32) {
    *num += 3.0
}


fn main() {
    let x = MyBox::new(2);
    assert_eq!(2, *x);

    let n = MyBox::new(String::from("Rust"));
    deref_coercion(&n);
    let mut m = MyBox::new(12.2);
    deref_mut_coercion(&mut m);
}

• Refcell

RefCell<T> is like Box<T> except the borrow checker is checked at runtime instead of compile time. This allows the programmer to make code that they know will work, but the borrow checker is unsure about.

RefCell<T> is only used in single-threaded scenarios.

Both immutable and mutable borrow checking is done at runtime. The value inside of RefCell<T> can be mutated even if RefCell<T> is immutable.

This is useful when you need to mutate something that is passed in as immutable.

• Reference Counted

A reference counted smart pointer is the same as a shared pointer in C++. The Rc<T> pointer keeps track of the number of references to it and frees up memory when the reference count is zero.

Rc<T> can only be used for single-threaded scenarios.

Only immutable borrow checking is done at compile time.

Rc<T> can only hold immutable data. Pair with RefCell<T> for mutability.

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let v = Rc::new(RefCell::new(vec![1, 2, 3]));
    let w = Rc::clone(&v);
    let x = Rc::clone(&v);

    // Add 4 to vector
    v.borrow_mut().push(4);

    //would_cause_panic(&v);

    println!("{:?}", x);
}

fn would_cause_panic(v: &Rc<RefCell<Vec<u32>>>) {
    let mut one_borrow = v.borrow_mut();
    // Panic because two mutable references
    // During rumtime with RefCell
    let mut two_borrow = v.borrow_mut();
}
Strong vs Weak Count

When strong_count reaches 0, the instance is cleaned up. Clean up does not care how many weak references there are. Using Weak<T> is one way to help prevent memory leaks (via circular referencing).

Since what Weak<T> references may have been dropped, it is necessary to call upgrade which returns an Option<RC<T>> to verify that the reference still exists.

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 6,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:#?}", leaf.parent.borrow().upgrade()); // None

    let branch = Rc::new(Node {
        value: 42,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    // Downgrade strong to weak
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:#?}", leaf.parent.borrow().upgrade()); // Some
}

• Traits

Smart pointers implement the Deref and Drop traits.

Standard Library

• Command Line Arguments

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect(); // Turn argument iterator into a vector
    println!("{:?}", args); // Prints the arguments along with the binary name

    // Convert first argument to i32
    let num: i32 = args[1].parse().unwrap();
}

• Environmental Variables

use std::env;

fn debug() {
    let DEBUG = env::var("DEBUG").is_err();
}

• Read File

use std::fs;

fn read_file(file_name: &str) -> String {
    fs::read_to_string(file_name)
        .expect("Failed to open file {}")
}

fn main() {
    let file_name = "file.txt";
    read_file(file_name);
}

Structs

Structs are essentially tuples with named fields. For example:

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

fn main() {
    // Normal Declaration
    let user1 = User {
        email: String::from("contact@daltoncole.com"),
        username: String::from("drc"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = build_user_annoying_way(String::from("bob@bob.bob"), String::from("bob"));
    let user3 = build_user_easy_way(String::from("john@bob.com"), String::from("john"));

    // Struct Update (quick way to change only some fields from another instance)
    let user4 = User {
        email: String::from("me@daltoncole.com"),
        username: String::from("crd"),
        ..user1 // Copies the rest of the data from user 1
    };

    println!(
        "The users are: {}, {}, {}, and {}", 
        user1.username, user2.username, user3.username, user4.username
    )
}

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

// Since the variable names share the same name as the field,
//   we can just use the variables instead
fn build_user_easy_way(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

• Methods

Struct methods are implemented in impl blocks. Multiple methods can be in a single impl block and you can have multiple impl blocks per struct.

The general syntax of methods are very similar to python methods, where instance methods require &self (just using self is allowed but rare). To call "class" level methods, the namespace operator :: is required.

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

impl Rectangle {
    // Self's type is automatically the struct's type
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn fits_inside(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

impl Rectangle {
    // Example where you wouldn't use self
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 25,
        height: 40,
    };

    let rect2 = Rectangle {
        width: 100,
        height: 45,
    };

    println!("Area of rect1: {}", rect1.area());
    println!("Fits inside: {}", rect1.fits_inside(&rect2));

    // Create a square rectangle. Use the namespace syntax here
    let square = Rectangle::square(7);
    println!("Area of square: {}", square.area());
}

• Tuple Structs

You can also create structs that behave like tuples, i.e. structs without named fields. This can be useful when you want to pass tuples that contain specific information into a function.

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

fn main() {
    let white = Color(255, 255, 255);
    let origin = Point(0, 0, 0);

    print_color(white);
    //print_color(origin); // Breaks
}

fn print_color(color: Color) {
    // Use "." followed by the index to index into a Tuple Struct
    println!("({}, {}, {})", color.0, color.1, color.2);
}

Tests

Rust comes with a unit and integration test suit baked into cargo. To make a function a test function, add #[test] on the line before the function.

Tests in rust have access to the private parts of everything.

The following is an example of a unit test:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn new(w: u32, h: u32) -> Rectangle {
        Rectangle {width: w, height: h}
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;  // Brings outer module into scope
    
    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle::new(5, 6);
        let smaller = Rectangle::new(4, 5);

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larget() {
        let smaller = Rectangle::new(4, 5);
        let larger = Rectangle::new(5, 6);

        assert!(!smaller.can_hold(&larger));
    }

    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("2 + 2 != 4"))
        }
    }
}

• Asserts

  • assert!(x): Passes if x is true
  • assert_eq!(x, y): Passes if x and y are equal
  • assert_ne!(x, y): Passes if x and y are not equal

• Custom Failure Messages

Custom failure messages are supported by the assert family micros.

pub fn concat(fname: &str, lname: &str) -> String {
    format!("{} {}", fname, lname)
}

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

    #[test]
    fn combine_names() {
        let full_name = concat("Daltie", "Colti");
        assert_eq!(
            full_name,
            "Daltie Coltie",
            "Daltie Coltie != {}",
            full_name
        ) // The same failure message logic can be applied to any assert! micro
    }
}

• Integration Tests

Integration tests are external to your library. They can only call your library's public API.

Integration tests are only for library crates. For binary crates, only unit tests can be used.

Integration tests go inside of a tests directory next to your src directory. Example:

// src/lib.rs in crate addr
pub fn add_two(x: i32) -> i32 {
    x + 2
}

// tests/integration_tests.rs
use addr;

#[test]
fn it_adds_two() {
    assert_eq!(4, addr::add_two(2));
}
Submodules

Each file in the tests directory is compiled as its own separate crate. To prevent this, put code you don't want separately tested into subdirectories inside of tests. These subdirectories will not be tested, but can act as support code for your tests.

• Should Panic

If a test should panic, add #[should_panic] before the function declaration.

#[test]
#[should_panic]
fn this_should_panic() {
    panic_function(1000);
}

• Unit Tests

Unit tests are used to test how your code interacts with itself, testing one module in isolation at a time.

Location

Unit tests live along side your code in the src directory in the same file as the code that it is testing. To create a unit test, create a module called tests with the #[cfg(test)] annotation. "cfg" stands for configuration.

Privacy

Unit tests can test the private parts of your code.

Traits

Traits in Rust are similar to interfaces in other languages. When a type implements a trait, that type will behave as the trait describes.

pub trait Shape {
    fn area(&self) -> f32;  // Traits declarations have ';' as opposed to {} blocks
}

pub struct Circle {
    radius: f32,
}

impl Shape for Circle {
    fn area(&self) -> f32 {
        self.radius * 3.14159
    }
}

pub struct Rectable {
    width: f32,
    height: f32,
}

impl Shape for Rectable {
    fn area(&self) -> f32 {
        self.width * self.height
    }
}

fn main() {
    let cir = Circle { radius: 12.25 };
    println!("{}", cir.area());
}

• Associated Types

Associated types act as a type placeholder for traits in method signatures. When the trait is implemented, the associated type is replaced with the concrete type.

struct Point {x: i32, y: i32}

trait Contains {
    // Declare placeholder types
    type A;
    type B;

    fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
}

impl Contains for Point {
    // Specify types during implementation
    type A = i32;
    type B = i32;

    fn contains(&self, num1: &Self::A, num2: &Self::B) -> bool {
        (&self.x == num1) && (&self.y == num2)
    }
    // This would also be a valid signature:
    //fn contains(&self, num1: &i32, num2: &i32) -> bool {}
}

fn main() {
    let num1 = 2;
    let num2 = 3;

    let points = vec![Point{x: 1, y: 2}, Point{x: 2, y: 3}];

    for point in points {
        match point.contains(&num1, &num2) {
            true => println!("Does contain point!"),
            false => println!("Does NOT contain point!"),
        }
    }
}

• Conditional Method Implementation

You can use traits to conditionally implement methods to a generic struct. Example:

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}  // Notice Self vs self
    }
}

impl<T: Display> Pair<T> {
    fn print(&self) {
        println!("{}, {}", self.x, self.y);
    }
}

fn main() {
    let p = Pair::new(5, 4);
    p.print();
}
Self

In the above example, you might notice the capital 'S' in Self. Capital Self can be used to refer to the type that is being implemented as opposed to self which refers to the calling object.

• Default Generic Type

A default generic type can be given for traits.

There are two main purposes for this:

  • Extend a type without breaking existing code
  • Customize specific cases
use std::ops::Add;

struct Feet(f32);
struct Inches(f32);

/* // Add trait definition
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
*/

// Use default
impl Add for Feet {
    type Output = Feet;

    fn add(self, other: Feet) -> Feet {
        Feet(self.0 + other.0)
    }
}

impl Add<Inches> for Feet {
    type Output = Feet;

    fn add(self, other: Inches) -> Feet {
        Feet(self.0 + (other.0 / 12.0))
    }
}

• Default Implementation

To have a default implementation for a trait, use a {} block.

Default implementations can also call other functions.

Example:

pub trait Shape {
    fn area(&self) -> f32 {
        0.0
    }
}

• Drop

The drop trait is similar to the deconstructor in C++. When a variable that has resources on the heap, it needs to implement the drop trait so when the variable leaves its scope, the resources are deallocated. If the drop trait in implemented, the copy trait cannot be.

• Supertraits

Supertraits allow a trait to require the implementation of another trait. Example:

use std::ops::Add;

#[derive(Debug, PartialEq, Copy, Clone)]
struct Point{ x: i32, y: i32 }

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point { x: self.x + other.x, y: self.y + other.y }
    }
}

// Supertrait - Has ':' with trait requirements
trait Print: std::fmt::Debug {
    fn print(&self) {
        println!("*** {:?} *** ", self);
    }
}

impl Print for Point {}

fn main() {
    let p1 = Point{x: 1, y: 2};
    p1.print();
}

• Traits As Parameters

Traits can also be parameters. These functions are similar to generics, but restrict the types to types that implement the trait. Example:

pub trait Shape {
    fn area(&self) -> f32;
}

pub trait TwoD {}

pub struct Circle {
    radius: f32,
}

impl Shape for Circle {
    fn area(&self) -> f32 {
        self.radius * 3.14159
    }
}

impl TwoD for Circle {}

// Traits as parameters - short hand
pub fn print_area(s: &impl Shape) {
    println!("{}", s.area());
}

// Traits as parameters - the long way
pub fn print_area2<T: Shape>(s: &T) {
    println!("{}", s.area());
}

// Multiple Traits
pub fn print_area3<T: Shape + TwoD>(s: &T) {
    println!("{}", s.area());
}

// where can be used to make trait bounds look pretty
pub fn print_areas<T, U>(s: &T, r: &U) 
    where T: Shape + TwoD,
          U: Shape
{
    println!("{}", s.area());
    println!("{}", r.area());
}

// Return type that implements trait
pub fn get_area(s: &str) -> impl Shape {
    if s == "Circle" {
        return Circle { radius: 0.0 };
    }
    Circle { radius: 0.0 }  // Imagine other Shapes being here
}

fn main() {
    let cir = Circle { radius: 12.25 };
    print_area3(&cir);
    print_areas(&cir, &cir);
}

Types

• Compound Types

Compound types combine multiple values into a single type. Rust has two primitive compound types: tuples and arrays.

The Tuple Type

Tuples have a fixed length and can contain different types.

fn main() {
    let tup = (10, 1_234, 5678, 12.4, true, 'a');
    let tup : (i32, i32, i32, f64, bool, char) = tup;

    // Destruct tup through pattern matching
    let (a, b, c, d, e, f) = tup;
    println!("{}, {}, {}, {}, {}, {}", a, b, c, d, e, f);

    // Destruct tup through indexing
    let ten = tup.0;
    let c = tup.5;
    println!("{}, {}", ten, c);

    // Return multiple values using a tuple
    let s1 = String::from("Daltie");
    let (s2, len) = find_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

// Multiple values can be returned from a function using tuples
fn find_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}
The Array Type

Arrays are of FIXED length and all elements must be of the same type. See vector in the standard library for a variable length container.

Array data is allocated on the stack rather than the heap (speedy!).

fn main() {
    // Array Declaration
    let a = [-1, 2, 3, 4, 5];
    let a: [i32; 5] = [1, 2, 3, 4, 5]; // i32 is the type, 5 is the length
    let a = [3; 5]; // [3, 3, 3, 3, 3]

    // Indexing
    let first = a[0];
    let second = a[1];

    // Out of Bounds Runtime error
    let will_panic = a[12]; // Rust will panic and crash instead of trying to access invalid memory
}

• Never Type

If a function will never return use the never return type !.

fn foo() -> ! {}

Some things that return the never type are continue and panic!.

• Scalar Types

Rust has four primary scalar types:

  • Integers
  • Floating-Point Numbers
  • Booleans
  • Characters
Integer Types
Integer Types
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 "arch" length is system architecture dependent.

Integer Literals
Number Literals Example
Decimal12_345_678
Hex0xfe
Octal0o76
Binary0b1111_0000
Byte (u8 only)b'A'
Floating-Point Types
Floating-Point Types
Length Type
32-bitf32
64-bitf64

Note: Rust defaults all floats to f64.

The Boolean Type

A boolean in Rust is either true or false and takes up one byte. The type is specified with bool.

The Character Type

The character type in Rust is a four byte Unicode Scalar Value. For example:

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let laughing_crying_face = '😂';
}

• Slice

A slice is a contiguous sequence of elements in a collection. The advantages of slices is they allow you to borrow part of a collection, such as a String or array. This is safe borrowing, if part of the original data is altered, the borrowed data is no longer valid.

String Slice
fn main() {
    let mut name = String::from("Daltie Cole");

    let f_name = first_name(&name[..]); // Let first_name() borrow name. Pass as a string literal

    //name.clear(); // Clear out the name

    // If name was cleared, this line would not compile
    println!("First name: {}", f_name);

    // Other slicing fun
    let daltie = &name[..7];
    let cole = &name[8..];
    let full_name = &name[..];
}

// By using &str as the parameter type instead of &String, string literals can be passed as well
fn first_name(name: &str) -> &str {
    // Convert name into an array of bytes
    let bytes = name.as_bytes();

    // .iter() creates an iterator over the array
    // .enumerate() wraps iter() into a series of tuples where
    //   the first element is the index and the second is the item
    for (i, &item) in bytes.iter().enumerate() {
        // If the element is a binary space
        if item == b' ' {
            // Return the slice from the beginning to the current point
            return &name[0..i];
        }
    }

    &name[..]
}

• Strings

Contains

Returns true if a string contains a substring:

if s.contains("apple") {}
Lines

Iterate though each line in a string:

for line in s.lines() {}
Lowercase

Creates a new string containing the lowercase letters of the old string:

s.to_lowercase();
Binary Strings

Use b"" to create a binary string.

• Type

The type keyword is similar to typedef in C++, a type becomes aliased as another type.

// Simple Example
type Meters = i32;
let x: i32 = 2;
let y: Meters = 5;
let z = x + y;

// Can be used with templates
type Result>T< = std::result::Result>T, std::io::Error<

Unsafe

The unsafe keyword allows you to create a block of code where some of Rust's safety guarantees are no longer applied. This is sometimes necessary when Rust is being to conservative.

Use cases:

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

It is the programmer's responsibility to make sure the code in a unsafe block is safe.

• Functions

unsafe fn unsafe_func() {} // Call unsafe inside of here, user beware! Read the docs!

fn abstraction() {} // Unsafe is inside this function, but we are smarter than 
// the compiler (maybe) so we know that the contents of the function are fine

// All functions called from a different programming language are unsafe
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        unsafe_func();
    }

    abstraction();

    unsafe {
        println!("Abs(-12) = {}", abs(-12));
    }
}

• Raw Pointers

Unsafe rust has two raw pointer types:

  • *const T
  • *mut T

Raw pointers:

  • Ignore borrowing rules
  • Do not guarantee the pointed to memory is valid
  • Can be null
  • No automatic clean-up
fn main() {
    let mut num = 4;

    // Raw pointers can be created in safe mode
    let rp1 = &num as *const i32; // *const i32 is the type
    let rp2 = &mut num as *mut i32; // *mut i32 is the type

    // Deallocating raw pointers can only be done in unsafe mode
    unsafe {
        //*rp1 = 10; // Does not work because *const
        println!("*rp1 = {}", *rp1);
        *rp2 = 12;
        println!("*rp2 = {}", *rp2);
    }
}

• Static Variables

Rust static variables are called global variables in other languages.

static PI: f32 = 3.14159;
static mut COUNTER: u32 = 0;

fn main() {
    // Accessing non-mutable static variables is safe
    println!("Pi = {}", PI);

    unsafe {
        // Accessing mutable static variables is unsafe (think threads)
        COUNTER += 1;
    }
}

• Traits

Traits are unsafe when at least one of its methods have some invariant that the compiler cannot verify. For example, using structs consisting of raw pointers for multi-threaded purposes. We'd have to mark the Send and Sync traits for our struct as unsafe since Rust cannot verify the struct's safety.

unsafe trait Apple {}
unsafe impl Apple for i32 {}