Giter Site home page Giter Site logo

rustcheatsheet's Introduction

Cheat sheet Rust

Table of Content


First

Let's get started with a simple "Hello, World!" program in Rust.

First, make sure that you have Rust installed on your machine. If not, you can download it from the official website (https://www.rust-lang.org/tools/install) and install it following the given instructions.

Once Rust is installed, you can create a new Rust file using any text editor you prefer. Save it with the .rs extension, for example, main.rs. Then, write the following code:

// This is your first Rust program!

fn main() {
    // Print "Hello, World!" to the console
    println!("Hello, World!");
}

Here's a breakdown of what's happening in the code:

  • fn main() {}: The main function. This is a special function where your program starts running.
  • println!(): A macro (not a function) that prints text to the console. You can recognize macros by the ! at the end.
  • "Hello, World!": A string, which we are passing to println!() to print.

To run the program, open a terminal, navigate to the directory where you saved your file, and use the rustc compiler to compile the source code into an executable:

rustc main.rs

This will create a new executable in the same directory. You can run it with:

./main

The output will be:

Hello, World!

Congratulations! You've written and run your first Rust program.

Second

Let's build on what we learned in the first tutorial. We'll explore variables and basic types in Rust.

Create a new Rust file called variables.rs and write the following code:

fn main() {
    // Declare a variable
    let greeting = "Hello, Rust!";
    println!("{}", greeting);

    // Declare a mutable variable
    let mut counter = 5;
    println!("Counter: {}", counter);

    // Modify the mutable variable
    counter = 10;
    println!("Counter: {}", counter);

    // Constants
    const MAX_VAL: u32 = 100_000;
    println!("Max Value: {}", MAX_VAL);

    // Shadowing
    let x = 5;
    let x = x * 2;
    let x = x + 10;
    println!("x: {}", x);
}

Here's a breakdown of what's happening:

  • let greeting = "Hello, Rust!"; Here, we're declaring an immutable variable called greeting that holds a string.
  • let mut counter = 5; mut keyword makes counter a mutable variable, meaning its value can be changed.
  • counter = 10; We're changing the value of counter here.
  • const MAX_VAL: u32 = 100_000; This declares a constant MAX_VAL. Constants are always immutable and you need to declare the type of the value.
  • In the shadowing section, we redeclare x twice. Each time, a new variable x is created and gets the value of the old x modified. The final value of x is 20.

Compile and run this program as you did in the previous tutorial. You'll see the values of the variables printed to the console.

Third

Let's now move on to control flow with conditionals and loops in Rust. This tutorial will focus on if/else statements, match statements, and a few types of loops.

Create a new Rust file called control_flow.rs and write the following code:

fn main() {
    let num = 10;

    // If/else statement
    if num < 10 {
        println!("The number is less than 10.");
    } else {
        println!("The number is 10 or greater.");
    }

    // Match statement
    match num {
        0 => println!("The number is zero."),
        1..=9 => println!("The number is between 1 and 9."),
        _ => println!("The number is 10 or greater."),
    }

    // Loop
    let mut counter = 0;
    loop {
        counter += 1;
        if counter > 5 {
            break;
        }
        println!("Loop count: {}", counter);
    }

    // While loop
    counter = 0;
    while counter < 5 {
        counter += 1;
        println!("While loop count: {}", counter);
    }

    // For loop
    for num in 1..6 {
        println!("For loop count: {}", num);
    }
}

Here's a breakdown of the code:

  • if num < 10 checks if the variable num is less than 10. If it is, it executes the first branch and prints "The number is less than 10.". If it's not, it executes the else branch and prints "The number is 10 or greater.".
  • match num checks the value of num and executes the branch that matches it. _ is a catch-all pattern that matches any value.
  • The loop keyword creates an infinite loop. break is used to exit the loop when counter > 5.
  • The while loop executes as long as counter < 5 is true.
  • The for loop iterates over the range 1..6, which includes 1, 2, 3, 4, 5.

Compile and run this program to see control flow in action in Rust.

Fourth

Let's delve into Rust functions. Functions are at the heart of Rust, as they are used to structure and reuse code. We will look at defining and calling functions, parameters, return values, and higher order functions.

Create a new Rust file called functions.rs and write the following code:

// Define a function
fn greet() {
    println!("Hello, Rust!");
}

// Define a function with parameters
fn greet_person(name: &str) {
    println!("Hello, {}!", name);
}

// Define a function with a return value
fn add(a: i32, b: i32) -> i32 {
    a + b  // no semicolon, so this is an expression that gets returned
}

// Define a higher order function
fn apply<F>(f: F, x: i32, y: i32) -> i32
where
    F: Fn(i32, i32) -> i32,
{
    f(x, y)
}

fn main() {
    // Call a function
    greet();

    // Call a function with parameters
    greet_person("Alice");

    // Call a function with a return value
    let sum = add(5, 7);
    println!("5 + 7 = {}", sum);

    // Call a higher order function
    let product = apply(|a, b| a * b, 5, 6);  // pass in a closure that multiplies
    println!("5 * 6 = {}", product);
}

Here's what's happening in the code:

  • greet is a simple function with no parameters and no return value. It's called in main with greet().
  • greet_person has one parameter, a string slice (&str). It's called with the string "Alice".
  • add has two i32 parameters and returns their sum, also an i32. Note that the last line doesn't have a semicolon, so it's an expression and its value gets returned.
  • apply is a higher order function. It takes a function f as a parameter, as well as two i32 values, and it applies f to those values.
  • In main, apply is called with a closure |a, b| a * b, which multiplies its arguments, and the numbers 5 and 6.

Compile and run this program to see how functions work in Rust. By the end of this tutorial, you should have a solid understanding of how to define and use functions in Rust.

Fifths

Let's explore Rust's data structures - structs and enums.

Structs are similar to objects in JavaScript or classes in languages like Java and C++, while enums allow you to define a type by enumerating its possible variants.

Create a new Rust file called data_structures.rs and write the following code:

// Define a struct
struct Point {
    x: f64,
    y: f64,
}

// Define a function that takes a Point
fn print_point(point: Point) {
    println!("Point at ({}, {})", point.x, point.y);
}

// Define an enum
enum Direction {
    North,
    South,
    East,
    West,
}

// Define a function that takes a Direction
fn print_direction(direction: Direction) {
    match direction {
        Direction::North => println!("We're heading North!"),
        Direction::South => println!("We're heading South!"),
        Direction::East => println!("We're heading East!"),
        Direction::West => println!("We're heading West!"),
    }
}

fn main() {
    // Instantiate a Point
    let p = Point { x: 5.0, y: 7.0 };
    print_point(p);

    // Use an enum
    let dir = Direction::North;
    print_direction(dir);
}

Here's what's happening in the code:

  • Point is a struct with two fields, x and y, both of type f64.
  • print_point is a function that takes a Point as a parameter and prints its coordinates.
  • Direction is an enum with four variants: North, South, East, West.
  • print_direction is a function that takes a Direction and uses a match statement to print a different message for each possible variant.
  • In main, we create a Point and a Direction and pass them to print_point and print_direction, respectively.

Compile and run this program to see how structs and enums work in Rust. After this tutorial, you should be familiar with defining and using basic data structures in Rust.

Sixth

In this tutorial, we will discuss Rust's ownership, borrowing, and lifetimes, which are central to Rust's memory safety guarantees.

Create a new Rust file called ownership.rs and write the following code:

fn main() {
    // Ownership and functions
    let s = String::from("hello");  // s comes into scope
    takes_ownership(s);             // s's value moves into the function and is no longer valid here

    let x = 5;                      // x comes into scope
    makes_copy(x);                  // x would move into the function, but i32 is Copy, so it's ok to still use x afterward

    // Borrowing
    let s = String::from("hello");
    no_take_ownership(&s);          // s is borrowed, not owned

    // Mutable borrowing
    let mut s = String::from("hello");
    change(&mut s);                 // mutable borrowing
    println!("{}", s);

    // Lifetimes
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        let smallest = smallest_string(&string1, &string2);
        result = smallest;
    }
    println!("Smallest string: {}", result);
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // some_string goes out of scope and `drop` is called

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
} // some_integer goes out of scope. Nothing special happens.

fn no_take_ownership(some_string: &String) {
    println!("{}", some_string);
} // some_string goes out of scope. Nothing special happens.

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

fn smallest_string<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() < y.len() {
        x
    } else {
        y
    }
} // Returns a reference to the smallest string

Here's what's happening in the code:

  • takes_ownership takes ownership of a String. After it's called, the passed String can no longer be used.
  • makes_copy takes an i32, which is Copy. This means that the integer data gets copied and the original can still be used.
  • no_take_ownership and change illustrate borrowing. &s allows you to create a reference to s, but not take ownership. &mut s is a mutable reference.
  • smallest_string shows how lifetimes work. Lifetimes ensure that any reference to an object will not outlive the object itself.

Compile and run this program to see how ownership, borrowing, and lifetimes work in Rust. By the end of this tutorial, you should have a basic understanding of these concepts, which are fundamental to Rust's design.

Seventh

In this tutorial, we will explore error handling in Rust, which is a key aspect of any robust program. Specifically, we will look at Rust's Result type, which can be used for functions that might fail.

Create a new Rust file called error_handling.rs and write the following code:

use std::num::ParseIntError;

// This function may fail if the string cannot be parsed into an integer
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.parse()
}

fn main() {
    match parse_number("10") {
        Ok(num) => println!("It's a number: {}", num),
        Err(e) => println!("Error: {}", e),
    }

    match parse_number("ten") {
        Ok(num) => println!("It's a number: {}", num),
        Err(e) => println!("Error: {}", e),
    }

    // You can also use the `?` operator to propagate the error up
    let number = match "10".parse::<i32>() {
        Ok(num) => num,
        Err(e) => return Err(e.into()),  // convert ParseIntError into a Box<dyn Error>
    };
    println!("Number: {}", number);
}

Here's what's happening in the code:

  • The parse_number function takes a string and tries to parse it into an integer. If this fails, it returns an error.
  • In main, we call parse_number twice: once with a string that can be parsed into a number, and once with a string that can't. In each case, we use a match statement to handle the Ok and Err cases.
  • The ? operator can be used to propagate errors. If the expression before ? is an Err, it will return from the current function and give the error to the caller. If it's Ok, it will take the value out of Ok and continue the code. Note: The ? operator can be used in functions that return a Result (or Option). It can't be used in the main function directly.

Compile and run this program to see how error handling in Rust works. After this tutorial, you should have a basic understanding of the Result type and how to use it for error handling.

Eighths

In this tutorial, we'll dive into modules and packages in Rust. These tools allow you to structure and organize large projects. Additionally, we will touch on pub keyword and use declaration.

Create a new directory for your project:

cargo new modules_and_packages
cd modules_and_packages

Then, add a new file named lib.rs in the src directory of your project:

src/lib.rs

// Define a module named "greetings"
mod greetings {
    // By default, everything is private in Rust. The `pub` keyword makes it accessible outside this module.
    pub fn hello() {
        println!("Hello from the greetings module!");
    }
}

// Use the `greetings` module
pub use greetings::hello;

Next, modify the src/main.rs file:

src/main.rs

// Import our library. This would be the name of your crate.
extern crate modules_and_packages;

// Use the `hello` function from our library
use modules_and_packages::hello;

fn main() {
    // Call the `hello` function
    hello();
}

Now, from your terminal, in the modules_and_packages directory, run:

cargo run

You should see the output: "Hello from the greetings module!"

Here's a summary:

  • Modules allow you to group related definitions together and make them reusable.
  • The pub keyword makes items public, allowing them to be accessible outside their module.
  • use allows you to bring items into scope, making it easier to reference them in your code.
  • extern crate brings an external crate into your project, making its items accessible. (Note: With the 2018 edition of Rust, extern crate is often no longer needed, as it's implicitly added by Cargo. But it's good to know about in case you come across it in older Rust code.)

By the end of this tutorial, you should understand how to use modules to organize your code and how packages and crates work in Rust.

Ninths

In this tutorial, we'll dive into iterators and closures in Rust. Both are powerful features of Rust that enable functional programming patterns.

Iterators allow you to process a sequence of elements.
Closures are anonymous functions that you can store in a variable or pass as arguments to other functions.

Let's get started:

Create a new Rust file named iterators_and_closures.rs and write the following code:

fn main() {
    // Closures
    let add = |x, y| x + y;
    println!("5 + 3 = {}", add(5, 3));

    let numbers = vec![1, 2, 3, 4, 5];

    // Iterators
    // Using `map` to transform each element in the iterator
    let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
    println!("Doubled numbers: {:?}", doubled);

    // Using `filter` to select certain items
    let evens: Vec<_> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
    println!("Even numbers: {:?}", evens);
    
    // Using `find` to get the first match
    let first_greater_than_three = numbers.iter().find(|&&x| x > 3);
    match first_greater_than_three {
        Some(val) => println!("First number greater than 3: {}", val),
        None => println!("No number greater than 3 found"),
    }
}

Here's a breakdown of the code:

  • Closures: We define a simple closure add that takes two parameters and returns their sum. Closures are defined using |...| {...} syntax.

  • Iterators: We have a Vec<i32> named numbers. Using the iterator methods map, filter, and find, we can transform, select, or search the numbers, respectively.

    • map: Applies a function to each item and collects the results into a vector.
    • filter: Filters items based on a predicate (a function returning bool).
    • find: Returns the first item that matches a predicate.

Compile and run the program to explore the workings of iterators and closures in Rust. After this tutorial, you should understand how to use these powerful tools in your Rust programs to enable more functional programming patterns.

Tenths

In this tutorial, we'll delve into struct methods and associated functions, as well as the concept of lifetimes in Rust. Both are crucial aspects of the language that further its expressiveness and safety.

Struct Methods and Associated Functions
Methods are similar to functions, but they are associated with a specific instance of a type (like a struct or enum). Associated functions are similar to static methods in other languages, and they don't take an instance.

Lifetimes
Lifetimes are a way of expressing the scope of validity of references within Rust code. They ensure that references don't outlive the data they point to.

Let's explore:

Create a new Rust file named methods_lifetimes.rs and write the following code:

struct Circle {
    radius: f64,
}

impl Circle {
    // Associated function (like static methods in other languages)
    fn new(radius: f64) -> Circle {
        Circle { radius }
    }

    // Method (works on an instance)
    fn area(&self) -> f64 {
        3.14159265358979323846 * self.radius * self.radius
    }
}

// A function with lifetimes
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let circle = Circle::new(5.0);  // Call associated function
    println!("Circle area: {}", circle.area());  // Call method

    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(&string1, &string2);
    }
    println!("Longest string: {}", result);
}

Key points in the code:

  • Circle Struct: This represents a geometric circle with a defined radius.

  • impl Block: Within this block, methods and associated functions related to the Circle struct are defined.

    • new: An associated function that returns a new instance of Circle.
    • area: A method that calculates the area of the circle. &self refers to the instance the method is called on.
  • longest Function: This function determines the longest of two string slices. It uses lifetimes ('a) to indicate that the returned reference has the same lifetime as the shortest of the two input lifetimes.

Compile and run the code. By the end of this tutorial, you should have a grasp on struct methods, associated functions, and a basic understanding of lifetimes in Rust.

Eleventh

In this tutorial, we'll look into enums and pattern matching in Rust. Enums (short for "enumerations") are a way to represent data that can be one of several possible variants. Pattern matching is an elegant way to handle such data.

Enums
Enums are similar to "sum types" in other languages. In Rust, they are a powerful construct that can hold data in their variants.

Pattern Matching
Pattern matching is a feature that lets you destructure and match on the data held in data structures, including enums.

Let's dive in:

Create a new Rust file named enums_patterns.rs and write the following code:

// Define an enum to represent a web event
enum WebEvent {
    PageLoad,
    PageUnload,
    KeyPress(char),
    Click { x: i64, y: i64 },
}

// Function to process a web event
fn process_event(event: WebEvent) {
    match event {
        WebEvent::PageLoad => println!("Page loaded!"),
        WebEvent::PageUnload => println!("Page unloaded!"),
        WebEvent::KeyPress(c) => println!("Key '{}' pressed!", c),
        WebEvent::Click { x, y } => println!("Clicked at x={}, y={}", x, y),
    }
}

fn main() {
    let pressed_key = WebEvent::KeyPress('x');
    let click_position = WebEvent::Click { x: 20, y: 80 };

    process_event(pressed_key);
    process_event(click_position);
}

Here's the breakdown:

  • WebEvent Enum: This enum represents different types of web events. Some variants hold data, like KeyPress, which has a char, and Click, which has two i64 values.

  • process_event Function: This function takes a WebEvent as an argument. It uses the match keyword to perform pattern matching on the event, executing different code depending on the event type.

  • main Function: We create two instances of WebEvent and pass them to process_event to see the pattern matching in action.

Compile and run the program to see how enums and pattern matching work together. By the end of this tutorial, you should understand how to define enums, store data in their variants, and use pattern matching to handle different enum variants.

Twelvfth

Certainly! In this tutorial, we'll focus on traits and generics in Rust. Both of these concepts are pivotal to writing reusable and flexible code.

Traits
A trait is a way to define shared behavior across types. Think of them as similar to interfaces in other languages.

Generics
Generics let you write a data type or function that is abstracted over types, ensuring type safety.

Let's proceed:

Create a new Rust file named traits_generics.rs and write the following code:

// Define a trait named `Printable`
trait Printable {
    fn format(&self) -> String;
}

// Implement `Printable` for `i32`
impl Printable for i32 {
    fn format(&self) -> String {
        format!("i32: {}", *self)
    }
}

// Implement `Printable` for `String`
impl Printable for String {
    fn format(&self) -> String {
        format!("String: {}", *self)
    }
}

// A generic function that takes a `Printable` type
fn print_it<T: Printable>(item: T) {
    println!("{}", item.format());
}

fn main() {
    let my_string = String::from("Hello");
    let my_int = 5;

    print_it(my_string);
    print_it(my_int);
}

Here's an overview:

  • Printable Trait: This trait contains a single method format which returns a String.

  • Implementing the Trait: We implement Printable for both i32 and String. Each type provides its custom behavior for the format method.

  • print_it Function: This is a generic function. The <T: Printable> syntax means "for some type T that implements the Printable trait." The function can then call the format method on its argument, ensuring it's compatible with any Printable type.

  • main Function: We create a String and an i32, then pass them to print_it, demonstrating the function's generic behavior.

When you compile and run the program, you will see how the same function, print_it, can operate on different types, thanks to traits and generics.

After completing this tutorial, you should be familiar with how to define traits, implement them for various types, and create generic functions that operate on trait-bounded types. This will enable you to write more reusable and type-safe Rust code.

Thirteenth

In this tutorial, we'll explore error handling in Rust. Rust takes a unique approach to handle errors by using types to represent success and failure, rather than relying on exceptions as many other languages do.

The two primary error-handling types in Rust are Option<T> and Result<T, E>.

Option
The Option type expresses the possibility of absence. It's an enum with two variants: Some(T) and None.

Result<T, E>
The Result type is used for functions that can fail. It's an enum with two variants: Ok(T) for success and Err(E) for errors.

Let's dive in:

Create a new Rust file named error_handling.rs and write the following code:

// Function that might fail, returning an Option
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

// Function that might fail, returning a Result
fn sqrt(number: f64) -> Result<f64, String> {
    if number < 0.0 {
        Err("Cannot take the square root of a negative number.".to_string())
    } else {
        Ok(number.sqrt())
    }
}

fn main() {
    match divide(5.0, 0.0) {
        Some(result) => println!("Division result is {}", result),
        None => println!("Cannot divide by zero"),
    }

    match sqrt(-9.0) {
        Ok(result) => println!("Square root is {}", result),
        Err(error) => println!("Error: {}", error),
    }
}

Key Points:

  • divide Function: This function returns an Option<f64>. It checks if the denominator is zero and returns None if it is, indicating the absence of a valid value.

  • sqrt Function: This function returns a Result<f64, String>. It checks if the number is negative and, if so, returns an Err with an error message. Otherwise, it returns the square root inside an Ok.

  • main Function: Here, we use pattern matching with match to handle the potential values (or errors) returned by our functions.

When you compile and run the program, you'll see how Rust handles errors in a type-safe manner, providing clear paths for success and failure scenarios.

After this tutorial, you should understand how to use Option and Result to gracefully handle errors in Rust, making your code robust and readable.

Fourteenth

In this tutorial, we'll explore modules and visibility in Rust. These concepts are essential for organizing code and creating reusable libraries.

Modules
Modules allow you to organize code into separate namespaces, facilitating code reuse and readability.

Visibility
Rust provides a powerful system to control the visibility of items (functions, structs, etc.) with the pub keyword, ensuring encapsulation.

Let's dive in:

Create a new Rust file named modules_visibility.rs and write the following code:

// Define a module named "math"
mod math {
    // Private function (not accessible outside the module)
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    // Public function (accessible outside the module)
    pub fn multiply(a: i32, b: i32) -> i32 {
        a * b
    }

    // Public nested module
    pub mod advanced {
        pub fn power(base: i32, exponent: i32) -> i32 {
            (0..exponent).fold(1, |acc, _| acc * base)
        }
    }
}

fn main() {
    // Accessing the public function from the math module
    println!("5 * 3 = {}", math::multiply(5, 3));

    // Accessing the public function from the nested module
    println!("2 power 3 = {}", math::advanced::power(2, 3));
    
    // This line would cause a compile error, because the `add` function is private
    // println!("5 + 3 = {}", math::add(5, 3));
}

Key Points:

  • math Module: We've defined a module named math. Inside this module, we have two functions: a private function add and a public function multiply.

  • advanced Submodule: Within the math module, we've defined another module named advanced. It has a public function power.

  • main Function: Here, we're accessing the functions from the math module and its submodule advanced. You'll notice that we commented out the call to the add function since it's private and cannot be accessed outside its module.

Compile and run the program. You'll see how modules in Rust allow you to organize and encapsulate your code, making it more maintainable and reusable.

After this tutorial, you should understand the basics of creating modules in Rust and controlling the visibility of items within those modules. This will be invaluable when you start building larger projects or libraries.

Fifteenth

In this tutorial, we'll explore closures and higher-order functions in Rust. Closures are a powerful feature, allowing you to capture variables from the surrounding environment. Higher-order functions are functions that take other functions (or closures) as arguments or return them.

Closures
Closures in Rust look like lambda functions or anonymous functions in other languages. They can capture values from their environment.

Higher-order Functions
Functions that can take other functions as parameters or return functions.

Let's dive in:

Create a new Rust file named closures_hof.rs and write the following code:

// A function that returns a closure
fn multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    |number| number * factor
}

// A higher-order function that applies a function to a value
fn apply<F>(func: F, value: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    func(value)
}

fn main() {
    // Simple closure that captures the `factor` variable
    let factor = 3;
    let triple = |x| x * factor;

    println!("Triple of 5 is: {}", triple(5));

    // Using a function that returns a closure
    let double = multiplier(2);
    println!("Double of 5 is: {}", double(5));

    // Using a higher-order function
    println!("Triple of 7 using apply: {}", apply(triple, 7));
}

Key Points:

  • triple Closure: We define a simple closure that triples its input. It captures the factor variable from the surrounding environment.

  • multiplier Function: This function returns a closure. The returned closure multiplies its input by the given factor.

  • apply Function: This is a higher-order function that takes a function func as a parameter and an i32 value, then applies the function to the value.

  • main Function: We demonstrate using the triple closure, the closure returned by multiplier, and the higher-order function apply.

Compile and run the program. Closures and higher-order functions allow for concise, expressive, and flexible code in Rust.

After this tutorial, you should have a foundational understanding of closures in Rust, how they can capture their environment, and how to utilize higher-order functions for more flexible code patterns.

Sixteenth

In this tutorial, we'll delve into concurrency in Rust. Concurrency is about executing multiple tasks at the same time, and Rust offers several tools to manage concurrent code safely.

We'll particularly explore threads in this tutorial.

Threads
Threads allow multiple operations to run in parallel. However, concurrent programming can introduce various pitfalls if not handled correctly. Rustโ€™s type system and ownership rules play a significant role in getting concurrency right.

Let's get started:

Create a new Rust file named concurrency_threads.rs and write the following code:

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

// A simple function that simulates a heavy computation
fn heavy_calculation(number: i32) -> i32 {
    println!("Computing for number {}", number);
    thread::sleep(Duration::from_secs(2));
    number * number
}

fn main() {
    // Spawn a new thread to run the heavy_calculation function
    let handle = thread::spawn(|| {
        let result = heavy_calculation(5);
        println!("Result from thread: {}", result);
    });

    // Do some work in the main thread as well
    for i in 1..5 {
        println!("Main thread working on: {}", i);
        thread::sleep(Duration::from_millis(300));
    }

    // Wait for the spawned thread to finish
    handle.join().unwrap();
}

Key Points:

  • heavy_calculation Function: This function simulates a computation that takes a couple of seconds.

  • thread::spawn: This spawns a new thread. The code inside the closure will be executed in this new thread.

  • handle.join(): This ensures the main thread waits for the spawned thread to finish before exiting.

  • main Function: Here, we spawn a new thread to run heavy_calculation while the main thread continues with its loop. Both operations occur concurrently.

Compile and run the program. You'll notice the main thread and the spawned thread run concurrently, showcasing basic multi-threading in Rust.

After this tutorial, you should have a basic understanding of how to use threads in Rust for concurrent programming. Rust's concurrency model ensures safety by preventing data races and other concurrency pitfalls through its type system and ownership rules.

Seventeenth

In this tutorial, we'll delve deeper into Rust's concurrency model by exploring the message-passing paradigm, particularly with channels. This concept allows threads to communicate safely without shared state, reducing the chances of race conditions.

Channels
Channels provide a mechanism for multiple threads to communicate by sending messages. Rustโ€™s standard library provides a channel function that creates a new channel.

Let's dive in:

Create a new Rust file named message_passing_channels.rs and write the following code:

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    // Create a simple channel
    let (tx, rx) = mpsc::channel();

    // Spawn a new thread
    thread::spawn(move || {
        let values = vec![
            String::from("Hello"),
            String::from("from"),
            String::from("the"),
            String::from("spawned"),
            String::from("thread!"),
        ];

        for value in values {
            tx.send(value).unwrap();
            thread::sleep(Duration::from_millis(600));
        }
    });

    // Receive messages in the main thread
    for received in rx {
        println!("Received: {}", received);
    }
}

Key Points:

  • mpsc::channel: This function creates a new channel and returns two ends: the transmitter (tx) and the receiver (rx).

  • tx.send(value): The spawned thread sends a series of messages via the transmitter.

  • rx: The receiver acts as an iterator. In the main thread, we loop through messages received from the channel until it's closed.

  • move Closure: We use the move keyword to move the ownership of values into the closure. Here, it ensures the transmitter end (tx) of the channel is owned by the spawned thread.

Compile and run the program. You'll see messages sent from the spawned thread and received in the main thread in the order they were sent, illustrating the concept of message passing between threads.

After this tutorial, you should understand how channels in Rust provide a safe and efficient mechanism for threads to communicate, encapsulating the complexities of concurrent programming and making it more approachable and less error-prone.

Eighteenth

In this tutorial, we'll explore the concept of Smart Pointers in Rust. Smart pointers are data structures that not only act like pointers but also have additional metadata and capabilities. One of the most commonly used smart pointers in Rust is Box<T>.

Box
A Box<T> allows you to store data on the heap rather than the stack. What makes it "smart" is that it automatically cleans up the heap memory when the Box goes out of scope.

Let's dive into the basics of Box<T>:

Create a new Rust file named smart_pointers_box.rs and write the following code:

// Define a simple recursive data structure using Box
enum List {
    Cons(i32, Box<List>),
    End,
}

use List::{Cons, End};

fn main() {
    // Using the Box to create a recursive data structure
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(End))))));

    // Function to compute the sum of the list
    fn sum_list(l: &List) -> i32 {
        match l {
            Cons(value, next) => value + sum_list(next),
            End => 0,
        }
    }

    println!("Sum of list is: {}", sum_list(&list));
}

Key Points:

  • List Enum: The List enum is a recursive data structure, where each element (Cons) holds an integer and a box that points to the next element. The last element is End.

  • Box::new: This creates a new box and places the data on the heap. In this case, we use it to allocate each Cons variant on the heap.

  • sum_list Function: This function recursively computes the sum of all elements in the list.

Compile and run the program. It will compute and display the sum of the elements in the list.

After this tutorial, you should understand the basic usage of the Box<T> smart pointer in Rust. It allows for heap allocation and is especially useful for building recursive data structures due to its deterministic cleanup of heap memory when the data goes out of scope.

Nineteenth

In this tutorial, we'll delve deeper into smart pointers and explore another important one: Rc<T> (Reference Counted). While Box<T> ensures data on the heap is cleaned up when it's no longer needed, Rc<T> allows data on the heap to be shared among multiple parts of your program.

Rc
The "Rc" stands for "Reference Counting". This smart pointer tracks the number of references to a value which determines whether or not a value is still in use. If there are zero references to a value, the value can be cleaned up without any references becoming invalid.

Let's explore the use of Rc<T>:

Create a new Rust file named smart_pointers_rc.rs and write the following code:

use std::rc::Rc;

#[derive(Debug)]
struct Data {
    value: i32,
}

fn main() {
    let data = Rc::new(Data { value: 42 });

    println!("Reference count after creating data: {}", Rc::strong_count(&data));

    {
        let reference1 = Rc::clone(&data);
        let reference2 = Rc::clone(&data);
        
        println!("Reference count after creating two references: {}", Rc::strong_count(&data));

        // Accessing data through one of the references
        println!("Data value from reference1: {:?}", reference1);
    }

    println!("Reference count after inner scope ends: {}", Rc::strong_count(&data));
}

Key Points:

  • Rc::new: This wraps the Data struct in an Rc<T> smart pointer.

  • Rc::clone: This does not deep-copy the data. Instead, it increments the reference count.

  • Rc::strong_count: This returns the current reference count, allowing us to track how many references to a value currently exist.

Compile and run the program. You'll notice the reference count changes as you create and drop references to the Rc<T>.

After this tutorial, you should understand how Rc<T> in Rust provides a way to share heap-allocated data safely among multiple parts of your program. It ensures that the data remains alive as long as there's at least one reference to it and cleans up the data when the reference count drops to zero.

Twenties

In this tutorial, we'll explore another crucial smart pointer: RefCell<T>. While Rust's borrowing rules enforce at compile time that you either have multiple immutable references or a single mutable reference, RefCell<T> allows you to bypass these rules and enforce borrowing rules at runtime.

RefCell
This smart pointer represents single ownership over the data it holds, but it allows mutable borrowing checked at runtime.

Let's understand how to use RefCell<T>:

Create a new Rust file named smart_pointers_refcell.rs and write the following code:

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

#[derive(Debug)]
struct Data {
    value: RefCell<i32>,
    siblings: RefCell<Vec<Rc<Data>>>,
}

fn main() {
    let data = Rc::new(Data {
        value: RefCell::new(42),
        siblings: RefCell::new(Vec::new()),
    });

    let sibling1 = Rc::new(Data {
        value: RefCell::new(10),
        siblings: RefCell::new(Vec::new()),
    });

    let sibling2 = Rc::new(Data {
        value: RefCell::new(20),
        siblings: RefCell::new(Vec::new()),
    });

    data.siblings.borrow_mut().push(Rc::clone(&sibling1));
    data.siblings.borrow_mut().push(Rc::clone(&sibling2));

    // Modifying value inside RefCell
    *data.value.borrow_mut() += 10;

    println!("Updated data value: {:?}", data.value);
    println!("Data siblings: {:?}", data.siblings.borrow().len());
}

Key Points:

  • RefCell::new: Wraps data within a RefCell, giving the ability to have multiple mutable references to this data at runtime.

  • borrow_mut(): Provides a mutable reference to the inner data if no other mutable references currently exist. This is checked at runtime.

  • siblings.borrow(): Provides an immutable reference to the inner data.

Compile and run the program. You'll see that RefCell<T> allows you to modify data even if there are multiple references to it, as long as only one mutable reference exists at a time.

Twenty first

In this tutorial, we'll delve into Rust's pattern matching mechanism by discussing match expressions and destructuring.

match Expressions
Rust's match expression is a powerful tool for pattern matching, which lets you compare a value against a series of patterns and then execute code based on the first pattern that matches.

Let's explore pattern matching:

Create a new Rust file named pattern_matching.rs and write the following code:

enum WebEvent {
    PageLoad,
    PageUnload,
    KeyPress(char),
    Click { x: i64, y: i64 },
}

fn web_event_handler(event: WebEvent) {
    match event {
        WebEvent::PageLoad => println!("Page loaded!"),
        WebEvent::PageUnload => println!("Page unloaded!"),
        WebEvent::KeyPress(c) => println!("Key '{}' pressed!", c),
        WebEvent::Click { x, y } => println!("Clicked at x={}, y={}", x, y),
    }
}

fn main() {
    let pressed = WebEvent::KeyPress('x');
    let clicked = WebEvent::Click { x: 20, y: 80 };

    web_event_handler(pressed);
    web_event_handler(clicked);
}

Key Points:

  • WebEvent Enum: This enum represents various web events we might be interested in.

  • match expression: The match expression checks the provided value (in this case, an instance of WebEvent) against all the given patterns and executes the associated code for the first match.

  • Destructuring: In the patterns WebEvent::KeyPress(c) and WebEvent::Click { x, y }, we're destructuring the enum variants to get the inner values, which we then use in the code associated with the pattern.

Compile and run the program. You'll see that the appropriate messages get printed based on the events processed by the web_event_handler function.

After this tutorial, you should understand how the match expression provides a concise way to handle various patterns in Rust. It's a powerful tool for control flow and lets you handle different possibilities in a clear and readable manner.

Twenty second

In this tutorial, we'll explore Closures in Rust. Closures are anonymous functions you can save in a variable or pass as arguments to other functions.

Closures
A closure captures values from the environment in which it's defined. They have the ability to capture values from their surrounding scope and can be short and expressive.

Let's delve into closures:

Create a new Rust file named closures.rs and write the following code:

fn main() {
    // Simple closure with no parameters
    let greet = || {
        println!("Hello, Rust!");
    };

    greet();

    // Closure with parameters
    let add = |x, y| {
        x + y
    };

    let result = add(5, 7);
    println!("5 + 7 = {}", result);

    // Closure that captures environment variables
    let factor = 3;
    let multiply = |x| x * factor;

    println!("10 times factor = {}", multiply(10));
}

Key Points:

  • Closure Syntax: Closures are defined using a pair of vertical bars ||, followed by a block of code. This block of code can capture variables from the surrounding environment.

  • Environment Capture: The multiply closure captures the factor variable from its surrounding environment.

Compile and run the program. You'll notice the different ways closures can be used in Rust, from simple no-argument closures to ones that capture environment variables.

Advanced

First

Let's explore some lesser-known, advanced Rust techniques and optimizations:

  1. Zero-Cost Abstractions: Rust promises that abstractions won't have a runtime cost.

    For instance, you can use iterators and higher-order functions like map and filter without fearing performance degradation.

    let sum: u32 = (0..1000).filter(|&x| x % 2 == 0).sum();

    The above code is as fast as the traditional loop but more expressive.

  2. Inlining: Rust's #[inline] attribute hints the compiler to inline a function, which can improve performance.

    #[inline]
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
  3. Custom Allocators: By default, Rust uses the system allocator, but you can specify custom allocators to optimize memory usage patterns for specific use-cases.

  4. Const Functions and const fn: These allow computation at compile-time. It's a way of getting more done during compilation and less during runtime.

    const fn compute_val() -> usize {
        // Some compile-time computation
        5 * 5
    }
    
    const VAL: usize = compute_val();
  5. Use of unsafe: While the goal is to avoid using unsafe in Rust, sometimes, for performance reasons, it can be justified. This allows for optimizations that can't be done safely in pure Rust.

  6. Custom Derive and Procedural Macros: You can generate custom implementations for your code. This allows for powerful metaprogramming techniques. For instance, the serde crate uses this to generate serialization/deserialization code for custom structs.

  7. Pattern Matching Optimizations: Rust's match is optimized. A common trick is to match against literals which can be optimized into a jump table at compile-time.

  8. Lazy Static: If you want to initialize something only once and use it across multiple calls or threads, lazy_static crate can be used:

    lazy_static! {
        static ref RE: Regex = Regex::new("...").unwrap();
    }
  9. Optimizing Builds: Using lto (Link Time Optimizations) and codegen units, you can further optimize binary sizes and performance.

    [profile.release]
    lto = true
    codegen-units = 1
  10. Using jemalloc: By default, Rust uses the system allocator. However, for certain workloads, using the jemalloc allocator might improve performance.

These are just some advanced techniques in Rust. As with all optimizations, it's essential to measure performance improvements with real-world data and understand the trade-offs being made.

Second

We'll delve into the standard library's offerings for concurrency and parallelism, providing more detailed explanations and advanced examples.

Rust's Standard Concurrency Mechanisms:

1. Threads

The most fundamental unit of execution in most operating systems is a thread. In Rust, you spawn a new thread using std::thread::spawn.

Usage: You should use raw threads for CPU-bound tasks or when you need a longer-lived, isolated piece of computation.

Advanced Example:

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

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("Thread says {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("Main thread says {}", i);
        thread::sleep(Duration::from_millis(2));
    }

    handle.join().unwrap();
}

2. Mutex (Mutual Exclusion)

A Mutex ensures that only one thread can access some data at any given time. The type name Mutex stands for "mutual exclusion".

Usage: Use a mutex to protect shared data from concurrent modification.

Advanced Example:

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        let handle = thread::spawn({
            let counter = &counter; // Make a reference to the Mutex
            move || {
                let mut num = counter.lock().unwrap();
                *num += 1;
            }
        });
        handles.push(handle);
    }

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

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

3. Rc (Reference Counting) and Arc (Atomic Reference Counting)

Rc and Arc are reference-counted pointers. While Rc is for use in single-threaded scenarios, Arc is for multi-threaded situations.

Usage: Use Arc when you need multiple owners of the data, and the data can be accessed from multiple threads.

Advanced Example:

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

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

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

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

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

4. Channels

Channels are a powerful feature in Rust to send data between threads.

Usage: Use channels for message-passing concurrency where you want to send data from one thread to another.

Advanced Example:

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    let tx1 = mpsc::Sender::clone(&tx);

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

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

    for received in rx {
        println!("Got: {}", received);
    }
}

5. Barriers

A barrier is a thread synchronization primitive. Barriers allow multiple threads to synchronize the beginning of some computation.

Usage: Use when you want to ensure all threads have reached a certain point before any continue.

Advanced Example:

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

fn main() {
    let iterations = 10;

    let barrier = Arc::new(Barrier::new(iterations));
    let mut handles = Vec::with_capacity(iterations);

    for _ in 0..iterations {
        let barrier_clone = barrier.clone();
        handles.push(thread::spawn(move || {
            println!("Before wait");
            barrier_clone.wait();
            println!("After wait");
        }));
    }

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

rustcheatsheet's People

Contributors

ladroid avatar

Stargazers

 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.