DEV Community

Cover image for Rust cheatsheet examples part_2
Stanislav Kniazev
Stanislav Kniazev

Posted on

Rust cheatsheet examples part_2

Let's continue with more complex Rust concepts and structures.

References, Ownership, and Borrowing

The stack is used for static memory allocation, while the heap is used for dynamic memory allocation, but both are stored in RAM, allowing for fast access by the CPU.
The stack is suitable only for data whose size is predetermined and constant. Conversely, data with variable or unknown sizes at compile time must be stored on the heap.

Ownership rules

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

Borrowing rules

  1. At any given time, you can have either one mutable reference or any number of immutable references.
  2. References must always be valid.

Ownership and functions

fn main() {
    let num = 5;
    copy_value(num); // num is copied by value

    let text = String::from("Hello Rust!");
    take_ownership(text); // text is moved into the function

    let new_text = give_ownership(); // return value is moved into new_text

    let some_text = String::from("Rust");
    let final_text = take_and_give_back(some_text); // some_text is moved and then returned to final_text
}
Enter fullscreen mode Exit fullscreen mode

Creating references

let text1 = String::from("hello world!");
let ref1 = &text1; // immutable reference

let mut text2 = String::from("hello");
let ref2 = &mut text2; // mutable reference
ref2.push_str(" world!");
Enter fullscreen mode Exit fullscreen mode

Iterators

 Iterators are lazy, meaning they have no effect until you call methods that consume the iterator to use it up.

let v1 = vec![1, 2, 3]; 
let mut v1_iter = v1.iter(); 

assert_eq!(v1_iter.next(), Some(&1)); 
assert_eq!(v1_iter.next(), Some(&2)); 
assert_eq!(v1_iter.next(), Some(&3)); 
assert_eq!(v1_iter.next(), None);
Enter fullscreen mode Exit fullscreen mode

Note that v1_iter is mutable: calling the next method on an iterator changes internal state that the iterator uses to keep track of where it is in the sequence. In other words, this code consumes the iterator. Each call to next eats up an item from the iterator.

The values we get from the calls to next are immutable references to the values in the vector. The iter method produces an iterator over immutable references. If we want to create an iterator that takes ownership of v1 and returns owned values, we can call into_iter instead of iter. Similarly, if we want to iterate over mutable references, we can call iter_mut instead of iter.

All iterators implement a trait named Iterator that is defined in the standard library. The definition of the trait looks like this:

// The `Iterator` trait only requires a method to be defined for the `next` element.
impl Iterator for Fibonacci {
    // `Item` type is used in the return type of the `next` method
    type Item = u32;

    // Here, we define the sequence using `.curr` and `.next`.
    // The return type is `Option<T>`:
    //     * When the `Iterator` is finished, `None` is returned.
    //     * Otherwise, the next value is wrapped in `Some` and returned.
    // We use Self::Item in the return type, so we can change
    // the type without having to update the function signatures.
    fn next(&mut self) -> Option<Self::Item> {
        let current = self.curr;

        self.curr = self.next;
        self.next = current + self.next;

        // Since there's no endpoint to a Fibonacci sequence, the `Iterator` 
        // will never return `None`, and `Some` is always returned.
        Some(current)
    }
}

// Returns a Fibonacci sequence generator
fn fibonacci() -> Fibonacci {
    Fibonacci { curr: 0, next: 1 }
}
Enter fullscreen mode Exit fullscreen mode

Copy, Move, and Clone

Simple types that implement the Copy trait are copied by value, meaning both the original and the copy can be used independently. Simple types often stored in the stack memory due to their fixed size and simplicity.

  1. All the integer types, such as i8, i16, i32, i64, i128, u8, u16, u32, u64, u128.
  2. The floating point types: f32 and f64.
  3. The Boolean type: bool, which can be either true or false.
  4. The character type: char, representing a single Unicode scalar value.
  5. Tuples, but only if they contain types that are also Copy. For example, (i32, f64) is Copy, but (i32, String) is not because String does not implement Copy.
  6. Arrays with a fixed size that contain types that are Copy. For example, [i32; 5] is Copy, but [String; 5] is not because String does not implement Copy.
  7. Pointers, such as raw pointers (*const T, *mut T) and function pointers (fn), but not references (&T, &mut T) or smart pointers like Box<T>. It's important to note that if a type implements the Drop trait, it cannot implement the Copy trait. This is because the Drop trait requires custom logic to be executed when a value goes out of scope, which would conflict with the unrestricted bitwise copying behavior of Copy types.
let num = 5; 
let copy_of_num = num; // `num` implements Copy, so it's copied, not moved
Enter fullscreen mode Exit fullscreen mode

For types that do not implement Copy, such as String, assignment moves the value. After moving, the original variable cannot be used.

let text = String::from("Hello");
let moved_text = text; // `text` is moved to `moved_text`
// println!("{}", text); // This would cause an error because `text` is no longer valid
Enter fullscreen mode Exit fullscreen mode

To explicitly clone the data, creating a separate instance that does not affect the original, use the clone method.

let text = String::from("Hello");
let cloned_text = text.clone(); // `text` is cloned, both `text` and `cloned_text` are valid
Enter fullscreen mode Exit fullscreen mode

Error Handling

Throw unrecoverable error

panic!("Critical error! Exiting!");
Enter fullscreen mode Exit fullscreen mode

Option enum

fn find_user_id(username: &str) -> Option<u32> {
    if user_db.exists(username) {
        return Some(user_db.get_id(username))
    }
    None
}
Enter fullscreen mode Exit fullscreen mode

Result enum

fn fetch_user(user_id: u32) -> Result<User, Error> {
    if is_user_logged_in(user_id) {
        return Ok(get_user_details(user_id))
    }
    Err(Error { message: "User not logged in" })
}
Enter fullscreen mode Exit fullscreen mode

? operator

The question mark operator (?) unwraps valid values or returns errornous values, propagating them to the calling function. It is a unary postfix operator that can only be applied to the types Result<T, E> and Option<T>.

fn calculate_salary(database: Database, user_id: i32) -> Option<u32> {
    Some(database.get_employee(user_id)?.get_position()?.salary)
}

fn establish_connection(database: Database) -> Result<Connection, Error> {
    let connection = database.find_instance()?.connect()?;
    Ok(connection)
}
Enter fullscreen mode Exit fullscreen mode

Threads

Rust provides a mechanism for spawning native OS threads via the spawn function, the argument of this function is a moving closure. Rust ensures safety and easy management of threads, adhering to its principles of memory safety and concurrency.

Creating a Thread

You can create a new thread by calling std::thread::spawn and passing a closure containing the code you wish to run in the new thread.

use std::thread;

const THREADS_COUNT: u32 = 10;
let mut items = vec![];

for i in 0..THREADS_COUNT {
    // Spin up another thread
    item.push(thread::spawn(move || {
        println!("this is thread number {}", i);
    }));
}

for i in item {
    // Wait for the thread to finish. Returns a result.
    let _ = i.join();
}
Enter fullscreen mode Exit fullscreen mode

Sharing Data Between Threads

Rust's ownership rules extend to threads, ensuring safe data sharing. To share data between threads, you can use atomic types, mutexes, or channels.

Using Mutex for Safe Data Access

Mutex means mutual exclusion, which means "only one at a time". Mutex is safe, because it only lets one process change data at a time. To do this, it uses .lock()

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

struct JobStatus {
    jobs_completed: u32,
}

fn main() {
    // using an Arc to share memory among threads, and the data inside
    // the Arc is protected with a mutex.
    let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 }));
    let mut handles = vec![];
    for _ in 0..10 {
        let status_shared = Arc::clone(&status);
        let handle = thread::spawn(move || {
            thread::sleep(Duration::from_millis(250));
            let mut jobs_count = status_shared.lock().unwrap();
            jobs_count.jobs_completed += 1;
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
        println!("jobs completed {}", status.lock().unwrap().jobs_completed);
    }
}
// Output:
jobs completed 5  
jobs completed 5  
jobs completed 5  
jobs completed 7  
jobs completed 7  
jobs completed 7  
jobs completed 7  
jobs completed 9  
jobs completed 9  
jobs completed 10
Enter fullscreen mode Exit fullscreen mode

Using Channels for Communication

Rust provides asynchronous channels for communication between threads. Channels allow a unidirectional flow of information between two end-points: the Sender and the Receiver.

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

let (tx, rx) = mpsc::channel();

for i in 0..10 {
    let tx = tx.clone();
    thread::spawn(move || {
        let message = format!("Message {}", i);
        tx.send(message).unwrap();
    });
}

for _ in 0..10 {
    let received = rx.recv().unwrap();
    println!("Received: {}", received);
}
Enter fullscreen mode Exit fullscreen mode

Combinators

#map

let some_name = Some("Rusty".to_owned());
let name_length = some_name.map(|name| name.len());

let name_result: Result<String, Error> = Ok("Alex".to_owned());
let person_result: Result<Person, Error> = name_result.map(|name| Person { name });
Enter fullscreen mode Exit fullscreen mode

#and_then

let numbers = Some(vec![1, 2, 3]);
let first_num = numbers.and_then(|nums| nums.into_iter().next());

let parse_result: Result<&'static str, _> = Ok("10");
let int_result = parse_result.and_then(|num_str| num_str.parse::<i32>());
Enter fullscreen mode Exit fullscreen mode

Generics, Traits, and Lifetimes

Generics used to create definitions for items like function signatures or structs, which we can then use with many different concrete data types.

struct Pair<T, V> {
    first: T,
    second: V,
}

impl<T, V> Pair<T, V> {
    fn mix<V2, T2>(self, other: Pair<V2, T2>) -> Pair<T, T2> {
        Pair {
            first: self.first,
            second: other.second,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Traits

trait defines functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic type can be any type that has certain behavior (implements certain Trait)

trait Voice {
    fn create(name: &'static str) -> Self; // required implementation 
    fn speak(&self) -> &'static str { "Oho" } // default implementation
}

struct Cat { name: &'static str }

impl Cat {
    fn meow() { // ... }
}

impl Voice for Cat {
    fn create(name: &'static str) -> Cat {
        Cat { name }
    }

    fn speak(&self) -> &'static str {
        "meow"
    }
}
Enter fullscreen mode Exit fullscreen mode

Trait bounds

The impl Trait syntax works for straightforward cases but is actually syntax sugar for a longer form known as a trait bound; it looks like this:

pub fn post<T: Summary>(item: &T) {
    println!("Musk broke his ketamine dose again {}", item.summarize());
}
Enter fullscreen mode Exit fullscreen mode

functions with multiple generic type parameters can contain lots of trait bound information between the function’s name and its parameter list

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {...}
// Alternative using "where" keyword:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
Enter fullscreen mode Exit fullscreen mode

impl trait

We can also use the impl Trait syntax in the return position to return a value of some type that implements a trait, as shown here:

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}
Enter fullscreen mode Exit fullscreen mode

By using impl Summary for the return type, we specify that the returns_summarizable function returns some type that implements the Summary trait without naming the concrete type. In this case, returns_summarizable returns a Tweet, but the code calling this function doesn’t need to know that.

The ability to specify a return type only by the trait it implements is especially useful in the context of closures and iterators

Trait objects

Dynamic dispatch through a mechanism called ‘trait objects’ - way to achieve polymorphism in Rust.
A trait object can be obtained from a pointer to a concrete type that implements the trait by casting it (e.g. &x as &Foo) or coercing it (e.g. using &x as an argument to a function that takes &Foo).

These trait object coercions and casts also work for pointers like &mut T to &mut Foo and Box<T> to Box<Foo>

pub trait Draw { 
  fn draw(&self); 
}

pub struct Screen {
  pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

struct Button {
    width: u32,
    height: u32,
    label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // code to actually draw a button
    }
}
// --------
use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}
// -------

use gui::{Button, Screen};

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No"),
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}
Enter fullscreen mode Exit fullscreen mode

Supertraits

When you define a trait, you can specify that it requires the functionality of another trait using the supertrait syntax. This means that in order for a type to implement the dependent trait, it must also implement the supertrait(s).

use std::fmt;
// `Log` is a trait that has a supertrait `fmt::Display` from the Rust standard library. The `fmt::Display` trait requires the implementing type to define how it will be formatted when displayed as a string

trait Log: fmt::Display { 
  fn log(&self) {
    let output = self.to_string();

    println!("Logging: {}", output);
  }
}
Enter fullscreen mode Exit fullscreen mode

Operator overloading

Rust allows for a limited form of operator overloading. There are certain operators that are able to be overloaded. To support a particular operator between types, there’s a specific trait that you can implement, which then overloads the operator.

use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Vector {
    x: i32,
    y: i32,
}

impl Add for Vector {
    type Output = Vector;

    fn add(self, other: Vector) -> Vector {
        Vector {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    let v1 = Vector { x: 1, y: 0 };
    let v2 = Vector { x: 2, y: 3 };

    let v3 = v1 + v2;

    println!("{:?}", v3);
}
Enter fullscreen mode Exit fullscreen mode

Lifetimes

Lifetimes ensure that references are valid as long as we need them to be, main goal of lifetimes is to prevent dangling references.
Most of the time, lifetimes are implicit and inferred, just like most of the time, types are inferred. We must only annotate types when multiple types are possible. In a similar way, we must annotate lifetimes when the lifetimes of references could be related in a few different ways. Rust requires us to annotate the relationships using generic lifetime parameters to ensure the actual references used at runtime will definitely be valid

Lifetimes in function signatures

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { 
    x
  } else { 
    y
  } 
}
Enter fullscreen mode Exit fullscreen mode

Lifetimes in Struct Definitions

This example demonstrates how to associate the lifetime of the fields within the Profile struct with the lifetime 'a, ensuring that the references to name and email are valid for the duration of the Profile instance's existence.

struct Profile<'a> { 
  email: &'a str,
  name: &'a str,
}

let user_name = "Alex"; 
let user_email = "alex@example.com"; 
let user_profile = Profile { name: user_name, email: user_email };
Enter fullscreen mode Exit fullscreen mode

Static Lifetimes

A 'static lifetime is the longest possible lifetime, and it can live for the entire duration of a program. It's typically used for string literals.

let static_str: &'static str = "Permanent text";
Enter fullscreen mode Exit fullscreen mode

static_str is a reference to a string literal with a 'static lifetime, meaning it's guaranteed to be valid for the entire program runtime.

Functions, Function Pointers, and Closures

Functions in Rust can be defined as associated functions of structs or as standalone functions. Function pointers and closures provide flexibility for dynamic function usage and functional programming paradigms.

Associated functions and methods

Structs can have associated functions and methods that act on instances of the struct.

struct Point { x: i32, y: i32 } 

impl Point { 
  fn create(x: i32, y: i32) -> Point { 
    Point { x, y } 
  } 

  fn get_x(&self) -> i32 { self.x }
}
let point = Point::create(5, 10); 
point.get_x();
Enter fullscreen mode Exit fullscreen mode

Function Pointers

Function pointers allow passing functions as arguments to other functions.

fn compute(val: i32, operation: fn(i32) -> i32) -> i32 {
    operation(val)
}

fn square(num: i32) -> i32 {
    num * num
}

let squared_value = compute(5, square);
Enter fullscreen mode Exit fullscreen mode

Creating Closures

Closures are anonymous functions that can capture their environment.

let increment = |num: i32| -> i32 { num + 1 };
let incremented_value = increment(5);
Enter fullscreen mode Exit fullscreen mode

Returning Closures

Closures can be returned from functions. The impl keyword is used to return a closure.

fn add(amount: i32) -> impl Fn(i32) -> i32 {
    move |num: i32| num + amount
}

let add_five = add(5);
let result = add_five(10); // result == 15

fn add_or_subtract(a: i32) -> Box<dyn Fn(i32) -> i32> {
  if a > 10 {  
    Box::new(move |b| b + a)
  } else {  
    Box::new(move |b| b - a)
  } 
}
let var1 = add_or_subtract(10); // -10
let var2 = var1(2); // -10 +2 = 8
Enter fullscreen mode Exit fullscreen mode

Closure Traits

  • FnOnce: consumes the variables it captures from enclosing scope.
  • FnMut: mutably borrows values from its enclosing scope.
  • Fn: immutably borrows values from its enclosing scope.
fn apply<F>(value: i32, mut func: F) -> i32
where
    F: FnMut(i32) -> i32,
{
    func(value)
}

let double = |x: i32| x * 2;
let doubled_value = apply(5, double);
Enter fullscreen mode Exit fullscreen mode

Store Closure in Struct

You can store closures in structs by specifying the closure trait in the struct definition.

struct Calculator<T>
where
    T: Fn(i32) -> i32,
{
    calculation: T,
}

let add_one = |num: i32| num + 1;
let calculator = Calculator { calculation: add_one };
let calculated_value = (calculator.calculation)(5);
Enter fullscreen mode Exit fullscreen mode

Function that Accepts Closure or Function Pointer

Functions can accept closures or function pointers as parameters, allowing for flexible argument types.

fn execute_twice<F>(mut func: F, arg: i32) -> i32
where
    F: FnMut(i32) -> i32,
{
    func(arg) + func(arg)
}

let add_two = |x: i32| x + 2;
let executed_value = execute_twice(add_two, 10);
Enter fullscreen mode Exit fullscreen mode

Pointers

References
let value = 10; 
let reference_to_value = &value; // Immutable reference 
let mutable_reference_to_value = &mut value; // Mutable reference
Enter fullscreen mode Exit fullscreen mode

Raw Pointers

Rust has a number of different smart pointer types in its standard library, but there are two types that are extra-special. Much of Rust’s safety comes from compile-time checks, but raw pointers don’t have such guarantees, and are unsafe to use.
*const T and *mut T are called ‘raw pointers’ in Rust. Sometimes, when writing certain kinds of libraries, you’ll need to get around Rust’s safety guarantees for some reason. In this case, you can use raw pointers to implement your library, while exposing a safe interface for your users. For example, * pointers are allowed to alias, allowing them to be used to write shared-ownership types, and even thread-safe shared memory types (the Rc<T> and Arc<T> types are both implemented entirely in Rust).

Here are some things to remember about raw pointers that are different than other pointer types. They:

  • are not guaranteed to point to valid memory and are not even guaranteed to be non-NULL (unlike both Box and &);
  • do not have any automatic clean-up, unlike Box, and so require manual resource management;
  • are plain-old-data, that is, they don't move ownership, again unlike Box, hence the Rust compiler cannot protect against bugs like use-after-free;
  • lack any form of lifetimes, unlike &, and so the compiler cannot reason about dangling pointers; and
  • have no guarantees about aliasing or mutability other than mutation not being allowed directly through a *const T.

Raw pointers are useful for FFI: Rust’s *const T and *mut T are similar to C’s const T* and T*, respectively. Raw pointers (*const T and *mut T) can point to any memory location.

#![allow(unused_variables)]
fn main() {
    let x = 5;
    let raw = &x as *const i32;

    let mut y = 10;
    let raw_mut = &mut y as *mut i32;
}

// When you dereference a raw pointer, you’re taking responsibility that it’s not pointing somewhere that would be incorrect. As such, you need `unsafe`:

#![allow(unused_variables)]
fn main() {
    let x = 5;
    let raw = &x as *const i32;

    let points_at = unsafe { *raw };

    println!("raw points at {}", points_at);
}
Enter fullscreen mode Exit fullscreen mode

Smart Pointers

  • Smart pointers are data structures that not only act like a pointer but also have additional metadata and capabilities.
  • Smart pointers own the data they point to and are usually implemented using structs..
  • Smart pointers implement the Deref and Drop traits. The Deref trait allows an instance of the smart pointer struct to behave like a reference so you can write your code to work with either references or smart pointers. The Drop trait allows you to customize the code that’s run when an instance of the smart pointer goes out of scope.

Box<T>

Use Box<T> to allocate values on the heap.

let boxed_data = Box::new(5);
Enter fullscreen mode Exit fullscreen mode

Rc<T> - multiple ownership with reference counting

let shared_data = Rc::new(5); 
let clone_of_shared_data = Rc::clone(&shared_data);
Enter fullscreen mode Exit fullscreen mode

Ref<T>, RefMut<T>, and RefCell<T> - enforce borrowing rules at runtime instead of compile time.

let a = 5;  
let x1 = RefCell::new(a);  
let x2 = x1.borrow();  // Ref - immutable borrow  
let x3 = x1.borrow_mut();  // RefMut - mutable borrow  
let x4 = r1.borrow_mut();  // RefMut - second mutable borrow 
Enter fullscreen mode Exit fullscreen mode
Multiple owners of mutable data
let x = Rc::new(RefCell::new(5));
Enter fullscreen mode Exit fullscreen mode

Packages, Crates, and Modules

Rust's module system allows you to organize your code into packages, crates, and modules for better readability and reusability.

  • Packages is cargo feature that lets you build, test, and share crates.
  • Crates - A tree of modules that produces a library or executable.
  • Modules and use - Let you control the organization, scope, and privacy of paths.
  • Paths - A way of naming an item, such as a struct, function, or module.
Creating a Package
$ cargo new my_project --bin # For a binary 
$ crate cargo new my_library --lib # For a library crate
Enter fullscreen mode Exit fullscreen mode

Defining and using modules

mod sausage_factory {
    pub use self::meats::PORK_SAUSAGE as sausage;
    pub use self::meats::VEGGIE_SAUSAGE as veggie_sausage;

    mod meats {
        pub const PORK_SAUSAGE: &'static str = "Juicy pork sausage";

        pub fn make_sausage() {
          get_secret_recipe();
            println!("sausage made with {} and secret ingredient!", PORK_SAUSAGE);
          }
        }

        // private function so nobody outside of this module can use it
        fn get_secret_recipe() -> String {
          String::from("Ginger")
        }
    }

    mod veggies {
        pub const VEGGIE_SAUSAGE: &'static str = "Delicious veggie sausage";
        ...
    }
}

fn main() {
  sausage_factory::meats::make_sausage();
  println!("Today we serve {} and {}", sausage_factory::sausage, sausage_factory::veggie_sausage)
}

Enter fullscreen mode Exit fullscreen mode
Import module and use with custom name:
// Import module and use with custom name:
use std::fmt::Result;  
use std::io::Result as IoResult;

// Re-exporting with pub use
mod outer_module {  
    pub mod inner_module {
        pub fn inner_public_function() {} 
    }
}  
pub use crate::outer_module::inner_module;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)