Rust Basics

Traits
  • Definition: Traits are a way to define shared behavior in Rust. They define a set of methods that a type must implement in order to fulfill the contract of the trait.
  • Purpose: Traits are used for abstracting over types, allowing you to write generic code that can work with multiple types as long as they implement the required behavior specified by the trait.
  • Syntax: Traits are declared using the trait keyword, followed by the trait’s name and a set of method signatures. Types can then implement traits using the impl keyword.
trait Drawable {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }
}
Closures
  • Definition: Closures are anonymous functions that can capture variables from their surrounding environment. They are similar to functions but are defined inline using a concise syntax.
  • Purpose: Closures are useful for writing code that requires a small, one-off function without the need to define a separate named function. They are often used for passing behavior as arguments to higher-order functions.
  • Syntax: Closures are written using a pipe (|) syntax to specify the parameters, followed by the body of the closure. They can capture variables from their enclosing scope using a move or borrow semantics.
let add = |x, y| x + y;
println!("The sum is: {}", add(3, 5));
where clause

In Rust, the where clause is used in function declarations, trait declarations, and various other contexts to specify additional requirements or bounds on the generic parameters or associated types.

When used in a function declaration, the where clause typically follows the list of generic parameters and is used to specify trait bounds or additional constraints on those parameters.

fn foo<T>(x: T) -> usize
where
    T: std::fmt::Debug,
{
    println!("{:?}", x); // We can use Debug trait here because of the where clause
    0
}
  • T is a generic parameter of the function foo.
  • The where clause specifies that T must implement the Debug trait, allowing us to use the println! macro inside the function body.

Here are some common uses of the where clause in function declarations:

  1. Trait Bounds: Specify traits that the generic type parameter must implement.
  2. Associated Type Bounds: Specify constraints on associated types within trait bounds.
  3. Multiple Bounds: Specify multiple trait bounds or bounds on multiple generic parameters.
  4. Complex Constraints: Specify more complex constraints that involve multiple types or traits.
“map” with Range

The map method in Rust is used to create a new iterator where each element is the result of applying a given function to the corresponding element of the original iterator. When used with a range, it allows you to transform each number in the range according to the function you provide.

fn main() {
    let squares: Vec<i32> = (1..5).map(|x| x * x).collect();
    println!("{:?}", squares); // This will print [1, 4, 9, 16]
}
  1. Range Creation:let range = 1..5;
  2. Mapping Function:let squares = range.map(|x| x * x);
    • The map method takes a closure |x| x * x which squares each element x in the range.
  3. Collecting Results:let squares: Vec<i32> = squares.collect();
    • The collect method is used to consume the iterator and collect the results into a vector.
  4. This creates a range from 1 to 4.
core::array::from_fn

The core::array::from_fn function in Rust is a utility that allows you to generate an array of fixed size by applying a closure to each index.

use core::array;

fn main() {
    // Create an array of 10 elements where each element is its index squared
    let squares: [usize; 10] = array::from_fn(|i| i * i);
    println!("{:?}", squares); // Prints: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
}
Option<T>

Option<T> is an enum in Rust that represents an optional value. It can be either:

  • Some(T): Indicates that there is a value of type T.
  • None: Indicates that there is no value.

The Option type is used extensively in Rust to handle cases where a value might be absent

if let

In Rust, the if let construct is used to match a pattern and execute code if the match is successful. It can be used to handle Option and Result types more ergonomically. Specifically, if let Some(next) = inputs_iter.peek() checks if peek() returns Some and extracts the value into next. If peek() returns None, the else block is executed.

fn main() {
    // Define a vector of vectors
    let inputs = vec![
        vec![1, 2, 3],
        vec![4, 5, 6],
        vec![7, 8, 9],
    ];

    // Convert the vector into an iterator and make it peekable
    let mut inputs_iter = inputs.into_iter().peekable();

    // Loop through the iterator
    while let Some(current) = inputs_iter.next() {
        // Print the current element
        println!("Current: {:?}", current);

        // Peek at the next element
        if let Some(next) = inputs_iter.peek() {
            // If there is a next element, print it
            println!("Next: {:?}", next);
        } else {
            // If there are no more elements, print a message
            println!("No more elements.");
        }
    }
}
Ownership & borrowing

In Rust, “ownership” refers to the ownership system that Rust uses to manage memory safely without a garbage collector. Ownership is a set of rules that governs how memory is managed, ensuring that memory is freed when it is no longer needed.

When we say that a function or method takes ownership of data, it means that the function or method takes control over the data, and the original owner can no longer access the data. Conversely, when we say that a function or method borrows data, it means that the function or method can temporarily use the data, but the original owner retains control and can continue to use it once the borrowing ends.

ownership:

fn take_ownership(v: Vec<i32>) {
    // `v` is now owned by this function
    println!("{:?}", v);
}

fn main() {
    let v = vec![1, 2, 3];
    take_ownership(v);
    // v is no longer accessible here because its ownership has been transferred
    // println!("{:?}", v); // This would cause a compile error
}

borrowing

fn borrow(v: &Vec<i32>) {
    // `v` is borrowed here, not owned
    println!("{:?}", v);
}

fn main() {
    let v = vec![1, 2, 3];
    borrow(&v);
    // v is still accessible here because its ownership was not transferred
    println!("{:?}", v); // This is fine
}
Immutable borrowing

When you borrow a value immutably, you use &. This means that the function can read the value but cannot modify it

mutable borrowing

When you borrow a value mutably, you use &mut. This allows the function to modify the value

Leave a Reply

Your email address will not be published. Required fields are marked *