Skip to content

Rust

Guidelines / Best Practices

Useful Macros

// The print line macro for writing to STDOUT
!println("The value of x is {x}");
!println("The value of x is {}", x);

// The debug macro, !dbg writes out to STDERR and it includes
// the file and line number where the !dbg invocation was made
!dbg(x);

Comments

// Single line comment
/// Documentation comment. They also support markdown notation
/// # Heading 1
/// ```
/// let one = 1;
/// ```

Variables

Declaration

Declare variables with the let keyword. By default, variable binding is immutable.

let     x: i32 = 42;    // immutable binding
let mut x: i32 = 42;    // mutable binding

Shadowing

Variables can be shadowed, which allows reuse of variable names in the same scope

Numerics

Declaration

The Rust compiler can infer what type a variable is

let x: i32 = 10;
let y: f64 = 2.17;

Or you can Use suffixes for explicit initialization of numerics

let i: i32 = 42i32;     // i = 42
let f: f64 = 3.14f64;   // f = 3.14

Control Flow

The match statement

A control-flow construct to compare values against a series of patterns. It's like a more powerful and complex switch statement in C.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

The if let statement

This statement is a succinct way to handle match patterns and is essentially syntactic sugar for a match that ignores all other values by default. In other words if let does not perform exhaustive checking like a match statement.

let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
}

Looping

An infinite loop with break

let mut counter = 0;
let result = loop {
    counter += 1;

    if counter == 10 {
        break counter * 2;
    }
};

println!("The result is {result}");
// Returns are implicit with no semicolon on the last line
fn multiply(x: i32, y: i32) -> i32 {
    x * y
}

// You can also use an explicit `return` statement
fn multiply(x: i32, y:i32) -> i32 {
    return x * y;
}

The conventional style for function names in rust is snake case.

fn hello_world() {
    println!("Hello World!");
}

Scoping

  • Curly braces {}
  • The use keyword
  • local keyword
  • static lifetime

Non-Lexical Lifetimes (NLL)

Memory Management

Ownership

Ownership refers to the set of rules used by Rust to ensure safety during memory management.

References and Borrowing

A reference is specified with the ampersand (&). Dereferencing is done with the asterisk (*)

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // calculate length with borrowed s1
    println!("The length of '{}' is {}.", s1, len);
}

Rust allows for as many unmutable (default) references to a variable as you want. However, you can only have a single mutable reference to a variable.

Slices

String Slices

let s = String::from("hello");
let slice = &s[0..2];   // slice from index 0 with length of 2
let slice = &s[..2];    // same as above
let slice = &s[2..];    // slice from index 2 to end of string
let slice = &s[..];     // slice of entire string

Be wary of mutibyte UTF-8 characters. See Storing UTF-8 Encoded Text with Strings

Collection Slices

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);

Data Structures

Enums

#[derive(Debug, PartialEq, Copy, Clone)]
pub enum VideoGameGenre {
    FPS,
    MOBA,
    Platformer,
    Metroidvania,
    RPG,
}

Structs

Declaration

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

Struct update syntax allows the remaining, unspecified fields of a new instantiation to be filled in by the values of another. The following example

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

Update Syntax

Useful Articles / Docs

Tuple structs

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Functions

Associated Function

An associated function refers to any function defined withing an impl. These functions can be methods or non-methods.

  • Methods include self as the first parameter. Methods are accessed .
  • Non-methods do not require a self. Non-methods are accessed with ::

Methods

Use the impl keyword to define methods on structs. A method must have a either self, &self, or &mut self as it's first parameter.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Generics

Traits

Traits as Parameters

Traits Bounds

  • Compound trait bounds with +
  • where clause
    fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
        U: Clone + Debug
    {
    

Lifetimes

Some general tips:

  1. Don't use References. Use ownership and share nothing. Pass and return variables through functions if you need to.
  2. Copy and Clone everything. The performance hit probably won't be noticeable. If it is, then refactor.
  3. Only use References

Lifetime Annotation Syntax

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

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

#### Lifetime Ellision Rules
#### The Static Lifetime

Attributes

panic!

Compilation / Building

Cargo

  • Run cargo doc --open from the project directory to open a local webpage listing docs for all the project's dependencies listed in Cargo.toml

rustc

  • Use rustc file.rs to compile a rust binary

rustup

Paths / using modules

Packages and Crates

  • Packages: one or more crates that provide functionality. A package contains a Cargo.toml files that describes how to build the crates.
  • Crates
  • Binary crate: programs that can be compiled into an executable, must have a main functions
  • Library crate: defines functions to be shared to other projects, does NOT have a main function
  • A package can contain at most one library crate. It can contain as many binary crates as you’d like, but it must contain at least one crate (either library or binary).
  • Use cargo new <dir> to create a new crate (library crate by default) in the dir directory
  • Specify a binary crate with cargo new <dir> --bin