Home Rust Cheat Sheet
Post
Cancel

Rust Cheat Sheet

An overview of essential features and concepts in Rust. It delves into unique aspects of Rust like lifetime management and error handling, showcases how generics and traits enhance code reusability and modularity, and sheds light on code organization in Rust projects. The guide also covers functional programming components like closures and iterators. Moreover, it outlines the robust testing framework of Rust, and guides on publishing Rust packages and installing binary crates using Cargo, Rust’s built-in package manager. This invaluable resource serves as both a handy reference and a refresher for anyone, whether they’re a beginner or an experienced Rustacean.

Good to know

  • Cargo is Rust’s build system and package manager.
    • https://crates.io/ is where you can find the dependencies.

Naming Convention

1
2
let variable_name = ...
const CONST_NAME = ...
  • Constants can be defined in global scope, but variables cannot

Beautiful

  • Compiler errors are very clear and actually helpful
    • You can use rustc --explain Exxxx, to explain the error by error code
      1
      2
      3
      4
      
      27 |     let number = if true { 5 } else { "six" };
       |                            -          ^^^^^ expected integer, found `&str`
       |                            |
       |                            expected because of this
      

Surprising

Variable shadowing

1
2
3
let x: String = ...
let x: Int = ... // this is valid. We can change a mutable variable type when we reassign it
x = "..." // Will not compile without `let`

String literals need to be converted to Strings.

1
"".to_string()

Annoying

  • snake_case_variable_names. I need to jump to a character _ in the number line for every space in a variable name.
  • Integer Overflow without exceptions, there are few methods (literally) to handle cases of overflow explicitly

Interesting

  • A rust developer is a Rustacean
  • The following two signatures are not equivalent, but the first will not accept string literals
    1
    2
    
    fn first_word(s: &String) -> &str { ... // does not accept string literals
    fn first_word(s: &str) -> &str { ... // accept either a string or a string literal
    
  • Documentation code examples run as tests

Conventions

Constructor

  • new: Constructor shouldn’t fail.
  • build: Constructor might fail.

Getting Started

Installation and Tooling

  • Official Installation Instructions

    Your first project

    1
    2
    3
    4
    5
    6
    7
    8
    
    cargo new project_name --vcs=git 
    cargo new library_name --lib
    cargo check # Compiles but doesn't build an artifact
    cargo build # Builds a debug artifact at: /target/debug
    cargo run   # Builds a debug artifact and runs it 
    cargo build --release # Builds a release artifact at: /target/release
    cargo add dependency@version
    cargo doc --open # Generate and open HTML documentation.
    
  • By convention
    • src/main.rs is the crate root for the binary crate.
    • src/lib.rs is the crate root for the library crate.

Simple IO

Println

1
2
println!("some string")
println!("Hello, {} {}", 5, "times."); // prints `Hello, 5 times.`
  • Println is a Macro
  • You can use #[derive(Debug)] above a struct name to be able to println it.
  • Note that there is also {:?}

    eprintln

  • Standard Output

    dbg!

    You can wrap a variable or an expression with dbg!(...) which will return the same variable after pretty printing it

Command Line Argument

1
2
use std::env;
let args: Vec<String> = env::args().collect();

The first argument is the program name, then the following are the arguments passed to the program

Read File Content

1
2
use std::fs;
let contents: String = fs::read_to_string(file_path)

Environment Variables

1
2
use std::env;
env::var("VARIABLE_NAME")

Variables

  • Variables are immutable by default, to make a variable mutable we need to use mut keyword
    1
    2
    
    let immutable_variable: u32 = 15; 
    let mut mutable_variable: u32 = 25; 
    

    Functions

    1
    2
    3
    
    fn plus_one(x: i32) -> i32 {
      x + 1
    }
    

Comments

1
2
3
4
5
6
7
8
9
// Single-line

/*
* Multi-line
*/

/// Documentation comment

//! Documentation comment. This comments on the containing block
  • Documentation comments support markdown

    Data Types

  • bool
  • char

    Integer

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
arch (architecture word size)isizeusize

Float

f32 and f64, where f64 is the default

Tuple

1
2
3
4
5
6
let tuple: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tuple;
let five_hundred = tuple.0;
let six_point_four = tuple.1;
let one = tuple.2;

  • () Unit is an empty tuple

Struct

  • Unit-like structs are structs that have no fields
    1
    
    struct UnitLikeStruct
    
  • Structs can be like a tuple because their variables have no names.
    1
    2
    3
    
    struct Point(i32, i32, i32);
    let origin = Point(0, 0, 0);
    println!("The origin is at ({}, {}, {})", origin.0, origin.1, origin.2);
    

    We can give the elements of the struct names

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    struct Point {
      x: i32,
      y: i32,
      z: i32,
    }
    let origin = Point { x: 0, y: 0, z: 0 };
    println!(
      "The origin is at ({}, {}, {})",
      origin.x, origin.y, origin.z
    );
    

These two are equivalent.

1
2
3
4
5
6
7
fn create_point_with_zero_z(x: i32, y: i32) -> Point {
    Point { x : x, y: y, z: 0 }
}

fn create_point_with_zero_z(x: i32, y: i32) -> Point {  
Point { x, y, z: 0 }   // note that we do not explicitly need to `x: x`
}

You can also create a new struct from an existing struct modifying one or more of its member variables

1
2
3
4
let point_with_zero_z = Point {
	z: 0,
	..arbitrary_point
};

Methods and Associated Functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct BankAccount {
    account_number: u32,
    balance: Money,
    account_type: String,
}

impl BankAccount { 
    fn deposit(&mut self, amount: Money) {
        self.balance.add(amount);
    }

    fn withdraw(&mut self, amount: Money) {
        self.balance.subtract(amount);
    }

    fn new(account_number: u32, initial_balance: Money, account_type: String) -> BankAccount {
        BankAccount {
            account_number,
            balance: initial_balance,
            account_type,
        }
    }
}

enum

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
enum Point {
    Origin,
    Point(i32, i32, i32),
}

impl Point {
    fn is_origin(&self) -> bool {
        match self {
            Point::Origin => true,
            _ => false,
        }
    }

    fn origin() -> Point {
        Point::Origin
    }
}

let origin = Point::origin();
let point = Point::Point(1, 2, 3); // default (auto-generated) constructor

match point {
    Point::Point(x, y, z) => println!("Point is at {}, {}, {}", x, y, z), // binding (deconstructing)
    Point::Origin => println!("Point is at origin"),
}

we can also

1
2
3
4
5
6
7
8
9
enum Message {
  Move{ x: u32, y: u32 },
}

let x: Message = Message::Move { x: 10, y: 30 };

match x {
  Message::Move { x, y } => {}
}

Collections

String

String interpolation

1
2
3
4
5
6
7
8
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let x = format!("{s1}-{s2}-{s3}");
let y = format!("{}-{}-{}", s1, s2, s3);

let iterator_over_lines = some_string.lines()
  • Strings cannot be indexed because it is a utf-8 string, not ASCII.
  • If you really must, you can slice a string in 4 bytes chunks let s = &hello[0..4];

Array

1
2
3
4
let a: [i32; 5] = [1, 2, 3, 4, 5];
let b = [1; 3]; // results in [1, 1, 1] repeating 1 three times
let first = a[0];
let second = a[1];

Range

1
let range: Range<i32> = (1..4);

Vector

1
2
3
4
5
6
7
let mut v1: Vec<i32> = Vec::new();
let mut v2: Vec<i32> = vec![1, 2, 3];
v1.push(5);
v2.push(6);

let third: &i32 = &v1[2];
let maybe_third: Option<&i32> = v1.get(2);

HashMap

1
2
3
4
5
6
let mut map = HashMap::new();
map.insert(1, "Hello");
map.insert(2, "World");
let value: Option<&&str> = map.get(&1);

map.entry(1).or_insert("!"); // will only insert if the key does not exist

Smart Pointers

Here is the memory leak zone For more information on how memory leaks can happen and why Reference Cycles Can Leak Memory

  • Data structures that act like a pointer but also have additional metadata and capabilities.
  • String and Vec<T> are smart pointers.
  • Implement the Deref and Drop traits.
    • 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.
    • Drop trait allows you to customize the code run when an instance of the smart pointer goes out of scope.
    • std::mem::drop Force drops
  • You can write your own smart pointers
1
2
3
let x: i32 = 5;  
let reference: Box<i32> = Box::new(x);  
let dereference: i32 = *reference;

Box<T>

  • Stores data in the heap instead of the stack
  • Used in situations when
    • When you have a type whose size can’t be known at compile time and you want to use a value of that type in a context that requires an exact size
    • When you have a large amount of data and you want to transfer ownership but ensure the data won’t be copied when you do so
    • When you want to own a value and you care only that it’s a type that implements a particular trait rather than being of a specific type ```rust pub enum List { Cons(i32, Box), Nil, }

impl List { pub fn nil() -> Box { Box::new(List::Nil) }

1
2
3
pub fn cons(value: i32, list: Box<List>) -> Box<List> {
    Box::new(List::Cons(value, list))
} } ```

Rc<T> Reference Counter

  • Used in single-threaded scenarios only
  • If multiple parts of our programs own the data and we know which part finishes last we can make that part the owner. But if we don’t, we use the reference counter.
  • Use Rc::Clone(&ref) to increase the reference count without creating a copy.

RefCell<T> Reference Cell

  • Used in single-threaded scenarios only
  • Enforce borrowing rules at runtime instead of compile time. Rust compiler is conservative and may not compile for programs the developer knows are correct. This gives the developer the ability to move the checks to the runtime. If the check fails, the program will panic.
  • interior mutability pattern is used to mutate the data under immutable reference. A good use case is a mock object.
  • We use borrow and borrow_mut methods instead of & and &mut to keep track of references in the run time.

Combining Rc<T> and RefCell<T>

  • Get a value that has multiple owners and can be mutated.

Ownership

Rules

  • Each value in Rust has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Shallow copying is not allowed. Instead, data is moved

1
2
3
4
let s1 = String::from("hello");
let s2 = s1; // This invalidates `s1`, ends the scope of s1

println!("{}, world!", s1); // This code does not compile

You can, though:

1
2
3
4
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy

println!("s1 = {}, s2 = {}", s1, s2);

Primitives are copied by default. The following will compile without needing to call clone()

1
2
3
4
let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

Even more interesting, calling a function takes over the variable ownership

1
2
3
let s1 = String::from("hello, world!");  
take_over(s1);  
println!("{}", s1); // This will not compile, `s1` is no longer valid

When a function returns a value, the ownership is transferred.

Borrowing/Referencing/&

1
2
3
let x: i32 = 5;  
let reference: &i32 = &x;  
let dereference: i32 = *reference;
1
2
3
4
5
6
7
8
9
fn main() {
    let s1 = String::from("hello, world!");
    borrow_string(&s1);
    println!("{}", s1);
}

fn borrow_string(s: &String) { // s here is immutable, and it cannot be changed
    println!("{}", s);
}

To make a reference mutable, you have to mark it with mut explicitly. You can only have one mutable reference for a variable.

1
2
3
4
5
6
7
8
9
10
fn main() {
    let mut s1 = String::from("hello, world!");
    borrow_string(&mut s1);
    println!("{}", s1);
}

fn borrow_string(s: &mut String) {
	s.push_str(" I am borrowed");
    println!("{}", s);
}

This will not compile

1
2
3
4
5
6
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s; // cannot borrow `s` as mutable more than once at a time

println!("{}, {}", r1, r2);

You cannot also create a mutable reference for something that is already being borrowed

1
2
3
4
5
6
7
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM

println!("{}, {}, and {}", r1, r2, r3);

Note that the cope of a reference ends the last time that reference is used. The following code works absolutely fine

1
2
3
4
5
6
let mut s = String::from("hello");  
let r1 = &s; // no problem  
let r2 = &s; // no problem  
println!("{}, and {}", r1, r2); // The scope of r1 and r2 ends here  
let r3 = &mut s; // Not a problem at all  
println!("{}", r3);

Slices: Reference to a subset of a collection

1
2
3
4
5
6
7
8
let s = String::from("hello world");
  
let hello = &s[0..5]; // the end of range = index of the last element + 1
let world = &s[6..11];
// alternatively
let hello_2 = &s[..5]; // from the beginning
let world_2 = &s[6..]; // until the end
let s_2 = &s[..]; // everything

Lifetime

1
2
3
&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { } // ?Maybe?? x and y have the same lifetime as 'a

The generic lifetime 'a will get the concrete lifetime that is equal to the smaller of the lifetimes of x and y.

1
2
3
struct SomeStruct<'a> {
    field: &'a str, // Ths struct cannot outlive field
}

Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes.

1
let s: &'static str = "I have a static lifetime."; // reference that lives for the entire life of the program
1
fn something<'a, T> // lifetime nad generics in the same time

Control Flow

if statement

Pattern Matching

if let

1
2
3
4
5
6
7
8
9
10
let result: Result<u8, String> = Ok(3u8);

match result {
	Ok(max) => println!("The maximum is configured to be {}", max),
	Err(_) => (),
}

if let Ok(max) = result {
	println!("The maximum is configured to be {}", max);
}

Repetition

loop

1
2
3
4
5
6
loop { // similar to `while true`
    continue;
    break some_value; // you can break with a value which would be the result of the loop expression.
};

while

1
2
3
4
while some_condition {
	 continue;
	 break; // you cannot break with a value similar to while.
}

for

1
for _ in 0..10 {}
1
for item in some_collection{}
1
2
3
4
let mut v = vec![100, 32, 57];
for i in &mut v {
	*i += 50;
}

Strings are special to iterate over a String. You can

1
2
for c in some_string.chars(){}
for b in some_string.bytes(){}

For Hashmaps

1
2
3
for (key, value) in &map {
	println!("{key}: {value}");
}

tail recursion

Functional Programming

Closures

1
2
3
4
5
6
fn  add_one_v1   (x: u32) -> u32 { x + 1 }
// Rewriting function as a closure
let add_one_v2 =      |x: u32| -> u32 { x + 1 };
let add_one_v3 =      |x|             { x + 1 };
let add_one_v4 =      |x|               x + 1  ;
let add_one_v5 = move |x|               x + 1  ; // force move ownership.
  • If we use a closure without a type annotation, the type of variables will be inferred from the first usage.
  • Closures can capture variables from their environment.
  • A closure that mutates a variable it captures must be declared as mut.
  • Closures implement one of these traits
    • FnOnce
    • FnMut
    • Fn

      Iterators

  • Iterators are lazily evaluated.
  • vector.iter()
    1
    
    vector.iter().map(closure).collect()
    

Error Handling

Unrecoverable Errors: Panic

1
panic!("crash and burn");

Recoverable Errors

1
2
3
4
enum Result<T, E> {
    Ok(T),
    Err(E),
}
1
2
3
4
5
File::open("hello.txt").unwrap(); //will panic if an error occurs.
File::open("hello.txt")
        .expect("hello.txt should be included in this project"); //Panic with a custom error message
File::open("hello.txt")? // propagates the result to the calling function
File::open("hello.txt")?.read_to_string(&mut username)?; // `?` can be chained
  • ? operator calls the from function, and the error type received is converted into the error type defined in the return type of the current function. This is defined in the From trait.
  • We are only allowed to use the ? operator in a function that returns Result, Option, or another type that implements FromResidual.
  • main function can return errors too fn main() -> Result<(), Box<dyn Error>>
  • User expect instead of unwrap to document why you know this code will not panic
    1
    2
    3
    
    let home: IpAddr = "127.0.0.1"
          .parse()
          .expect("Hardcoded IP address should be valid");
    

    Standard Errors

  • std::io::Error
  • Error constructor signature: pub fn new<E>(kind: ErrorKind, error: E) -> Error, Note ErrorKind

    Generics

1
2
3
4
5
6
7
8
9
10
11
struct SomeStruct<T>{
    field: T,
}

impl<T> SomeStruct<T> {
    fn new(field: T) -> Self {
        Self { field }
    }
}

fn do_something<T, R>(value: T) -> R {...}

Traits

1
2
3
4
5
6
7
8
9
pub trait SomeTrait {
    fn do_something(&self) -> String {
        // optional default implementation
    }
}

impl<T> SomeTrait for SomeStruct<T> {
    fn do_something(&self) -> T {}
}
1
2
3
fn multiple_traits_expected(value: (impl SomeTrait + SomeOtherTrait)) {}  // Opposite to structs we need `impl` keyword here
fn multiple_traits_expected_generics<T: SomeTrait + SomeOtherTrait>(value: T) {}
fn multiple_traits_expected_generics_with_where<T>(value: T) where T: SomeTrait + SomeOtherTrait {}

We can also conditionally implement a trait for any type that implements another trait.

1
2
trait SomeNewDisplayBehaviour
impl<T: Display> SomeNewDisplayBehaviour for T {}

Safe Duck Typing (trait objects)

  • Prefer generics over trait objects because
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
fn main() {
    let mallard = MallardDuck {};
    let rubber = RubberDuck {};

    let ducks: Vec<&dyn Duck> = vec![&mallard, &rubber];

    for duck in ducks {
        listen_to_duck(duck);
    }
}

fn listen_to_duck(duck: &dyn Duck) {
    duck.quack();
}

struct MallardDuck {}

impl Duck for MallardDuck {
    fn quack(&self) {
        println!("Mallard Quack!");
    }
}

struct RubberDuck {}

impl Duck for RubberDuck {
    fn quack(&self) {
        println!("Rubber Squeak!");
    }
}

Concurrency

Rust has few concurrency features. The concurrency features listed below are part of the standard library. One may use these or other concurrency implementations.

Threads

  • Rust standard library threads correspond 1:1 to operating system threads.
  • When the main thread completes, all other threads shut down, whether they have completed or not.
1
2
3
let handle: JoinHandle<i32> = thread::spawn(closure()); // closures returning a value  
let joined: thread::Result<i32> = handle.join(); // wait for the thread to finish  
let result: i32 = joined.unwrap(); // unwrap result
  • We must move variables to the closure running in a thread.
1
2
3
4
let v = vec![1, 2, 3];  
let handle = thread::spawn(move || {  
  println!("Here's a vector: {:?}", v);  
});

Sleep

1
2
3
4
use std::thread;
use std::time::Duration;

thread::sleep(Duration::from_secs(1));

Channels

  • std::sync::mpsc::channel(): Multiple Producer, Single Consumer
1
let (tx, rx) = mpsc::channel();

Transmitter

  • tx.send()
  • send return a Result which will error if the receiver has dropped
  • To have multiple transmitters we need to clone tx:
    1
    
    let tx1 = tx.clone();
    

Receiver

  • rx.recv() blocks
  • rx.try_recv() doesn’t block.
  • recv and try_recv return a Result, which will return an error if the transmitter is closed.
  • We can iterate over the received messages
1
2
for received in rx { ... }
rx.iter().map{ ... }.for_each{ ... }

Mutex

1
2
3
4
5
6
7
8
9
use std::sync::{LockResult, Mutex, MutexGuard};

let m: Mutex<i32> = Mutex::new(5);
{
	let result: LockResult<MutexGuard<i32>> = m.lock();
	let mut num: MutexGuard<i32> = result.unwrap();
	*num = 6;
} // num goes out of scope here, so the lock is released
println!("num = {:?}", m.lock().unwrap());
  • To give multiple threads access to a Mutex we need to Arc<T>, an atomically reference counter
1
2
3
let mutex: Mutex<i32> = Mutex::new(0);  
let counter: Arc<Mutex<i32>> = Arc::new(mutex);  
let counter: Arc<Mutex<i32>> = Arc::clone(&counter); // for each thread
  • Using Arc<Mutex<T>> may result in deadlocks.

Visibility Rules

  • items in a module are private to that module and its sub-modules
  • Making enum pub makes its fields public.
  • Making a struct pub, doesn’t make its members public. We need to make them public explicitly.
  • You can use methods like the ok method on Result or the ok_or method on Option to do the conversion explicitly.
  • To bring something into scope (i.e. import) we need to use use. For example use std::io;
  • Rust has a set of items defined in the standard library that it brings into the scope of every program. This set is called the prelude

Notes

  • Rust library is called crate, a library crate to be more specific as opposite to binary crate which is your executable.

Code Organization

  • We cannot test code in main.rs directly. So, we put our business logic in lib.rs. Check link

Testing

1
2
3
4
5
6
7
8
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}
  • assert!, assert_eq!, assert_ne!
    1
    2
    3
    4
    5
    
    assert!(
      result.contains("Carol"), // assertion
      "Greeting did not contain name, value was `{}`", // message
      result output
    );
    
1
2
3
4
#[should_panic(expected = "hello")] // contains
fn it_works_some_how() {
    panic!("hello world")
}
1
2
#[test]
fn it_works_some_how() -> Result<(), String> {} // will fail on `Err`
1
2
3
#[test]
#[ignore]
fn this_test_not_going_to_run()
1
2
3
4
5
6
cargo test
cargo test part_of_test_name    # partial match
cargo test --help               # Help for cargo test
cargo test -- --ignored         # Runs ignored tests
cargo test -- --help            # Help for the test binary
cargo test -- --show-output     # Show output from passing tests (printlns)
  • You can only write integration tests for code in library crates lib.rs but not binary crates main.rs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    ├── Cargo.lock
    ├── Cargo.toml
    ├── src
    │         └── lib.rs
    │         └── main.rs
    └── tests
      ├── common
      │         └── mod.rs
      └── integration_test.rs
    
  • common is not part of the tests but can be included in the tests (for setup, etc.)
  • code in lib can be exported pub to be used in both main and integration tests

Publishing your package to crates.io

  • Once a crate is pulished it cannot be deleted
  • login at crates.io
  • create a token
  • cargo login $TOKEN
  • cargo publish
    • cargo publish -p package_name if you have multiple packages in the same workspace.
  • cargo yank --vers "0.0.1" Pervents others from downloading and or depending on this version.
  • cargo yank --vers "0.0.1" --undo The reverse of the previous operation

    Installing binary crates

    1
    2
    
    cargo install crate_name
    cargo uninstall crate_name
    

    Resources

  • Official Rust Book
  • Rustlings
This post is licensed under CC BY 4.0 by the author.
Contents

Akka Scala Cheat Sheet

How to Make Refactoring a High-Yield, Low-Risk Investment?

Comments powered by Disqus.