kuniga.me > Docs > Rust Cheatsheet

Rust Cheatsheet

Index

  1. Basic types
    1. Type names
    2. Arrays
      1. Initialize
      2. Iterate
    3. Bool
    4. Enum
    5. Integers
      1. Bit operations
    6. Iterator
    7. Option
    8. Pair
    9. String
      1. Types
      2. String (variable length)
      3. Operations
    10. Struct
      1. Destructuring
      2. Methods
    11. Tuple
    12. Void
  2. Functions
    1. Closure
      1. Multi-line
  3. Flow Control
    1. Conditional
    2. Loops
      1. iter() vs into_iter()
    3. Result
      1. API
      2. Example
      3. Generic Error
      4. Propagation
  4. Data structures
    1. BTreeMap
    2. BTreeSet
    3. Vector
      1. Initialize
      2. Inserting
      3. Iterating
      4. Mapping
      5. Filtering
      6. Length
      7. Sorting
      8. Destructured assignment
    4. HashMap
    5. HashSet
    6. Queue
  5. Object Oriented
    1. Structures
      1. Interior Mutability
    2. Traits
      1. Default implementation
      2. Trait type
      3. Multiple trait types
      4. Return trait type
      5. Conditional struct implementation
      6. Conditional trait implementation
      7. Dynamic dispatch
  6. I/O
    1. Read from stdin
    2. Read CLI arguments
    3. Printing to stdout
    4. Temporary File
  7. Math
    1. Exponentiation
    2. Square root
  8. Mutability
  9. Attributes
  10. Modules
    1. Nested
  11. Memory Management
    1. Smart Pointers
    2. Ownership

Basic types

Type names

Arrays

Arrays are of fixed size. For variable size, see Vec.

Initialize

// Initialize array
let arr: [i32; 3] = [1, 2, 3];
let arr: [i32; 10] = [0; 10];

Iterate

let it:
let v: Option<Self::Item> = it.next()

Bool

Enum

enum Direction {
    N,
    E,
    S,
    W,
}

Adding conversion to string:

use std::fmt;

impl fmt::Display for Direction {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let s = match self {
            Color::N => "North",
            Color::E => "East",
            Color::S => "South",
            Color::W => "West",
        };
        write!(f, "{}", s)
    }
}

// In code
let s = Direction::N.to_string();

Integers

Bit operations

! is the Rust version of ~

Iterator

Note that Iterator is not a class but rather a trait that classes implement.

Get the next element:

// returns Split class which implements Iterator
let mut parts = s.split(',');
let e: Option<&str> = parts.next();

NOTE: next() is not idempotent.

Option

// Handle option:
match my_option {
    Some(value) => value,
    None => 1, // handle null value
}

Create Some:

Some(1)

Throw if None:

my_option.unwrap()

Test if some:

my_option.is_some()

Test if none:

my_option.is_none()

Get a default value if none:

let v = my_option.unwrap_or(default_value)

or if the default is expensive to compute:

let v = my_option.unwrap_or_else(|| get_default_value())

Pair

Done via tuples. See Tuple.

String

Types

String (variable length)

// Empty string
let mut my_str: String = "".to_owned();

// Other form
let mut my_str = String::from("Hello");

// Length
my_str.len();

// Iterate over its characters
for c in my_str.chars() {
    // do something with c
}

Operations

let concatenated = format!("{}{}", a, b);

From literal strings:

let s: String = "hello".to_owned();

Convert to int:

let i = s.parse::<i32>().expect("Should be numeric");

Convert from int:

let i: i32 = 10;
let s = i.to_string();

Convert to Vec<char>:

let cs: Vec<char> = s.chars().collect();

Split:

let parts: Vec<&str> = s.split(',').collect();

Split into multiple lines. There’s a shortcut for splitting by the character \n:

let parts: Vec<&str> = s.lines().collect();

Substring:

s.contains(p);

Trim:

let trimmed = s.trim();

Struct

keywords: record / object / shape

struct Tree {
    guess: Vec<i32>,
    children: Vec<Tree>,
}

Destructuring

let foo = Foo { x: (1, 2), y: 3 };
let Foo { x: (a, b), y } = foo;

Methods

struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }
}

Tuple

Type: (T1, T2), e.g. (i32, bool)

Create:

let tup: (i32, String) = (64, "hello")

Access:

tup.0 // 64
tup.1 // "hello"

or via destructuring:

let (n, s) = &tup;

Void

“void” is an empty tuple an in rust is called the unit type. Example:

fn returns_void() -> () {
    return ();
}

() is also what gets returned if no return is provided:

fn returns_void() -> () {}

Functions

fn myFun(arg1: i32, arg2: i32) -> i32 {
}

Rust doesn’t support default arguments

Closure

keywords: lambda

    let plus_one = |x: i32| x + 1;

Multi-line

let multi_line = |x: i32| {
    let mut result: i32 = x;
    result += 1;
    result
}

Flow Control

Conditional

if n < 0 {
    print!("{} is negative", n);
} else if n > 0 {
    print!("{} is positive", n);
} else {
    print!("{} is zero", n);
}

Early-return/continue:

let Some(value) = maybe else {
    continue;
}

Loops

for i in 0..3 {
    println!("{}", i);
}

See also “Iterating” on different data structures.

iter() vs into_iter()

iter() borrows the collection, so it’s still valid after the loop. However, each element is also a borrowed reference. There’s a variant iter_mut(), in which the borrowed reference is mutable, so any changes are done inline on the collection.

for item in vec.iter() {
    // If vec is Vec<T>, item is a borrowed reference &T
}

into_iter() moves the collection into the loop, so it’s not valid afterwards. Each element in the loop is not a reference.

for item in vec.into_iter() {
    // If vec is Vec<T>, item is T, and was moved from vec
}
// vec is not valid anymore

If you wish to mutate the element:

for mut item in vec.into_iter() {
    // item was moved from vec
}

Result

Keyword: Exceptions

API

let r: Result<()> = Ok(());
if r.is_ok() {
    println!("ok");
}
if r.is_err() {
    println!("not ok: {}", r.unwrap_err());
}

Example

Result has type Result<T, E> and is union of Ok<T> and Err<E>. Example of a function that returns a Result:

fn maybe(v: i32) -> Result<i32, String> {
    if (v == 0) {
        return Err("Cannot be 0".to_owned());
    }
    Ok(v)
}

On the caller side:

let x = maybe(v);
match x {
    Ok(value) => println!("{}", value),
    Err(error) => println!("{}", error),
}

Generic Error

To avoid specifying the error type, one can use the Result from the crate anyhow. Importing create in Cargo.toml:

[dependencies]
anyhow = "1.0"

Example from before:

use anyhow::{Result, anyhow};
fn maybe(v: i32) -> Result<i32> {
    if v == 0 {
        return Err(anyhow!("Cannot be 0"));
    }
    Ok(v)
}

The rest is the same. Noting that anyhow::Result<T> is an alias for Result<T, anyhow::Error>. The anyhow:Error is created using the macro anyhow!.

To map Result<T, E> into Result<T>:

use anyhow::{Result, anyhow};
fn my_func() -> Result<T, E>;

my_func()
    .map_err(|e| Error::new(e).context("new error"))?;

Propagation

If a function calls functions returning Result it can propagate the results like so:

use anyhow::{Result, anyhow};
...
fn forward(v: i32) -> Result<i32> {
    let r = maybe(v);
    if r.is_err() {
        return r;
    }
    let x = r.unwrap();
    Ok(x + 1)
}

There’s a very convenient operator, ?, to avoid this boilerplate:

use anyhow::{Result, anyhow};
...
fn propagate(v: i32) -> Result<i32> {
    let x = maybe(v)?;
    Ok(x + 1)
}

Keyword: rethrow

If some context is needed, we can use with_context():

use anyhow::{Result, anyhow};
...
fn propagate(v: i32) -> Result<i32> {
    let x = maybe(v).with_context(||
        format!("third_error")
    )?;
    Ok(x + 1)
}

In case of error

third_error

Caused by:
0: original_error
1: secondary_error

Data structures

BTreeMap

BTreeMap is another implementation of a efficient key-value (the other being HashMap). One analogy for C++ is that BTreeMap is std::ordered_map and HashMap is std::unordered_map.

use std::collections::BTreeMap;

// Create empty
let mut my_map = btreemap! {};

// Create initialized
use maplit::btreemap;
let my_map = btreemap! {
    "blue" => 1,
    "green" => 2,
    "red" => 3
};

BTreeSet

There’s an analogy between BTreeSet and HashSet with BTreeMap and HashMap.

// Create empty
let mut my_map = btreeset![];

// Create initialized
use maplit::btreeset;
let fruits = btreeset![
    "apple",
    "banana"
]

Vector

Initialize

let mut vec = Vec::new();

Fixed size, same value:

let mut vec = vec![0; 100];

Inserting

vec.push(1);

Iterating

for item in vec.iter() {
    ...
}

Mapping

let u = vec![1, 2, 3];
let v: Vec<_> = u.iter().map(f).collect();

Filtering

let u = vec![1, 2, 3];
let v: Vec<_> = u.iter().filter(f).collect();

In-place

let mut u = vec![1, 2, 3];
u.retain(f);

Length

vec.len()

Sorting

vec.sort_by(|a, b| a.cmp(b))

Destructured assignment

Like in Python, we can do destructured assignment by assuming a fixed length of a vector, but we have to handle the other cases:

let [a, b] = vec.as_slice() else {
    panic!();
}

HashMap

Reference: HashMap.

Import name:

use std::collections::HashMap;

Type definition:

HashMap<String, String>;

Create empty:

let mut my_map = HashMap::new()

Create initialized:

let my_map = HashMap::from([
    ("blue", 1),
    ("green", 2),
    ("red", 3),
]);

or

use maplit::hashmap;
let my_map = hashmap!{
    "blue" => 1,
    "green" => 2,
    "red" => 3
};

Insert:

my_map.insert(
    "key_a".to_string(),
    "value_a".to_string(),
);

Access:

match my_map.get("key_a") {
    Some(value) => println!("value={}", value),
    None => println!("not found")
}

Update

if let Some(value) = my_map.get_mut(key) {
    *curr_value = 1;
}

All values

for value in my_map.values() {
}

All keys

for key in my_map.keys() {
}

HashSet

Use:

use std::collections::HashSet;

Create:

let s = HashSet::new();

Create initialized:

use maplit::hashset;
let my_map = hashset![1, 2, 3];

From vector:

let v = vec![1, 2, 3];
// cloned needed if borrowing v
let s: HashSet<_> = v.iter().cloned().collect();

Set intersection:

let i: HashSet<_> = s1.intersection(&s2).cloned().collect();

Queue

use std::collections::VecDeque;

// Create
let mut queue = VecDeque::new();

// Enqueue
queue.push_back("a");

// Dequeue
if let Some(x) = queue.pop_front() {
    // use x
}

// Is empty?
if !queue.is_empty() {
    // use queue
}

Object Oriented

Structures

pub structure MyClass {
    my_field: bool,
}

impl MyClass {
    // Constructor-like
    pub fn new() -> MyClass {
        return MyClass {
            my_field: true,
        }
    }

    // Read Method
    pub fn get(&self) -> bool {
        self.my_field
    }

    // Write Method
    pub fn set(&mut self, value: bool) {
        self.my_field = value;
    }
}

Interior Mutability

Keywords: Bypass read-only of &self.

By default you can’t mutate a field inside a method if you pass &self. There’s a way to bypass this for implementation details, by using RefCell, Mutext or Atomic types.

pub structure MyClass {
    my_field: bool,
    was_called: Arc<AtomicBool>,
}

impl MyClass {
    // Constructor-like
    pub fn new() -> MyClass {
        return MyClass {
            my_field: true,
            was_called: Arc::new(AtomicBool::new(false))
        }
    }

    // Read Method with internal mutability
    pub fn get(&self) -> bool {
        self.was_called.store(true, Ordering::Relaxed);
        self.my_field
    }
}

Traits

Traits are analogous to interfaces in other languages (though it can have default implementation).

pub trait Summary {
    fn summarize(&self) -> String;
}

Implement a trait for a structure:

pub struct Article {
    pub text: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        self.text
    }
}

Note: we can’t implement external traits on external types.

Default implementation

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

impl Summary for Article {}

Trait type

Traits can be used as type constraints:

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Syntax sugar to:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Multiple trait types

Multiple traits requirement:

pub fn notify(item: &impl Summary + Display) {
    ...
}

or

pub fn notify<T: Summary + Display>(item: &T) {
    ...
}

Return trait type

pub fn get() -> impl Summary {
    ...
}

Note however that get() cannot return different implementations of Summary.

Conditional struct implementation

Below, the method my_summary is only implemented for template types implementing Summary, e.g. C<Article>.

struct C<T> {
    x: T,
}

impl<T: Display> C<T> {
    fn my_display(&self) {
        ...
    }
}

Conditional trait implementation

A trait can be implemented for a struct conditioned on its type satisfying some other trait:

impl<T: Display> AnotherTrait for T {
    ...
}

Dynamic dispatch

If we want to have a pointer to objects of a class implementing a trait MyTrait, we can use dynamic dispatch so that methods are resolved at runtime. Example:

struct A {}
struct B {}
trait MyTrait {
    fn method(&self) {}
}

impl MyTrait for A {
    fn method(&self) {}
}
impl MyTrait for B {
    fn method(&self) {}
}

fn f(ptr: Arc<dyn MyTrait>) {
    ptr.method()
}


f(Arc::new(A {}));
f(Arc::new(B {}));

I/O

Read from stdin

use std::io::{self, Read};

let mut input = String::new();
io::stdin().read_to_string(&mut input)
    .expect("Failed to read input");

Read CLI arguments

use std::env;

// Skip program name
let args: Vec<String> = env::args().skip(1).collect();

Printing to stdout

Vector:

let vec = vec![1, 2, 3, 4, 5];
println!("Vector:\n{:#?}", vec);

Temporary File

use tempfile::NamedTempFile;
use std::io::{Write, Read};

let mut tmp_file = NamedTempFile::new()?;

// Write
writeln!(tmp_file, "Hello world!")?;

// Read
let mut contents = String::new();
tmp_file.reopen()?.read_to_string(&mut contents)?;

Math

A lot of the math operations are methods on the numerical types.

Exponentiation

let b: i32 = 10;
let p10: i32 = b.pow(2 as u32);

Note that the exponent has to be positive, since a negative one could change the type of the base to floating point.

Square root

let x: f64 = 10.0;
let y: f64 = x.sqrt();

Not defined for integer types.

Mutability

Variable doesn’t need to be mutable if it’s initialized only once:

let x: i32;

if check() {
    x = 1;
} else {
    x = 2;
}

Attributes

Attributes are in the form #[foo] and placed on top of functions, structs and modules to add metadata to it, e.g.

#[test]
fn test_invalid_argument() {
    ...
}

or

#[derive(Clone, Debug, Eq)]
struct Tree {
    ...
}

Modules

Declaration:

mod math {
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    pub fn sub(a: i32, b: i32) -> i32 {
        a - b
    }
}

Usage:

fn f() {
    math::sub(1, 2);
}

Nested

Modules can be nested but because of visibility being private by default, inner modules must be made explicitly public:

mod outer {
    pub mod inner {
        pub fn f() {
            println!("Hello world");
        }
    }
}

fn f() {
    outer::inner(1, 2);
}

Memory Management

Smart Pointers

Box is analogous to std::unique_ptr in C++.

let x = Box::new(5);

Remembering that move happens by default on assignment in Rust:

let y = x; // x is now invalid

std::sync::Arc is analogous to std::shared_ptr in C++. It’s a reference counted pointer to the heap. Creation:

let x = Arc::new(5);

Share ownership:

let y = Arc::clone(&x);

Ownership

In Rust we have 3 ways of passing data from one variable to another:

There’s a convention for classes to implement these different modes.

Mode / semantics API
Copy to_
Borrow as_
Move into_

Reference: Rust API Guidelines