Skip to content

Latest commit

 

History

History
executable file
·
1525 lines (1137 loc) · 47.7 KB

rust-chapter-1-fundamentals.md

File metadata and controls

executable file
·
1525 lines (1137 loc) · 47.7 KB

What is Rust / Why Rust?

Rust Overview:

  • Rust is a system programming language focused on performance, reliability, and "safety".
  • Designed for low-level control over system resources, preventing common programming errors like null or dangling pointers, buffer overflows, and data races.

Key Features:

  • Memory Safety: Rust's ownership and borrowing systems prevent common errors, enhancing code reliability and security.
  • Performance: Provides low-level control over hardware resources, leading to high performance in systems programming tasks.
  • Concurrency: Built-in support for concurrency and parallelism, making it easier to write multithreaded code without data races.
  • Community: A growing community contributes to open-source libraries and tools, providing resources and support for Rust projects.
  • Expressiveness: Rust offers a rich set of features for expressing complex ideas concisely. Its functional programming style aids code readability and bug avoidance.

Installing Rust:

Run the following command:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

For Windows, additional requirements include Visual Studio Build Tools. Download Visual Studio and install the "rust-analyser" extension.

Documentation:

Explore Rust documentation for learning:

Creating Our First Rust Project:

Use the following command to create a new Rust project named "rust_project":

cargo new rust_project

What is Cargo?

  • Cargo is the Rust package manager, similar to pip in Python.
  • It helps in project creation, building, and compilation.
  • Inside the project, you'll find src with main.rs and Cargo.toml. The latter is crucial, containing project configurations and information, including dependencies.

Running the Project: To execute the project, navigate to its directory and run:

cargo run

Hello World:

Main Function and Macros:

The following code represents the quintessential "Hello, World!" program in Rust:

fn main() {
    println!("Hello, world!");
}
  • fn: Stands for "function," and in Rust, it denotes a mini-program or block of code.
  • main: This is the main function where the program begins its execution.
  • Macros: println! is a macro, which is a built-in function. The "!" sign indicates its macro nature. In Rust, macros provide powerful meta-programming capabilities.
    • Alternative: You can use print! if you want to print without a new line.

Comments:

Rust supports both single-line and multi-line comments:

  • Single-line Comment:

    // This is a single line comment
  • Multi-line Comment:

    /*
    This is a multi-line comment
    This is a comment block
    */

Errors:

In Rust, certain rules must be followed to avoid errors:

  • Single quotes (') should not be used for strings; otherwise, the compiler will throw an error, specifically E0762 - unterminated character literal.
  • Characters should be enclosed in single quotes.
  • To get more information about a specific error, such as E0762, you can use the --explain flag with rustc:
    rustc --explain E0762

Primitive (Scalar Types)

In Rust, primitive data types, also known as scalar types, include integers, floats, booleans, and characters.

Integers:

Integers in Rust can be categorized into two types: Unsigned (never negative) and Signed (can be negative or positive). Each type has variants based on the number of bits it occupies.

  • Unsigned Integers: u8, u16, u32, u64, u128, usize
  • Signed Integers: Default is i32, and variations include i8, i16, i32, i64, i128, isize

Integer Literals: These are representations of values directly in source code, using various bases such as decimal, binary, hex, and octal, as well as byte.

Example:

fn main() {
    println!("Max size of an i32: {}", i32::MAX);
}

The numbers following i or u denote the number of bits. The higher the bit, the more data it can hold. usize and isize represent the number of bytes required to reference any location in memory.

Floats:

Floats in Rust come in two types: f32 and f64, with 32-bit and 64-bit precision, respectively. It's important to note that .14 is not a float; the left-hand side should not be empty.

Example:

fn main() {
    println!("Max size of an f32: {}", f32::MAX);
}

Boolean:

Booleans in Rust are represented by the bool type and can only be true or false.

Example:

let is_true = true;
let is_false = false;

Characters:

Characters in Rust are represented by the char type, occupying 4 bytes. Characters can be emojis, Unicode characters, or any data that fits within 4 bytes. It's important to note that a character is not limited to letters and is distinct from a string.

fn main() {
    let emoji = '😊';
    println!("Character: {}", emoji);
}

Variables

In Rust, variables are distinctly different from many other languages. They are immutable by default, meaning once a value is assigned, it cannot be changed. However, you can make a variable mutable by using the mut keyword.

Examples:

Standard Variable:

fn main() {
    let hello: &str = "hello world!";
    println!("{}", hello);
}

Mutable Variable:

fn main() {
    let mut hello: &str = "hello world!";
    println!("{}", hello);

    hello = "hello again!";
    println!("{}", hello);
}

Numeric Variables:

fn main() {
    let x: i32 = 5;
    let y: i32 = 7;
    println!("Math in Rust: {} + {} = {}", x, y, x + y);
}

Constants:

fn main() {
    const NUMBER: i32 = 27;
    println!("{}", NUMBER);
}

Constants are declared using uppercase snake_case and can be used globally. They are immutable and offer performance benefits during compilation.

Scope and Shadowing

Scope:

fn main() {
    let x: i32 = 5;
    {
        let y: i32 = 7;
        println!("Math: x + y = {}", x + y);
    }
    println!("Math: x + y = {}", x); // This line will throw an error.
}

Variables have scope, and local variables work within the scope they are defined. Attempting to use a variable outside its scope will result in an error.

Shadowing:

Example 1:

fn main() {
    let x: i32 = 5;
    {
        let x: i32 = 7;
        println!("{}", x);
    }
    println!("{}", x);
}

Here, the variable x is shadowed within the inner scope.

Example 2:

fn main() {
    let x: i32 = 5;
    let x: &str = "hello!";
    println!("{}", x);
}

This results in a warning for an unused variable (x), and hello! is printed. Shadowing allows redeclaration within the same scope.

To remove the warning, you can prefix the variable with an underscore (_x) or use #![allow(unused)] at the beginning of the page.

Suffixes and Underscores

Suffixes:

Suffixes specify the type of numeric literal.

fn main() {
    let x: u32 = 5;
    let y: u32 = 6u32;
    let z: u32 = 7_u32;
    println!("{}, {}, {}", x, y, z);
}

Underscores:

Underscores can be used as separators in numeric literals to enhance readability.

fn main() {
    let x: i32 = 1_000_000;
}

Primitives (Compound Types)

In Rust, compound types include tuples and arrays,each serving distinct purposes.

Tuples

Tuples allow the grouping of multiple values of any type. They have a maximum size of 12 entries.

Example:

fn main() {
    let student_a: (&str, char, f64) = ("Bob", 'A', 7.9);
    let (name_student_a, grade_student_a, gpa_student_a) = student_a;

    println!(
        "Student name is {}, his class grade is {}, his overall GPA is {}.",
        name_student_a, grade_student_a, gpa_student_a
    );
}
  • Note: When using type annotations for each element of the tuple, be cautious, as it may work in some environments like Visual Studio but might not work in the terminal.
    • e.g., : let (name_student_a: &str, grade_student_a: char, gpa_student_a: f64) = student_a;
      • this will work in Visual Studio, not in terminal.

Arrays

Arrays in Rust differ from tuples as they require elements to be of the same data type. They use square brackets [] and can store up to 32 values.

Example:

fn main() {
    let students: [&str; 4] = ["Bob", "Linda", "David", "John"];
    println!("The first student in the array is {}.", students[0]);
}

Slices

Slices allow the creation of subsets from elements within a collection, such as an array, without creating a new collection.

Example:

fn main() {
    let array: [i32; 7] = [1, 2, 3, 4, 5, 6, 7];
    let slice = &array[1..5];
    println!("{:?}", slice);
}

Slices are also mutable and can be used to modify the original array:

fn main() {
    let mut array: [i32; 7] = [1, 2, 3, 4, 5, 6, 7];
    let slice = &mut array[1..5];
    
    slice[0] = 8;
    slice[1] = 9;
    println!("{:?}", slice);
    println!("{:?}", array);
}

Slices are useful for working with portions of data within a larger collection without duplicating it.


Strings

Strings Overview

In Rust, there are several types of strings, but commonly used are String and &str.

  • str: String slice, immutable, and borrowed (data cannot be modified).
  • &str: Borrowed string slice, a subset of String, consisting of a pointer, bytes, and length.
  • String: String with the capacity to modify its data.

When dealing with static, unchanging data like a greeting, &str is preferred. If modification is necessary, String is the appropriate choice.

Example:

fn main() {
    let mut name: &str = "Bob";
    // name.push_str("test") --> Error: "method not found in &str"
    
    let mut name: String = String::new();
    name.push_str("Bob");
    name.push_str(" test");
    println!("{}", name);
}

Alternatively, converting a string literal to a String can be achieved using the to_string() method.

fn main() {
    let mut name = "Bob".to_string();
    name.push_str(" test");
    println!("{}", name);
}

Or by using String::from():

fn main() {
    let mut name: String = String::from("Bob");
    name.push_str(" test");
    println!("{}", name);
}

Character Escaping

Character escaping is essential for writing readable code, especially when including quotes.

Example:

fn main() {
    println!("\"I think everybody should study ants and their philosophy—it’s simple, but it’s powerful: \
        Never give up, look ahead, stay positive and do all you can.\"\
        -Jim Rohn");
}

Additionally, string concatenation can be performed using the concat! macro.

Example:

fn main() {
    println!("Hello World!");
    println!("{}", concat!("Hello", " World", "!"));
}

String handling in Rust involves understanding the differences between &str and String and selecting the appropriate type based on the use case.


User Input

Modules and Libraries

In Rust, modules and libraries are crucial for organizing and reusing code effectively. The use keyword is used to bring modules and libraries into scope.

use std::io;

For more information, refer to: std

User Input

Example:

#![allow(unused)]
use std::io;

fn main() {
    println!("Enter your name: ");
    let mut name: String = String::new();
    io::stdin().read_line(&mut name);

    let enter: &str = "You may now enter.";
    println!("Hello there {}. {}", name.trim_end(), enter);
}

In the example, the std::io module is imported to facilitate user input. The read_line method is then used to capture input from the user. The entered data is stored in a String variable, and the trim_end method is applied to remove trailing whitespaces. User input is a fundamental aspect of interactive Rust applications, and utilizing modules like std::io simplifies the process.


Math Operators

Overview

Mathematical operations are fundamental in programming, and Rust provides various operators to perform calculations. In this example, we explore basic math operations using variables of type i32 and f64.

fn main() {
    let x: i32 = 7;
    let y: i32 = 3;
    let x_float: f64 = x as f64;
    let y_float: f64 = y as f64;

    println!("{} / {} = {}", x, y, x_float / y_float);
    println!("{} % {} = {}", x, y, x_float % y_float);

    // The following line will throw an error (expected `u32`, found `i32`)
    // Use `try_into().unwrap()` as suggested by the compiler, but it's not recommended.
    // Instead, it is better to use `u32` directly to avoid potential issues.
    println!("{} ^ {} = {}", x, y, i32::pow(x, y.try_into().unwrap()));
}

In this example, we perform division (/) and modulo (%) operations using both integer (i32) and floating-point (f64) variables. Additionally, an attempt to calculate exponentiation (^) using i32::pow is made. However, the compiler suggests converting y to u32 using try_into().unwrap() due to type mismatch.

It's important to note that exponentiation with i32::pow expects a u32 type, and caution is advised when using the suggested conversion, as it may introduce unexpected behavior.


Dependencies in Rust

Overview

Dependencies are external packages or libraries that enhance the functionality of your Rust project. In Rust, managing dependencies is done through the Cargo.toml file, and one popular platform to find and share Rust packages is crates.io.

Adding Dependencies

To include a dependency in your project, you need to specify it in the Cargo.toml file under the [dependencies] section. For example:

[dependencies]
rand = "0.8.5"

In this example, the project depends on the rand crate, and the version is specified as "0.8.5". You can find the appropriate version information on crates.io or the documentation associated with the crate.

Using Dependencies

After adding a dependency, you can use it in your Rust code. In the following example, the rand crate is used to generate random numbers:

use rand::Rng;

fn main() {
    let x: i32 = rand::thread_rng().gen_range(1..51);
    let y: i32 = rand::thread_rng().gen_range(51..101);
    let x_float: f64 = x as f64;
    let y_float: f64 = y as f64;

    println!("{} / {} = {}", x, y, x_float / y_float);
    println!("{} % {} = {}", x, y, x_float % y_float);

    // Note: The following line uses the `rand` crate to generate random numbers.
    // Ensure you've added the `rand` crate as a dependency in your `Cargo.toml`.

    // Issue: Overflow when attempting to calculate x^y
    // Fix: Use the pow method from the f64 type since x and y are converted to f64.
    println!("{} ^ {} = {}", x, y, f64::powi(x_float, y));
}

In this code, the rand::thread_rng().gen_range() function is used to generate random integers within specified ranges. It showcases how dependencies can seamlessly integrate with your Rust project to provide additional functionality.

Troubleshooting

If you encounter an overflow issue when attempting to calculate x^y, as indicated by a panic, it may be due to a type mismatch. Ensure that the exponent (y) is of type u32 when using the i32::pow method. Alternatively, consider using the powi method from the f64 type for the calculation.


Simple Calculator in Rust

use std::io;

fn main() {
    let mut num1 = String::new();
    println!("Enter the first number: ");
    let _ = io::stdin().read_line(&mut num1);
    let num1: f64 = num1.trim().parse().expect("Enter a valid number");

    let mut num2 = String::new();
    println!("Enter the second number: ");
    let _ = io::stdin().read_line(&mut num2);
    let num2: f64 = num2.trim().parse().expect("Enter a valid number");

    // Performing arithmetic operations
    println!("{} + {} is: {}", num1, num2, num1 + num2);
    println!("{} - {} is: {}", num1, num2, num1 - num2);
    println!("{} * {} is: {}", num1, num2, num1 * num2);
    println!("{} / {} is: {}", num1, num2, num1 / num2);
    println!("{} % {} is: {}", num1, num2, num1 % num2);
}

User Input

The program prompts the user to enter two numbers, reads the input using stdin().read_line(), and converts the input into a floating-point number (f64). The trim() method is used to remove any leading or trailing whitespace, and parse() converts the string to a number. If the conversion fails, it prints an error message.

Note

Ensure that you enter valid numbers; otherwise, the program may produce unexpected results or encounter errors during parsing.


Control Flow

Comparison Operators and Truth Tables

Control flow in Rust allows you to make decisions based on conditions. Comparison operators help in evaluating conditions, and truth tables determine the logical outcomes of combinations of boolean values.

use std::io;

fn main() {
    let a: i32 = 5;
    let b: i32 = 10;
    let c: bool = true;
    let d: bool = false;

    // Comparison Operators: >, >=, <, <=, ==, !=
    println!("a > b: {}", a > b);   // false
    println!("a != b: {}", a != b); // true

    // Truth Tables
    println!("True or False: {}", c || d); // true
    println!("True and False: {}", c && d); // false
}

In this example, we explore comparison operators (>, >=, <, <=, ==, !=) to evaluate conditions. The results are then printed using println!. Additionally, truth tables for logical operations (|| for "or" and && for "and") showcase the logical outcomes based on boolean values.

Understanding these concepts is crucial for creating conditional statements and branching in Rust programs.


Conditional Statements

Conditional statements in Rust allow you to make decisions in your code based on certain conditions. Two common constructs for this purpose are the if statement and the if-else statement.

Example 1:

fn main() {
    let money: i32 = 10;
    let can_purchase_drink: bool = if money >= 10 {
        true
    } else {
        false
    };
    println!("Can Purchase a drink?: {}", can_purchase_drink);
}

In this example, the if statement checks if the variable money is greater than or equal to 10. Depending on the result, the variable can_purchase_drink is assigned either true or false, indicating whether the person can purchase a drink.

Example 2:

use std::io;

fn main() {
    println!("How much money do you have?");
    let mut input_money: String = String::new();
    io::stdin().read_line(&mut input_money);
    let money: i32 = input_money.trim().parse().expect("Entry was not an integer!");

    println!("How old are you?");
    let mut input_age: String = String::new();
    io::stdin().read_line(&mut input_age);
    let age: i32 = input_age.trim().parse().expect("Entry was not an integer!");

    if (age >= 21) && (money >= 80) {
        println!("You can have a drink!");
    } else if (age >= 21) && (money < 80) {
        println!("Come back with more money!");
    } else if (age < 21) && (money >= 80) {
        println!("Nice try kid!");
    } else {
        println!("You are too young and too poor!");
    }
}

In this more complex example, the program prompts the user for their money and age. The if-else statements evaluate both age and money conditions, providing different outputs based on the combinations. For input, the lines:

let mut input_money: String = String::new();
io::stdin().read_line(&mut input_money);
let money: i32 = input_money.trim().parse().expect("Entry was not an integer!");

are crucial. They read a line of input from the user, store it as a string, and then parse it into an integer (i32). This ensures that the input is of the expected type, handling errors gracefully with the expect method.


Match: Pattern Matching

The match keyword in Rust allows for pattern matching, providing a concise and powerful way to handle different cases. It's often used as an alternative to long sequences of if-else statements.

Example 1:

fn main() {
    let candidacy_age: i32 = 33;

    match candidacy_age {
        1..=24 => println!("Cannot hold office!"),
        25 | 26 | 27 | 28 | 29 => println!("Can run for House!"),
        30..=34 => println!("Can run for Senate!"),
        35..=i32::MAX => println!("Can run for President!"),
        _ => println!("Are you an infant!"),
    };
}

In this example, the match statement checks the value of candidacy_age against different patterns. The patterns can include ranges (1..=24), specific values (25 | 26 | 27 | 28 | 29), or even the wildcard _ for a catch-all case. It ensures that all possible values are covered.

Example 2:

use std::cmp::Ordering;

fn main() {
    let my_age: i32 = 21;
    let drinking_age: i32 = 21;

    match my_age.cmp(&drinking_age) {
        Ordering::Less => println!("Cannot drink!"),
        Ordering::Equal => println!("Woo, you can drink!"),
        Ordering::Greater => println!("You can drink!"),
    };
}

In this second example, the cmp method is used for comparing two values (my_age and drinking_age). The match statement then handles the different ordering cases (Less, Equal, and Greater). This demonstrates how match can be used not only for direct value matching but also for more complex scenarios, leveraging libraries and their associated enums.

Using match can lead to cleaner and more readable code, especially when dealing with multiple conditions or scenarios. It's a powerful tool for expressing intent and handling different cases efficiently.


Loops

In Rust, loops provide a way to repeatedly execute a block of code. There are three main types of loops: for, while, and loop.

For Loop:

  • Description:

    • The for loop is used for iterating over a range or a collection (start to finish of an iterate).
  • Example:

    fn main() {
        let mut vegetables = ["Cucumber", "Spinach", "Cabbage"];
        for veggie in vegetables.iter() {
            println!("{}", veggie);
        }
    }
  • Practical Use Case:

    • Iterating over a range of IP addresses for a ping sweeper:
      let ip_range = 1..=254;
      for ip in ip_range {
          // Ping 192.168.1.ip
          // More practical applications can include network scanning or automation.
      }

Rust Ping Sweeper

This Rust program simulates a ping sweeper by pinging a range of IP addresses (192.168.1.1 to 192.168.1.254) and reporting whether each IP address is reachable or not.

use std::process::Command;

fn main() {
    let ip_base = "192.168.1.";

    for ip in 1..=254 {
        let ip_address = format!("{}{}", ip_base, ip);
        let ping_result = Command::new("ping")
            .arg("-c")
            .arg("1")
            .arg(&ip_address)
            .status()
            .expect("Failed to execute ping command.");

        if ping_result.success() {
            println!("{} is reachable.", ip_address);
        } else {
            println!("{} is not reachable.", ip_address);
        }
    }
}

Code Breakdown

  • The program uses the Command struct from the std::process module to execute the ping command for each IP address.

  • The IP addresses are generated by iterating over the range 1 to 254 and appending them to the common base "192.168.1.".

  • The ping command is executed with the -c 1 option to send only one ICMP echo request, and the exit status is checked.

  • If the exit status indicates success, it prints that the IP address is reachable; otherwise, it prints that the IP address is not reachable.

  • The program reports the reachability status for each IP address in the specified range.

While Loop:

  • Description:

    • The while loop executes a block of code as long as the given condition is true.
  • Example:

    fn main() {
        let mut num: i32 = 1;
        while num < 10 {
            println!("{}", num);
            num += 1;
        }
    }

Loop:

  • Description:

    • The loop keyword creates an infinite loop that continues until explicitly stopped.
  • Example 1:

    fn main() {
        let mut num: i32 = 0;
        println!("Counting started!");
        loop {
            num += 1;
            println!("{}", num);
            if num == 10 {
                println!("We have reached 10!");
                break;
            }
        }
    }
  • Example 2:

    fn main() {
        let mut num: i32 = 0;
        println!("Counting started!");
        loop {
            num += 1;
            println!("{}", num);
            if num == 10 {
                println!("We have reached 10%! Continue counting.");
                continue;
            }
            if num == 100 {
                println!("We have reached 100%! Exit the program!");
                break;
            }
        }
    }
  • Additional Explanation:

    • In the second example, after reaching the count of 10, a continue statement is used. This allows for printing a status update (e.g., "We have reached 10%") and then continuing the loop. This pattern can be useful for tracking progress or providing status updates during long-running operations.

Loops are essential for creating iterative and repetitive behavior in your Rust programs. The choice of loop type depends on the specific requirements of your code, such as iterating over a collection, executing while a condition is true, or creating an infinite loop with explicit control flow.


Functions

Functions in Rust are like mini-programs, providing an organized block of code that can be called from other parts of the program. One notable feature is that it doesn't matter where functions are placed outside of the main function.

fn main() {
    add_one_hundred(100);
    add(100, 100);
    println!("{}", multiply(20, 10));

    let (added, multiplied) = add_and_multiply(4, 3);
    println!("Added: {}", added);
    println!("Multiplied: {}", multiplied);
}

In this example, various functions are called from the main function, showcasing different ways functions can be utilized.

fn add_one_hundred(num: i32) {
    println!("{}", num + 100);
}

The add_one_hundred function takes an integer num as a parameter and prints its value plus 100.

fn add(x: i32, y: i32) {
    println!("{}", x + y);
}

The add function takes two parameters, x and y, and prints their sum.

fn multiply(x: i32, y: i32) -> i32 {
    x * y // or return x * y
}

The multiply function takes two parameters, x and y, and returns their product. The -> i32 denotes that the function returns an integer.

fn add_and_multiply(x: i32, y: i32) -> (i32, i32) {
    (x + y, x * y)
}

The add_and_multiply function takes two parameters, x and y, and returns a tuple of their sum and product.

Functions in Rust can have parameters, can return values, and can be used for a variety of purposes, promoting code organization and reusability.


Simple Calculator

In this example, a simple calculator program is created, allowing the user to input two numbers and an operator (+, -, *, /). The program then performs the corresponding operation based on the chosen operator.

use std::io;

fn main() {
    println!("Num1: ");
    let mut input_num1 = String::new();
    io::stdin().read_line(&mut input_num1);
    let num1: f64 = input_num1.trim().parse().expect("Entry is not a number!");

    println!("Num2: ");
    let mut input_num2 = String::new();
    io::stdin().read_line(&mut input_num2);
    let num2: f64 = input_num2.trim().parse().expect("Entry is not a number!");

    println!("Operator (+, -, *, /): ");
    let mut input_operator = String::new();
    io::stdin().read_line(&mut input_operator);
    let operator: char = input_operator.trim().parse().expect("Entry is not an operator!");

    match operator {
        '+' => add(num1, num2),
        '-' => sub(num1, num2),
        '*' => mul(num1, num2),
        '/' => div(num1, num2),
        _ => println!("Are you fool?"),
    };
}

fn add(num1: f64, num2: f64) {
    println!("{}", num1 + num2);
}

fn sub(num1: f64, num2: f64) {
    println!("{}", num1 - num2);
}

fn mul(num1: f64, num2: f64) {
    println!("{}", num1 * num2);
}

fn div(num1: f64, num2: f64) {
    println!("{}", num1 / num2);
}

This program takes user inputs for two numbers and an operator, then uses a match statement to determine which operation to perform. Functions for addition, subtraction, multiplication, and division are defined separately, enhancing code modularity and readability.


Other Data Types

Vectors

Vectors in Rust are similar to arrays but come with added flexibility. While arrays have a fixed size, vectors are resizable dynamic arrays, making them more versatile.

  • Declaration:

    • There are multiple ways to declare a vector.

      fn main() {
          let mut vec1 = Vec::new();
          vec1.push(1);
      
          let mut vec2 = vec![1, 2, 3];
          vec2.push(4);
      
          // Storing items of an element into a variable
          let second_element: i32 = vec2[1];
          println!("The second element is {}.", second_element);
      
          println!("The length of vec1 & vec2 is {} & {}.", vec1.len(), vec2.len());
      
          for element in vec2.iter() {
              println!("Element: {}", element);
          }
      }
  • Advantages:

    • Vectors are slower than arrays but more flexible due to their resizable nature. They allow for the storage and manipulation of a collection of data, overcoming the size limitations of arrays.
  • Functions:

    • Vec::new(): Creates a new, empty vector.
    • push(): Appends an element to the end of the vector.
    • vec![]: Macro for creating a vector with initial values.
  • Accessing Elements:

    • Elements can be accessed using indexing (vec2[1]) or iteration (for element in vec2.iter()).
  • Length:

    • The length of a vector can be obtained using the len() method.

Vectors are a fundamental data structure in Rust, offering dynamic size and flexibility, making them suitable for situations where the size of the data is not known at compile time.


Structures

Structures in Rust allow the grouping of multiple variables under a single name, enabling the use of different data types within that grouping.

fn main() {
    struct Car {
        make: String,
        model: String,
        year: u32,
        price: f64,
    }

    let mut huracan = Car {
        make: String::from("Lamborghini"),
        model: String::from("Huracan"),
        year: 2022,
        price: 320000.00,
    };

    println!("The cost of a {} {} {} is ${}.", huracan.year, huracan.make, huracan.model, huracan.price);
}

In this example, a Car structure is defined, representing a car with properties like make, model, year, and price. An instance of the structure (huracan) is then created, and its values are accessed and printed.

fn main() {
    struct Rectangle {
        width: u32,
        height: u32,
    }

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

    let rect = Rectangle { width: 7, height: 77 };
    let area1 = rect.width * rect.height;
    println!("The area of a rectangle is {}", area1);

    let area2 = rect.area();
    println!("The area of a rectangle is {}", area2);
}

In this example, a Rectangle structure is defined with properties for width and height. The impl block is used to implement a method named area, which calculates the area of the rectangle. The importance of impl is demonstrated by providing a cleaner and more organized way to define methods associated with a particular structure.

The two ways of calculating the area (area1 and area2) showcase the difference between direct computation and using a method. Using methods improves code readability, maintainability, and allows for additional functionality to be associated with a particular type or structure.


Enum

Enumeration, commonly known as Enum, is a custom data type in Rust that allows you to define a set of named values. Enums are created using the enum keyword, followed by the name of the enum and the possible values it can take.

Basic Enum Example:

fn main() {
    enum Direction {
        Up,
        Down,
        Left,
        Right,
    }

    let up = Direction::Up;
}

In this example, a Direction enum is defined with four possible values: Up, Down, Left, and Right. An instance of the Direction enum (up) is created with the Up variant.

Enum with Associated Data:

Enums can also contain data associated with each variant. Here's an example:

#[derive(Debug)]
enum Shape {
    Circle(f32),
    Rectangle(f32, f32),
}

fn main() {
    let circle = Shape::Circle(10.0);
    let rectangle = Shape::Rectangle(30.0, 40.0);

    println!("{:?}", circle);
    println!("{:?}", rectangle);
}

In this example, a Shape enum is defined with two variants: Circle and Rectangle. The Circle variant contains a single f32 value representing the radius, while the Rectangle variant contains two f32 values representing width and height. Instances of the Shape enum are created for a circle and a rectangle.

Significance of #[derive(Debug)] and {:?}:

The #[derive(Debug)] annotation is used to automatically generate a debug representation for the enum. The println!("{:?}", circle); statement uses the {:?} format specifier to print the debug representation of the circle instance. This is helpful for debugging and inspecting the values stored in the enum variants.

Enum Methods:

Enums in Rust can have methods defined on them. This allows for operations specific to each variant:

// Define the Direction enum
#[derive(Debug)]
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

// Implement the opposite method for the Direction enum
impl Direction {
    fn opposite(&self) -> Direction {
        match *self {
            Direction::Up => Direction::Down,
            Direction::Down => Direction::Up,
            Direction::Left => Direction::Right,
            Direction::Right => Direction::Left,
        }
    }
}

// Main function to demonstrate the opposite method
fn main() {
    // Create a Direction enum variant
    let direction = Direction::Up;

    // Call the opposite method
    let opposite_direction = direction.opposite();

    // Print the results
    println!("Original Direction: {:?}", direction);
    println!("Opposite Direction: {:?}", opposite_direction);
}

In this example, a method opposite is defined for the Direction enum, which returns the opposite direction. This demonstrates how enums in Rust can encapsulate functionality within methods.

Enums are a powerful feature in Rust, providing a clean and expressive way to model and work with different sets of named values, whether they have associated data or not.


Generics

Generics in Rust provide a powerful way to create functions or data types without specifying the concrete types they will work with. They allow you to write more flexible and reusable code by using placeholder types until they are defined later.

Generic Functions:

use std::ops::Add;

fn main() {
    fn sum<T: Add<Output=T>>(a: T, b: T) -> T {
        a + b
    }

    let x = sum(1, 2);
    let y = sum(2.3, 3.4);

    println!("Value of x: {}", x);
    println!("Value of y: {}", y);
    println!("Value of z: {}", sum(3.3, 7.7));
}

In this example, a generic function sum is defined to add two values of the same type. The function is not restricted to specific types like i32 or f64; instead, it uses the generic type T with the trait bound Add<Output=T>. This allows the function to work with various numeric types. The function is then used with both integers and floating-point numbers, showcasing its flexibility.

Generic Structs:

fn main() {
    #[derive(Debug)]
    struct Item<T> {
        x: T,
        y: T,
    }

    let i = Item { x: 1.1, y: 2.2 };
    println!("{}, {}", i.x, i.y);
}

Here, a generic struct Item is defined with fields x and y of the same generic type T. An instance of the struct is created with x and y having values of type f64. The Debug trait is derived to enable the printing of the struct for debugging purposes.

Generics add flexibility to functions and data structures by allowing them to work with different types without specifying those types in advance. They enhance code reusability and make it possible to write more abstract and versatile code until the specific types are determined later on.


Traits

Traits in Rust are a language feature that allows the definition of a set of methods that can be implemented by any type satisfying the requirements of the traits. Essentially, traits provide a common interface for types that may have different implementations, promoting code reuse and generic programming.

Example: Damage Trait

trait Damage {
    fn damage(self: &mut Self);
}

#[derive(Debug)]
struct HP {
    hp_remaining: i32,
}

impl Damage for HP {
    fn damage(self: &mut Self) {
        self.hp_remaining -= 1;
    }
}

fn main() {
    let mut hp = HP { hp_remaining: 100 };
    hp.damage();
    println!("You took a hit! HP Remaining: {:?}", hp);
}

In this basic example, the Damage trait defines a single method damage. The HP struct implements this trait, allowing instances of HP to take damage. Traits like these are fundamental for code reuse and defining common behaviors across different types.

Example: Drawable Trait

trait Drawable {
    fn draw(&self);
}

#[derive(Debug)]
struct Circle {
    radius: f32,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }
}

fn draw_shape<T: Drawable>(shape: &T) {
    shape.draw();
}

fn main() {
    let circle = Circle { radius: 7.7 };
    circle.draw();              // Method 1 (using specific types)
    draw_shape(&circle);         // Method 2 (using generics)
}

Here, the Drawable trait defines a draw method, and the Circle struct implements this trait. The draw_shape function takes any type implementing Drawable, showcasing the power of generics and traits in creating versatile and reusable code.

Traits play a crucial role in enabling code abstraction, making it possible to write generic code that works with various types, providing a common interface for different implementations.


Memory Management

Ownership

In Rust, memory is managed through a system of ownership and borrowing. Each value in Rust has an owner responsible for managing the memory used by that value. When a value goes out of scope, its memory is automatically freed, eliminating the need for manual memory management or garbage collection, which can lead to bugs, performance issues, and security vulnerabilities.

Stack vs Heap

  • Stack:

    • Fast
    • Values are stored in order
    • All values are fixed-sized
    • Follows Last In, First Out (LIFO) order
  • Heap:

    • Slower
    • Values are unordered
    • Variable-sized values
    • Uses a return address for requested space called a pointer

When requesting space from the heap, the OS allocates free space, provides a return address (pointer), and the requested space is accessed using the pointer.

Ownership Rules

  1. Each value has an owner (owned by a variable).
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the memory becomes free.

Example

fn main() {
    let name = String::from("Wolverine");
    let new_name = name;
    println!("Hello, my name is {}.", new_name);
}

In this example, ownership of the value in name is transferred to new_name. Trying to use name after the transfer is not possible because the value has moved. Rust enforces these ownership rules to ensure memory safety.

Stack and Heap

Strings in Rust consist of a pointer, length, and capacity. The pointer on the stack points to a section in the heap. When ownership is transferred, the pointer is moved, and the original owner becomes uninitialized.

To use the original value after ownership transfer, a clone method can be used, creating a deep copy of both stack and heap data.

fn main() {
    let name = String::from("Wolverine");
    let new_name = name.clone();
    println!("Hello, my name is {}.", new_name);
    println!("Hello, my name is {}.", name);
}

In this case, a new section of the heap is allocated for the cloned value, and both heap sections remain in memory. Rust's ownership system ensures memory safety without the need for a garbage collector.


Borrowing and References

References: &

In Rust, references allow borrowing values without taking ownership. The reference symbol is &. It is used to create a reference to the value without transferring ownership.

Example 1

fn main() {
    let a = String::from("Wolverine");
    let b = &a;
    println!("My name is {}.", *b);
}

In this example, b is a reference to the value owned by a. The * symbol is used to dereference the value, allowing it to be used.

Example 2

fn main() {
    let s1 = String::from("Hello"); // Owner is s1
    let len = calculate_length(&s1);
    println!("The length of {} is {}", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len() // Return the length of the string pointed to by s
}

Here, the calculate_length function takes a reference to a String as an argument. The reference &String allows the function to access the value without taking ownership.

Immutable and Mutable Borrow

fn main() {
    let mut x = 10;
    let y = &mut x;
    println!("{}", *y);
    println!("{}", x); // (immutable borrow occurred)
    *y += 1; // This will not work (mutable borrow after immutable borrow)
}
fn main() {
    let mut x = 10;
    let y = &mut x;
    println!("{}", *y);
    *y += 1;
    println!("{}", x); // Order of operations is important
}

In these examples, we demonstrate immutable (&) and mutable (&mut) borrowing. It's important to note that you can't have an immutable borrow followed by a mutable borrow. Additionally, once you go back to the original owned value (x in this case), you can't use mutable borrow again.

Rust's borrowing system ensures that values are not modified while borrowed, providing a safe and efficient way to manage memory without compromising mutability.


File Input & Output

In Rust, handling file input and output involves using the std::fs::File module along with std::io::prelude::*. Let's explore various file operations:

Writing to a File

use std::fs::File;
use std::io::Write;

fn main() {
    let mut file = File::create("hello.txt").expect("Failed to create file.");
    file.write_all("Hello!\n".as_bytes()).expect("Failed to write to file.");
    file.write_all(b"How are you?\n").expect("Failed to write to file.");
}

In this example, we create a file named "hello.txt" and write content to it using write_all. The as_bytes() method is used to convert a string slice to a byte slice.

Appending to a File

use std::fs::OpenOptions;
use std::io::Write;

fn main() {
    let mut file = OpenOptions::new().append(true).open("hello.txt").expect("Failed to open file.");
    file.write_all(b"Hello Again!\n").expect("Failed to append to file.");
}

To append content to an existing file, we use OpenOptions with the append(true) option.

Reading from a File

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

fn main() {
    let mut file = File::open("hello.txt").expect("Failed to read the file.");
    let mut file_content = String::new();
    file.read_to_string(&mut file_content).unwrap();
    println!("{}", file_content);
}

To read from a file, we use File::open and then read_to_string to read the file's content into a String.

Deleting a File

use std::fs;

fn main() {
    fs::remove_file("hello.txt").expect("Failed to delete file.");
}

To delete a file, the remove_file function from the std::fs module is used.

Importance of std::io::prelude::*

use std::io::prelude::*; is a convenient way to bring several traits from the std::io module into scope. This allows you to use multiple traits without explicitly specifying each one. The prelude module contains commonly used traits for I/O operations, such as Read and Write.

Here are the main traits provided by std::io::prelude::*:

  • Read: The Read trait provides the read method, allowing you to read bytes from a source. It is used in conjunction with types that implement reading, such as files or network streams.

  • Write: The Write trait provides the write method, allowing you to write bytes to a destination. It is used in conjunction with types that implement writing, such as files or buffers.

By using use std::io::prelude::*;, you can avoid having to explicitly write use std::io::{Read, Write}; and make your code more concise.


Error Handling in Rust

In Rust, error handling is crucial for writing robust and reliable code. There are two types of errors: recoverable and unrecoverable.

Recoverable Errors

Recoverable errors are handled using the Result enum and the Option enum. The Result enum has variants Ok for success and Err for an error.

fn main() {
    let result = divide(10.0, 0.0);
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(msg) => println!("Error: {}", msg),
    }
    println!("The show must go on!");
}

fn divide(x: f64, y: f64) -> Result<f64, String> {
    if y == 0.0 {
        return Err(String::from("Cannot divide by zero"));
    }
    Ok(x / y)
}

Using unwrap() or expect() is a way to handle results more conveniently, but it can lead to panic in case of an error.

fn main() {
    let result = divide(10.0, 0.0).unwrap();
    println!("Result: {}", result);
    println!("The show must go on!");
}
fn main() {
    let result = divide(10.0, 0.0).expect("Divide by zero error");
    println!("Result: {}", result);
    println!("The show must go on!");
}

Panic! macro is used for unrecoverable errors.

fn main() {
    let result = divide(10.0, 0.0);
    println!("Result: {}", result);
    println!("The show must go on!");
}

fn divide(x: f64, y: f64) -> f64 {
    if y == 0.0 {
        panic!("Cannot divide by zero.");
    }
    x / y
}

? Operator

The ? operator provides a more concise way to handle Result values, reducing boilerplate code.

use std::fs::File;
use std::io::prelude::*;

fn main() {
    let result = read_file("test.txt");
    match result {
        Ok(contents) => println!("File contents:\n{}", contents),
        Err(err) => println!("Error reading file:\n{}", err),
    }
}

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = match File::open(path) {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut contents = String::new();
    match file.read_to_string(&mut contents) {
        Ok(_) => Ok(contents),
        Err(e) => Err(e),
    }
}

Using the ? operator can significantly simplify the code.

use std::fs::File;
use std::io::prelude::*;

fn main() {
    let result = read_file("test.txt");
    match result {
        Ok(contents) => println!("File contents:\n{}", contents),
        Err(err) => println!("Error reading file:\n{}", err),
    }
}

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;

    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

In the above example, the ? operator is used to propagate errors more concisely, making the code cleaner and more readable.