Types

Struct with Generics

struct Wrapper<T> {
    value: T,
}


impl<T> Wrapper<T> {
    pub fn new(value: T) -> Self {
        Wrapper { value }
    }
}

Structs

Classic Structs

struct ColorClassicStruct {
    red: i32,
    green: i32,
    blue: i32,
}


let green = ColorClassicStruct {
    red: 0,
    green: 255,
    blue: 0,
};

assert_eq!(green.red, 0);
assert_eq!(green.green, 255);
assert_eq!(green.blue, 0);

Tuples

struct ColorTupleStruct(i32, i32, i32);


let green = ColorTupleStruct(0, 255, 0);

assert_eq!(green.0, 0);
assert_eq!(green.1, 255);
assert_eq!(green.2, 0);

Unit-Like Struct

Unit-like Struct: No fields, no data stored, behave similarly to ()

struct UnitLikeStruct;


let unit_like_struct = UnitLikeStruct;
let message = format!("{:?}s are fun!", unit_like_struct);

Methods

Optional Arguments

DO NOT DO THIS: fn greet(name = 1) , this is WRONG

Here's an example of using optional arguments in Rust:

fn main() {
    
    // Call greet function with no arguments
    greet(None);  
    // Call greet function with an optional argument
    greet(Some("Alice".to_string()));
}

fn greet(name: Option<String>) {
    // Match the optional argument
    match name {
        // If there is a name, print a personalized greeting
        Some(name) => println!("Hello, {}!", name),
        // If there is no name, print a generic greeting
        None => println!("Hello, world!"),
    }
}

Patterns of methods arguments

  • fn get_char(data: String) takes the String by value, allowing you to modify the string data, but it will make a full copy of the string data, which can be expensive for large strings.
  • fn get_char(data: &String) takes a reference to a String, allowing you to access the string data, but not modify it.
  • fn get_char(mut data: &String) Wrong syntax - you cannot take a mutable reference to an immutable reference.
  • fn get_char(mut data: String) takes a mutable String, allowing you to modify the string data, but it will also make a full copy of the string data.
SyntaxOwnershipMutable
fn get_char(data: &String)CallerNo
fn get_char(mut data: &String)N/AN/A
fn get_char(data: String)Transferred to FunctionNo
fn get_char(mut data: String)Transferred to FunctionYes

Enum

enum Message {
    Quit,
    Move(Point),
    Echo(String),
    ChangeColor((u8, u8, u8)),
}

fn triggerSpecificEvent(message: Message) {
    // TODO: create a match expression to process the different message variants
    match message {
        Message::ChangeColor((a, b, c)) => change_color((a, b, c)),
        Message::Echo(s) => echo(s),
        Message::Move(p) => move_position(p),
        Message::Quit => quit(),
    }
}

Strings

  • String is a string type that is stored as a vector (Vec<u8>) of bytes, but it guarantees that it is a valid UTF-8 sequence. String is heap-allocated, growable, and not null-terminated.
  • &str is a string slice type that always points to a slice (&[u8]) of a valid UTF-8 sequence, and can be used to view the contents of a String, much like how &[T] is to Vec<T>. &str does not own the data it references, so it is typically used as a function parameter type.
  • The two differ primarily in ownership and heap allocation. When you need to modify strings, it is recommended to use the String type because it is mutable and owns the data, allowing you to modify its contents at will. When you only need to view or reference a string, it is recommended to use the &str type instead, because it is immutable and does not own the heap memory, reducing memory overhead and copying operations.
  • String processing functions in Rust usually take and return &str types instead of String types. This is because &str is more flexible and easier to use. If you need to convert a String to an &str type, you can use the & symbol or the as_str() method. If you need to convert an &str type to a String type, you can use the to_string() or to_owned() methods.

fn string_slice(arg: &str) { 
    println!("{}", arg);
}
fn string(arg: String) { 
    println!("{}", arg);
}

fn main() {
    let string1 = "blue"; // &str
    let string2 = String::from("hi"); // String
    let string3 = "rust is fun!".to_owned(); // String
    let string4 = "nice weather".into(); // String
    let string5 = format!("Interpolation Station"); // String
    let string6 = &String::from("abc")[0..1]; // &str
    let string7 = "   hello there   ".trim(); // &str
    let string8 = "Happy Monday!".replace("Monday", "Tuesday"); // String
    let string9 = "my SHIFT KEY IS STUCK".to_lowercase(); // String

    string_slice(string1);
    string(string2);
    string(string3);
    string(string4);
    string(string5);
    string_slice(string6);
    string_slice(string7);
    string(string8);
    string(string9);
}

String & &Str

  • String is a heap-allocated data type owned by the current thread. It can be mutated and resized.
  • &str is a reference to a string literal or a String instance, and it can't be mutated or resized. It's a view into the String's memory without ownership of the allocation.
fn main() {
    // Create a new String instance with the value "green"
    let word = String::from("green"); 
    // Call the is_a_color_word function with a reference to the "word" String instance as an argument.
    if is_a_color_word(&word) {
        // If is_a_color_word returns true, print "That is a color word I know!"
        println!("That is a color word I know!");
    } else {
        // If is_a_color_word returns false, print "That is not a color word I know."
        println!("That is not a color word I know.");
    }
}

// This function takes a reference to a string slice as an argument.
fn is_a_color_word(attempt: &str) -> bool {
    // Check if "attempt" is equal to "green", "blue", or "red"
    attempt == "green" || attempt == "blue" || attempt == "red"
}

Convert &str to String

To convert a &str to a String, you can use the to_string() or to_owned() methods. For example:

let s1: &str = "hello";
let s2: String = s1.to_string();
let s3: String = s1.to_owned();

Both to_string() and to_owned() will create a new String instance that contains a copy of the original &str.

Match

let num = 5;

match num {
    1 => println!("The number is one"),
    2 => println!("The number is two"),
    3 => println!("The number is three"),
    4 => println!("The number is four"),
    5 => println!("The number is five"),
    6..=8 => println!("The number is between 6 and 8 (inclusive)"),
    9..11 => println!("The number is between 9 and 11 (exclusive)"),
    _ => println!("The number is not in the range 1 to 10"),
}

Options

Some common use cases for Option<T> include:

  • Returning a value that may not exist: In this case, the function would return an Option<T> rather than a T. For example, if you were searching for a specific item in a list, you might return Option<T> to indicate that the item was either found or not found.
  • Dealing with errors: You might use Option<T> to represent an error condition, such as when a file can't be opened.
  • Simplifying code: Option<T> can be useful for simplifying code that would otherwise require multiple if statements to check for a value's existence
let maybe_number: Option<i32> = Some(42);

match maybe_number {
    Some(n) => println!("The number is {}", n),
    None => println!("There is no number."),
}

Error Handling

// main function
fn main() {
    let result = divide(10, 2); // call divide function with arguments 10 and 2, and store the result in a variable
    match result { // match the result against two possible outcomes: Ok and Err
        Ok(value) => println!("Result is {}", value), // if the result is Ok, print the value of the result
        Err(error) => println!("Error occurred: {}", error), // if the result is Err, print the error message
    }
}

// divide function
fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 { // check for division by zero
        return Err(String::from("Cannot divide by zero")); // if b is 0, return an Err with the message "Cannot divide by zero"
    }
    Ok(a / b) // if b is not 0, return an Ok with the result of dividing a by b
}

map_err

// 
enum ParsePosNonzeroError {
    Creation(CreationError),
    ParseInt(ParseIntError),
}

impl ParsePosNonzeroError {
    fn from_creation(err: CreationError) -> ParsePosNonzeroError {
        ParsePosNonzeroError::Creation(err)
    }
    fn from_parseint(err: ParseIntError) -> ParsePosNonzeroError {
        ParsePosNonzeroError::ParseInt(err)
    }
}

fn parse_pos_nonzero(s: &str) -> Result<PositiveNonzeroInteger, ParsePosNonzeroError> {
    // let x: i64 = s.parse().unwrap();
    // if we use unwrap, we will get a panic if the parse fails

    // now we will use map_err to convert the error type
    let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?;
    PositiveNonzeroInteger::new(x).map_err(ParsePosNonzeroError::from_creation)
}

Traits

Impl

TL;DR: impl is used to define a method for a struct.

struct Circle {
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

Implementing a trait on a type

In this code, we do the following:

  1. Define a trait called AppendBar without real implementation.
  2. Implement the AppendBar trait for the String type and add the implementation for the append_bar function.
  3. Use the append_bar function on a String instance.
// Define a new trait called AppendBar
trait AppendBar {
    // Define a function called append_bar that takes ownership of the object and returns the same type
    fn append_bar(self) -> Self;
}

// Implement the AppendBar trait for the String type
impl AppendBar for String {
    // Implement the append_bar function for the String type
    fn append_bar(self) -> Self {
        let mut s = self;
        s.push_str("Bar");
        s
    }
}

// Usage for the AppendBar trait 
fn main() {
    let s = String::from("Foo");
    let s = s.append_bar();
    println!("s: {}", s);
}

Trait with default implementation

Trait implementations can have default implementations for some or all of the methods. For example:

trait Foo {
    fn foo(&self);
    fn bar(&self) {
        println!("This is a default implementation.");
    }
}

Trait as a parameter

Here we have 1 trait Implemented for 2 structs.

We can use impl {trait_name} as a parameter type, so we can pass any type that implements the trait.

pub trait Licensed {
    fn licensing_info(&self) -> String {
        "some information".to_string()
    }
}

struct SomeSoftware {}

struct OtherSoftware {}

impl Licensed for SomeSoftware {}
impl Licensed for OtherSoftware {}

fn compare_license_types(
    // for below two parameters, we can pass any type that implements the Licensed trait
    software: impl Licensed, 
    software_two: impl Licensed
) -> bool {
    software.licensing_info() == software_two.licensing_info()
}

For a more complex example, we can use impl {trait_name} + {trait_name} to specify multiple traits.

pub trait SomeTrait {
    fn some_function(&self) -> bool {
        true
    }
}

pub trait OtherTrait {
    fn other_function(&self) -> bool {
        true
    }
}

struct SomeStruct {}
struct OtherStruct {}

impl SomeTrait for SomeStruct {}
impl OtherTrait for SomeStruct {}
impl SomeTrait for OtherStruct {}
impl OtherTrait for OtherStruct {}

// Here we can pass any type that implements both SomeTrait and OtherTrait
fn some_func(item: impl SomeTrait + OtherTrait) -> bool {
    item.some_function() && item.other_function()
}

Lifetime

Why do we need lifetime?

The main aim of lifetimes is to prevent dangling references, which cause a program to reference data other than the data it’s intended to reference. Consider the following example, which will not compile:

// The lifetime `'a` is defined here between the function name and the parameter list.
// This means that the function will take two string slices with the same lifetime `'a`.
// The return value will also have the same lifetime `'a`.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    // The if statement checks which string slice is longer and returns a reference to it.
    // Since both `x` and `y` have the same lifetime `'a`, the returned reference will also have the same lifetime.
    if x.len() > y.len() { x } else { y }
}

Threads & Concurrency

There are 2 ways to handle concurrency in Rust:

  1. Threads
  2. MPSC (Multiple Producer, Single Consumer)

Share/Mutate Data between Threads

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

struct JobStatus {
    jobs_completed: u32,
}
fn main() {
    // Create a new JobStatus instance wrapped in a Mutex and Arc to allow for shared ownership
    // Mutex is required if we want to mutate the value
    let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 }));
    
    let mut handles = vec![];

    // Spawn 10 threads, each with a clone of the shared JobStatus instance
    for _ in 0..10 {
        let status_shared = Arc::clone(&status);
        // Spawn a new thread and move the clone of the shared JobStatus instance into the closure
        let handle = thread::spawn(move || {
            thread::sleep(Duration::from_millis(250));

            // Lock the Mutex and update the jobs_completed field
            let mut s_shared = status_shared.lock().unwrap();
            s_shared.jobs_completed += 1;
        });
        handles.push(handle);
    }
    for handle in handles {
        // Trigger the thread to start executing
        handle.join().unwrap();

        // Print the value of the jobs_completed field
        println!("jobs completed {}", status.lock().unwrap().jobs_completed);
    }
}

Another Example: Share data between threads


#![forbid(unused_imports)] 
use std::sync::Arc;
use std::thread;

fn main() {
    let numbers: Vec<_> = (0..100u32).collect();
    // Arc is important here because we are using threads.
    // Arc makes it possible to share ownership across threads and make it thread-safe.
    let shared_numbers = Arc::new(numbers);
    let mut joinhandles = Vec::new();

    for offset in 0..8 {
        // We are using shared_numbers across threads, so we need to clone it. 
        let child_numbers = shared_numbers
            .iter()
            .cloned()
            .filter(|&n| n % 8 == offset)
            .collect::<Vec<_>>();
        println!("Child numbers: {:?}", child_numbers);
        
        joinhandles.push(
            thread::spawn(move || {
                let sum: u32 = child_numbers
                    .iter()
                    .filter(|&&n| n % 8 == offset)
                    .sum();
                println!("Sum of offset {} is {}", offset, sum);
            })
        );
    }
    for handle in joinhandles.into_iter() {
        handle.join().unwrap();
    }
}

Enabling Recursive Types with Boxes

The key point is that we need to use Box to the recursive(self referencing) to work;


#[derive(PartialEq, Debug)]
pub enum List {

    // This is a enum, apparently, it references List itself, we need to use Box to make it work
    // Cons is a enum, it has two fields, a number and next pointer
    Cons(i32, Box<List>), 

    // This is a enum, represents a empty list`
    Nil, 
}

fn main() {
    println!("This is an empty cons list: {:?}", create_empty_list());
    println!("This is a non-empty cons list: {:?}", create_non_empty_list());
}

pub fn create_empty_list() -> List {
    // Simply return an empty list, List::Nil is a enum
    let list = List::Nil;
    list
}

pub fn create_non_empty_list() -> List {
    // Simply return a non-empty list, List::Cons is a enum
    let list = List::Cons(1, Box::new(List::Nil));
    list
}

Troubleshooting

cargo run runs main.rs, i want to run bar.rs

  1. Add below code to Cargo.toml:

    [[bin]]
    name = "src"
    path = "main.rs"
    
    [[bin]]
    name = "bar"
    path = "bar.rs"
    
    
  2. You can run cargo run --bin bar to run the bar binary.

Borrowed value does not live long enough

This happens when a scope of a variable is too short.

// this function expects the lifetime of arguments(`x` and `y`) to be the same as the return value(`&'a str`
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// THIS CODE WILL NOT COMPILE
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        // string2 does not have same lifetime as string1 
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is '{}'", result);
}

Solutions:

  1. Make the lifetime of string2 the same as string1:
  2. Delete println!() to make the scope of result the same as string2: