Post

Rust Cheatsheet

Useful Rust snippets and example code for commonly used patterns and features.

In this post we’ll look at some useful snippets and examples when programming in Rust.

  1. Create a New Project
  2. Program Execution
  3. Ownership & Borrowing
  4. Files
  5. Zero Cost Abstractions
  6. Option & Result
  7. Working with Structs
  8. Random Numbers
  9. Time


Create a New Project

Create a New Folder for Project

1
cargo new [project_name]

Create a New Project in Current Directory

1
cargo init


Program Execution

Compile & Check Program

Compile ONLY

1
cargo check

Run Program (Dev)

Compile & Run

1
cargo run

Run Program (Release)

Compile & Run

1
cargo run --release

Clean Builds

Removes all existing Dev & Release builds. target folder will be deleted. All dependencies will be recompiled upon next build.

1
cargo clean

Check Dependency Sizes

strip = true must NOT be present in the profile section of the Cargo.toml file for this to work.

1
cargo install cargo-bloat --no-default-features

Then run one of the following:

1
2
cargo bloat --crates
cargo bloat --release --crates

Profiles

See Rust docs for full documentation.

You can specify profile settings in Cargo.toml. These settings will be used during compilation.

  • cargo run/build uses dev
  • cargo run/build --release uses release
1
2
3
4
5
6
7
8
[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3
strip = true
lto = true
codegen-units = 1


Ownership & Borrowing

Ownership Rules:

  1. Each value has an owner and only one owner.
  2. When the owner goes out of scope, the value is dropped.

Borrowing Rules:

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

Ownership & Borrowing - Example 1

Take the below code, it looks simple. Create a vector, then call a function that takes in vector twice.

However, this ISN’T valid and the compiler throws an error on line 5:

1
2
use of moved value: `my_numbers`
value used here after move
1
2
3
4
5
6
7
8
9
10
fn main() {
    let my_numbers: Vec<i32> = vec![123, 456];

    print_data(my_numbers);
    print_data(my_numbers);
}

fn print_data(data: Vec<i32>) {
    println!("{:?}", data);
}

Here’s what’s happening. When print_data is called the first time, the variable my_number is being moved inside that function. The data variable inside the function is now the new owner. When the function finishes the variable data is no longer in scope. Therefore, the data associated with it (our vector) will be cleaned up. This is fine, but it means when the function is done my_numbers doesn’t exist anymore because it’s no longer the owner. When we try and use the variable again, we get an error.

To fix this we need to borrow the value, instead of taking ownership of it. Here’s how to do that:

1
2
3
4
5
6
7
8
9
10
fn main() {
    let my_numbers: Vec<i32> = vec![123, 456];

    print_data(&my_numbers);
    print_data(&my_numbers);
}

fn print_data(data: &Vec<i32>) {
    println!("{:?}", data);
}
  • Notice the function parameter has been updated to now accept a reference, and we’re now passing a reference in the function calls on line 4 and line 5.

Ownership & Borrowing - Example 2

If we want to borrow the value and also have it be mutable, then we need to use a mutable reference with &mut.

We can see below how to pass a mutable reference to a function.

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
    let mut my_numbers: Vec<i32> = vec![123, 456];

    update_data(&mut my_numbers, -100);
    update_data(&mut my_numbers, 50);
    
    println!("Data: {:?}", my_numbers);
}

fn update_data(data: &mut Vec<i32>, value_to_add: i32) {
    data.push(value_to_add);
}

Ownership & Borrowing - Example 3

We create a variable borrow, then inside a new scope block we create a vector of numbers my_numbers and try and save a reference to it in the borrow variable. Once outside the scope block we try and use borrow.

Checking the compiler we get the error:

1
2
`my_numbers` does not live long enough
borrowed value does not live long enough
1
2
3
4
5
6
7
8
fn main() {
    let borrow: &Vec<i32>;
    {
        let my_numbers: Vec<i32> = vec![123, 456];
        borrow = &my_numbers;
    }
    println!("{:?}", borrow);
}

If this code were allowed then we would have a dangling reference. When the inner scope block ends, the variable my_numbers is dropped and all references to it are invalid. This breaks the 2nd rule above for borrowing which states that references must always be valid. Therefore, the code is not valid.


Files

Importing & Exporting

File - example.rs

1
2
3
pub fn test_function() {

}

File - main.rs

1
2
3
4
5
mod example;

fn main() {
    example::test_function();
}

Reading a File to a String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::fs;

fn main() {
    let path = "file.txt";

    match fs::read_to_string(path) {
        Ok(contents) => {
            println!("File Contents: {}", contents);
        },
        Err(e) => {
            println!("Failed to read file: {}", e);
        }
    }
}

Reading a File to a Buffer

This shows how to read the raw contents of a file into a Vec<u8> (Buffer):

Short Version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::fs;

fn main() {
    let path = "file.txt";
    
    match fs::read(path) {
        Ok(contents) => {
            println!("RAW File Contents: {:?}", contents);
        },
        Err(e) => {
            println!("Failed to Read File: {}", e);
        }
    }
}

Long Version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fs::File;
use std::io::Read;

fn main() {
    let path = "file.txt";
    
    if let Ok(file_data) = read_file(&path) {
        println!("RAW File Contents: {:?}", file_data);
    } else {
        println!("Failed to Read File");
    }
}

fn read_file(file_path: &str) -> Result<Vec<u8>, std::io::Error> {
    let mut file_contents: Vec<u8> = Vec::new();
    let mut file_obj = File::open(file_path)?;

    file_obj.read_to_end(&mut file_contents)?;

    return Ok(file_contents);
}

Writing a String to a File

Short Version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fs;

fn main() {
    let file_name = "file.txt";
    let data = "some data to write to a file";
    
    match fs::write(file_name, data) {
        Ok(_) => {
            println!("File write successful");
        },
        Err(err) => {
            println!("Failed to write file: {}", err);
        }
    }
}

Long Version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::fs::File;
use std::io::{self, Write};

fn main() {
    let _ = write_file();
}

fn write_file() -> Result<bool, io::Error> {
    let file_name = "file.txt";
    let data = "some data to write to a file";

    let mut file = File::create(file_name)?;
    file.write_all(data.as_bytes())?;

    return Ok(true);
}

Writing a Buffer to a File

How to write a Vec<u8> of data to a file:

1
2
3
4
5
6
7
8
9
10
11
12
use std::fs::File;
use std::io::{self, Write};

fn write_file() -> Result<bool, io::Error> {
    let file_name = "file.txt";
    let data: Vec<u8> = vec![0,1,2,3,4,5];

    let mut file = File::create(file_name)?;
    file.write_all(&data)?;

    return Ok(true);
}
  • fs::write(file_name, data) can also be used as shorthand.

Append to File

1
2
3
4
5
6
7
8
9
10
11
12
use std::fs::OpenOptions;
use std::io::{self, Write};

fn write_file() -> Result<bool, io::Error> {
    let file_name = "file.txt";
    let data = "some data to write to a file\n";

    let mut file = OpenOptions::new().create(true).append(true).open(file_name)?;
    file.write_all(data.as_bytes())?;

    return Ok(true);
}
  • .create(true) means that the file will be created if it doesn’t already exist. If this isn’t present, and the file doesn’t exist, an Error will be thrown.

Parsing a String/File to JSON

1
2
3
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::fs;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Person {
    name: String,
    age: u32,
    countries_visited: Vec<String>,
}

fn read_data_from_json() -> Result<Person, String> {
    let json_bytes = fs::read("file.txt").unwrap_or(vec![]);
    let json_string = String::from_utf8(json_bytes).unwrap_or(String::from(""));

    return match serde_json::from_str::<Person>(&json_string) {
        Ok(person) => Ok(person),
        Err(err) => Err(err.to_string()),
    }
}   

fn main() {
    if let Ok(person) = read_data_from_json() {
        println!("{:?}", person);
    }
}
1
Person { name: "Jeff", age: 20, countries_visited: ["Australia"] }

Writing JSON/Struct to a File

1
2
3
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Note serde_json::to_string_pretty can be used instead of serde_json::to_string to format the JSON in a readable way.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
use std::fs;
use serde::Serialize;

#[derive(Debug, Serialize)]
struct Person {
    name: String,
    age: u32,
    countries_visited: Vec<String>,
}

fn write_data_to_json(person: &Person) -> Result<bool, String> {
    let json_string = serde_json::to_string(&person);

    if json_string.is_err() {
        return Err(String::from("Failed to convert to JSON string"));
    }

    if let Err(_) = fs::write("file.json", json_string.unwrap()) {
        return Err(String::from("Failed to write JSON string to File"));
    }

    return Ok(true);
}

fn main() {
    let person1 = Person {
        name: String::from("Jeff"),
        age: 20,
        countries_visited: vec![String::from("Australia")],
    };

    let _ = write_data_to_json(&person1);
}


Zero Cost Abstractions

  1. What you don’t use, you don’t pay for.
  2. What you do use, you couldn’t hand code any better.

Examples:

  • Iterators
  • Pattern Matching from Enums

Iterators

1
2
3
4
5
fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let sum: i32 = numbers.iter().sum();
    println!("Sum: {}", sum);
}

The use of .iter().sum() is as efficient as manually iterating over the vector and summing the numbers because Rust’s iterator abstractions are optimized away.

Enum Pattern Matching

You can use pattern matching for enums to decide what happens if a variable is each variant.

  • You can use _ as a handler for the else case if you don’t care about the remaining variants.
  • You MUST handle every variant. This is enforced by the compiler.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Permissions {
    Read,
    Create,
    Update,
}

fn main() {
    let value = Permissions::Update;

    match value {
        Permissions::Read => println!("READ Permission"),
        Permissions::Create => println!("CREATE Permission"),
        Permissions::Update => println!("UPDATE Permission"),
    };
}
1
UPDATE Permission

If an additional variant is added to the enum definition and you don’t update any other code, you will get a compiler error.

  • If you are using _ in the pattern match, then you WON’T get a compiler warning, as this will handle the newly added variant!
1
2
3
4
5
6
enum Permissions {
    Read,
    Create,
    Update,
    Delete,
}
  • Delete variant added.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
error[E0004]: non-exhaustive patterns: `Permissions::Delete` not covered
  --> src\main.rs:11:11
   |
11 |     match value {
   |           ^^^^^ pattern `Permissions::Delete` not covered
   |
note: `Permissions` defined here
  --> src\main.rs:1:6
   |
1  | enum Permissions {
   |      ^^^^^^^^^^^
...
5  |     Delete,
   |     ------ not covered
   = note: the matched value is of type `Permissions`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
14 ~         Permissions::Update => println!("UPDATE Permission"),
15 ~         Permissions::Delete => todo!(),


Option & Result

Option

Option Example - Pattern Matching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn my_function(param: i32) -> Option<bool> {
    if param == 0 {
        return Some(false);
    } else if param == 1 {
        return Some(true);
    }

    return None;
}

fn main() {
    for i in 0..3 {
        match my_function(i) {
            Some(result) => println!("Result: {}", result),
            None => println!("No Result!"),
        }
    }
}
1
2
3
Result: false
Result: true
No Result!

Result

Result Example - Pattern Matching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn my_function(a: i32) -> Result<i32, String> {
    if a == 0 {
        return Err(String::from("0 is not valid input!"));
    }

    return Ok(5 * a);
}

fn main() {
    for i in 0..3 {
        match my_function(i) {
            Ok(result) => println!("Result: {}", result),
            Err(err) => println!("Error: {}", err),
        };
    }
}
1
2
3
Error: 0 is not valid input!
Result: 5
Result: 10

More Option/Result Examples

Example - unwrap_or

  • Using unwrap_or gets the value out of Some variant, or if it’s the None variant the value provided will be used.
  • Using unwrap_or gets the value out of Ok variant, or if it’s the Err variant the value provided will be used.
1
2
3
4
5
6
fn main() {
    for i in 0..3 {
        let result = my_function(i).unwrap_or(false); // my_function returns Option<bool>
        println!("Result: {}", result);
    }
}
1
2
3
Result: false
Result: true
Result: false

Example - unwrap

  • Using unwrap gets the value out of the Some variant. If it’s the None variant, the program will panic.
  • Using unwrap gets the value out of the Ok variant. If it’s the Err variant, the program will panic.
1
2
3
4
5
6
fn main() {
    for i in 0..3 {
        let result = my_function(i).unwrap(); // my_function returns Option<bool>
        println!("Result: {}", result);
    }
}
1
2
3
4
Result: false
Result: true
thread 'main' panicked at src\main.rs:13:37:
called `Option::unwrap()` on a `None` value

Example - if let

  • Using if let allows you to conditionally access a specific variant.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn my_function(a: i32) -> Result<i32, String> {
    if a == 0 {
        return Err(String::from("0 is not valid input!"));
    }

    return Ok(5 * a);
}

fn main() {
    for i in 0..3 {
        if let Ok(result) = my_function(i) {
            println!("Result: {}", result);
        }
    }
}
1
2
Result: 5
Result: 10


Working with Structs

Defining a Struct

1
2
3
4
5
6
#[derive(Debug)]
struct Person {
    first_name: String,
    last_name: String,
    age: u32,
}

Struct Methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
impl Person {
	// Not required, but can be added
	pub fn new() -> Self {
		Person {
			first_name: String::from("John"),
			last_name: String::from("Smith"),
			age: 18,
		}
	}

    pub fn update_first_name(&mut self, new_first_name: &str) {
        self.first_name = String::from(new_first_name);
    }

	pub fn print_summary(&self) {
		println!("First Name: {}", self.first_name);
		println!("Last Name: {}", self.last_name);
		println!("Age: {}", self.age);
	}
}
1
2
3
let mut person = Person::new();
person.update_first_name("Jack");
person.print_summary();
1
2
3
First Name: Jack
Last Name: Smith
Age: 18

Searching a Vector of Structs

We have a Vector of Person structs, and we want to find if any person is 25 and get the struct/object is so.

1
2
3
4
5
6
7
8
9
10
let mut person_list: Vec<Person> = vec![]; // Assume this is populated

match person_list.iter().find(|person| person.age == 25) {
    Some(person) => {
        // Do something with the person that was found
    },
    None => {
        // No Person was found
    },
};

Some useful methods:

  • position returns Option<usize> for the index if a match in the Vector occured.
  • any returns bool if a match in the Vector occured.
  • find returns Option<Self::Item>, in this case Option<Person> for struct if a match in the Vector occured.

Sorting a Vector of Structs

We have a Vector of Structs and we want to sort them in a specific way according to our own function. Example, sort Vec<Person> by their age:

1
2
3
4
5
6
7
8
9
10
11
12
use std::cmp::Ordering;

person_list.sort_by(|a, b| {
    // Sort by "age" field, Small -> Big
    if a.age > b.age {
        return Ordering::Greater;
    } else if a.age == b.age {
        return Ordering::Equal;
    } else {
        return Ordering::Less;
    }
});

A similar result can be achieved with sort_unstable_by_key. However, it’s not as flexible.

1
2
person_list.sort_unstable_by_key(|person| person.age); // Small -> Big
person_list.sort_unstable_by_key(|person| u32::MAX - person.age); // Big -> Small
  • sort_by_key and sort_by_cached_key are also available depending on the your use case.

Mutation Issues

If we modify the above example to use find instead of position, then inside of the Some(person) pattern we try and use our function update_first_name, it will fail.

1
person.update_first_name("Steve");
1
`person` is a `&` reference, so the data it refers to cannot be borrowed as mutable

To fix this we need to use a mutable iterator:

1
person_list.iter_mut()


Random Numbers

1
cargo add rand

or

1
2
[dependencies]
rand = "0.8"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();

    let small_number = rng.gen::<u8>();
    let big_number = rng.gen::<u128>();

    let dice_roll_1: u32 = rng.gen_range(1..7); // 1 to 6. 7 is NOT included
    let dice_roll_2: u32 = rng.gen_range(1..=6); // 1 to 6. 6 is included

    println!("Small (u8): {}", small_number);
    println!("Big (u128): {}", big_number);
    println!("Dice 1: {}", dice_roll_1);
    println!("Dice 2: {}", dice_roll_2);
}
1
2
3
4
Small (u8): 191
Big (u128): 338387472999988388919276434146473488855
Dice 1: 1
Dice 2: 6

If you want to pass the rng to a function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use rand::Rng;
use rand::rngs::ThreadRng;

fn main() {
    let mut rng = rand::thread_rng();

    let my_random_number = get_random_number(&mut rng);

    println!("Number: {}", my_random_number);
}

fn get_random_number(rng: &mut ThreadRng) -> u32 {
    return rng.gen();
}
1
Number: 780483661


Time

Timestamps

How to get the current time as a timestemp.

1
2
3
4
5
6
7
8
9
10
use std::time::{SystemTime, UNIX_EPOCH};

fn main() {
    let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();

    println!("Timestamp (ns): {}", timestamp.as_nanos());
    println!("Timestamp (μs): {}", timestamp.as_micros());
    println!("Timestamp (ms): {}", timestamp.as_millis());
    println!("Timestamp (s): {}", timestamp.as_secs());
}
1
2
3
4
Timestamp (ns): 1727954879182449500
Timestamp (μs): 1727954879182449
Timestamp (ms): 1727954879182
Timestamp (s): 1727954879

Timers

You can use the below code when you want benchmark how long a certain piece of code is taking to execute. For more accurate results this does need to be done a lot of times.

  • By default the results will be formatted nicely depending on the time taken (microseconds, milliseconds, seconds).
1
2
3
4
5
6
7
8
// Create the timer
let timer: std::time::Instant = std::time::Instant::now();

// Stop the timer
let time_taken = timer.elapsed();

// Print result
println!("\nTime Taken: {:?}", time_taken);
This post is licensed under CC BY 4.0 by the author.