1 - Rust Course

Course link
Rust site

Install Rust
Create .rs file from VSCode
fn main() {println!("Text {}", value)}
println!()
{} is the placeholder for value in a string

$ rustc filename
$ . /filename
Cargo steps
cargo new filename - creates project

$ cd to project folder
$ cargo run

Discuss Primitive data types (AKA scalar data types)
int - signed (i8, i16...) & unsigned (u8, u16...)
float - f32, f64
bool
char

Discuss Compound data types
array - same type, fixed count
tuple - mixed type, fixed count
slice - same type, dynamic count
string - mutable, growable, owned
string slice (&str) - immutable, reference

argument
.push_str("")

mutability - Variables are immutable by default and can be made mutable with "mut"

Memory Types
heap - Dynamic, mutable, slower
stack - Immutable, faster.

Rust cleans memory allocated to any value.

Rust allows hoisting. Functions can be declared and called from anywhere in the code.

Example function syntax:

fn name_of_func(argument) {
	// ...
}

expressions return a value and statements do not.

an expression that evaluates to a certain value or mathematical operation will evaluate to the last line in the expression and therefore the last expression does not require a ;

e.g.

let _x = {
	let price = 5;
	let qty = 10;
	price * qty
};`

functions returning values

fn add(a: i32, b: i32) -> i32{a+b}
(parameter a and b added) -> (data type that you want returned)

const or static should be used for global values instead of let.

borrowing

A reference can be mutable (&mut) or immutable (&).
Referencing borrows the value from the owner.

Rules:
You can only have 1 mutable reference OR any number if immutable references.
You may not have both mutable and immutable references in the same scope. Use structs instead (Example - Mutable & Immutable Reference).

impl is the keyword for adding methods to structs. These are similar to how classes work in object-oriented programming. They always have self&self, or &mut self

Structs

from The Rust Programming Language

struct name {
	value: type
}

Structs are similar to tuples, discussed in “The Tuple Type” section, in that both hold multiple related values. Like tuples, the pieces of a struct can be different types. Unlike with tuples, in a struct you’ll name each piece of data so it’s clear what the values mean. Adding these names means that structs are more flexible than tuples: You don’t have to rely on the order of the data to specify or access the values of an instance.

To define a struct, we enter the keyword struct and name the entire struct. A struct’s name should describe the significance of the pieces of data being grouped together. Then, inside curly brackets, we define the names and types of the pieces of data, which we call fields. For example, Listing 5-1 shows a struct that stores information about a user account.

Example struct

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

You can call a struct from a function.

Example struct from function

// struct declared previously
   fn build_user(email: String, username: String) -> User{
        User{
            active: true,
            email,
            username,
            sign_in_count: 1,
        }
        
       let user3 = build_user("email@email.com".to_string(), "username".to_string());
    println!("The user's email is {} and the sign-in count is {}", user3.email, user3.sign_in_count);

You can create instances from other instances. Use the syntax ..struct1 to fill in values from that instance

Example instances from instance

    let user2 = User{
        email: String::from("another@m.com");
        ..user1
    }

Tuple structs are like other structs but don't have named values

Example tuple struct

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

let black = Color(0, 0, 0);
let white = Color(255, 255, 255);

Unit-like structs have no fields and used when you need a type to implement a trait but don't need to store data.

Example unit-like struct

``
    struct AlwaysEqual;   let subject = AlwaysEqual;  

e.g.
.push_str("") -  Appends a string slice (&str) to the end of a String.
.len() - Calculate lentgh of a collection, returning the number of elements or bytes it contains as a usize value.

from The Rust Programming Language

Methods are similar to functions: We declare them with the fn keyword and a name, they can have parameters and a return value, and they contain some code that’s run when the method is called from somewhere else. Unlike functions, methods are defined within the context of a struct (or an enum or a trait object, which we cover in Chapter 6 and Chapter 18, respectively), and their first parameter is always self, which represents the instance of the struct the method is being called on.
...
The main reason for using methods instead of functions, in addition to providing method syntax and not having to repeat the type of self in every method’s signature, is for organization. We’ve put all the things we can do with an instance of a type in one impl block rather than making future users of our code search for capabilities of Rectangle in various places in the library we provide.

impl

Implementations of functionality for a type, or a type implementing some functionality.

There are two uses of the keyword impl:

  • An impl block is an item that is used to implement some functionality for a type.
  • An impl Trait in a type-position can be used to designate a type that implements a trait called Trait.

Implementing Functionality for a Type

The impl keyword is primarily used to define implementations on types. Inherent implementations are standalone, while trait implementations are used to implement traits for types, or other traits.

An implementation consists of definitions of functions and consts. A function defined in an impl block can be standalone, meaning it would be called like Vec::new(). If the function takes self&self, or &mut self as its first argument, it can also be called using method-call syntax, a familiar feature to any object-oriented programmer, like vec.len().

Example

struct Example {
    number: i32,
}

impl Example {
    fn boo() {
        println!("boo! Example::boo() was called!");
    }

    fn answer(&mut self) {
        self.number += 42;
    }

    fn get_number(&self) -> i32 {
        self.number
    }
}
:  

Can be declared in global scope (outside main function).
Not allowed to be made mutable.
Should add type annotation.
Use UPPER_SNAKE_CASE

Declaring the same variable again using the variable.

This is different from declaring it as mutable.

You are able to change the type of the value with shadowing.

Example

fn main() {
    let x = 5;
    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x in the main function is: {x}");
}

/* 
Compiles to: 
The value of x in the inner scope is: 12
The value of x in the main function is: 6
*/

Control flow

If ... Else

Branch code based on conditions.

If else for functions

fn main() {
    let age: u16 = 15;
    if age >= 16 {
        println!("You can drive a car!");
    } else {
        println!("You can't drive a car!");
    }
}
    // multiple conditions with else if
    // % calculates the remainder of an operation
    let number = 6;
    if number % 4 == 0 {
        println!("number is divisible by 4!");
    } else if number % 3 == 0 {
        println!("number is divisible by 3!");
    } else if number % 2 == 0 {
        println!("number is divisible by 2!");
    } else {
        println!("number is not divisible by 4, 3, or 2!");
    }

If else for declarations

If and else will need to evaluate to the same type.

    let condition = true;
    let number = if condition {5} else {6};
    println!("Number: {number}");
    
    // Number: 5
    
    /* let number = if condition {5} else {"six"};
    Will not compile. */

Loops

loop {
code
if ... {break ...}
}

Example

    let mut counter = 0;

    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!{"{result}"};
    //Result prints as 20 because that is what follows break (counter * 2)

Nested loop example

       let mut count = 0;
        'counting_up: loop {
            println!("count = {count}");
            let mut remaining = 10;
            loop {
                println!("remaining = {remaining}");
                if remaining == 9 {
                    break;
                }
                if count == 2 {
                    break 'counting_up;
                }
                remaining -= 1;
            }
            count += 1;
        }
        println!("End count = {count}");

While loop example

        let mut number = 3;
        while number !0 = {
            println!("{number}");
            number -= 1;
        }
        println!("HEY!!!");

Example array

    let a = [1,2,3,4,5,6];
    for element in a {
    println!("{element}");
    }

A versatile tool used to represent a type that can take on one of several possible variants. Allows variants to hold different kinds and amounts of data.

Syntax:

enum name {
	type1,
	type2
}

let x = name::type1

These could be assign by structs, but can also be assigned type directly in the enum. Enums can contain any type, strings, numeric types, structs, even other enums.

Example Enum

enum IpAddrKind {
        V4,
        V6
}

let _four = IpAddrKind::V4;
let _six = IpAddrKind::V6;

fn route(_ip_kind: IpAddrKind) {}

routeV4;
routeV6;

Example assigning type via enum

Each variant can have different types and amounts of associated data.

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let home = IpAddr::V61");

IpAddr::V4() is a function call that takes a String argument and returns an instance of the IpAddr type. We automatically get this constructor function defined as a result of defining the enum.

Example with multiple types

enum Message { 
	Quit, 
	Move { x: i32, y: i32 }, 
	Write(String), 
	ChangeColor(i32, i32, i32), 
}

Option
enum Option<T>

Example error handling with Option

fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

fn main() {
  
let result = divide(10.0, 2.2);
match result{
    Some(x) => println!{"Result: {}", x},
    None => println!{"Cannot divide by Zero!"},
}
}

Example error handling with Result

fn divideResult(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by 0".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

match divideResult(100.23, 0.0){
    Ok(result) => println!{"Result: {}", result},
    Err(err) => println!{"Error: {}", err},
}

Rust lacks null. Instead, the Option and Result enums can be used to check for empty values.

Collections

Vec<T>

let mut _v:Vec<i32> = vec![1,2,3];
(or, less common:) let _v:Vec<i32> = Vec::new()

Dynamic array that will grow or shrink as needed.

Vectors allow you to store more than one value in a single data structure that puts all the values next to each other in memory. Vectors can only store values of the same type. They are useful when you have a list of items, such as the lines of text in a file or the prices of items in a shopping cart.

Example vector:

    let mut _v:Vec<i32> = vec![1,2,3];
    _v.push(5);
    _v.push(6);
    _v.push(7);
    _v.push(8);
    _v.push(9);

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

Referencing elements inside the vector

Direct indexing:
let third: &i32 = &_v[2];

Get method:

    let fourth = _v.get(3);
    match fourth {
        Some(fourth) => print!("The fourth element is {fourth}."),
        None => println!("There is no fourth element.")
    }

The borrow checker will not allow you to have an immutable reference to an element in a vector if the vector will be changed later, because adding to the vector may cause the entire thing to be moved to a different space in the memory if there is not enough room to add to it.

String collection

Use .push and .push_str to append to a string.

Example:

    // opt 1
    let _s = "whatever".to_string();
    // opt 2
    let _s = String::from("whatever");
    // opt 3
    let mut _s = String::from("foo");
    _s.push_str("bar");
    _s.push('!');
    println!("{}", _s);
    // Output: foobar!
    
    

Use the + operator to concatenate string values.

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

With format!

    let hi1 = String::from("Shalom");
    let hi2 = String::from("Hola");
    let full_message = format!("{hi1} - {hi2}");
    println!("{full_message}");
    // Output: Shalom - Hola

From the Rust book:

"If we need to concatenate multiple strings, the behavior of the + operator gets unwieldy:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;

At this point, s will be tic-tac-toe. With all of the + and " characters, it’s difficult to see what’s going on. For combining strings in more complicated ways, we can instead use the format! macro:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");

This code also sets s to tic-tac-toe. The format! macro works like println!, but instead of printing the output to the screen, it returns a String with the contents. The version of the code using format! is much easier to read, and the code generated by the format! macro uses references so that this call doesn’t take ownership of any of its parameters."

let name = HashMap::new();

"The type HashMap<K, V> stores a mapping of keys of type K to values of type V using a hashing function, which determines how it places these keys and values into memory. Many programming languages support this kind of data structure, but they often use a different name, such as hashmapobjecthash tabledictionary, or associative array, just to name a few."

Examples:

    let mut scores = HashMap::new();

    scores.insertfrom("Blue"), 10;
    scores.insertfrom("Yellow"), 50;


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

    let team_name = String::from("Blue");
    let score = scores.get(&team_name).copied().unwrap_or(0);

    println!("Team {}'s score is {}!", team_name, score);