In this post we’ll look at some useful snippets and examples when programming in Rust.
- Create a New Project
- Program Execution
- Ownership & Borrowing
- Files
- Zero Cost Abstractions
- Option & Result
- Working with Structs
- Random Numbers
- Time
Create a New Project
Create a New Folder for Project
1
| cargo new [project_name]
|
Create a New Project in Current Directory
Program Execution
Compile & Check Program
Compile ONLY
Run Program (Dev)
Compile & Run
Run Program (Release)
Compile & Run
Clean Builds
Removes all existing Dev & Release builds. target
folder will be deleted. All dependencies will be recompiled upon next build.
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:
- Each value has an owner and only one owner.
- When the owner goes out of scope, the value is dropped.
Borrowing Rules:
- At any given time, you can have either one mutable reference or any number of mutable references.
- 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
- What you don’t use, you don’t pay for.
- 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"),
};
}
|
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,
}
|
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);
}
}
}
|
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:
Random Numbers
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();
}
|
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);
|