Back to Compendiums

Rust Book

by matsjfunke

The following is my subjective summary of the rust book

  • for a more parctical learning checkout:

Installation / Update

Install Rust, with rustup, which manages Rust versions and associated tools:

bash
1# Install Rust
2curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
3
4# Follow the on-screen instructions to complete the installation.
5
6# Update Rust
7rustup update
8
9# verify installation / check version
10rustc --version
11

Compile / run

bash
1# create rust file
2echo 'fn main() { println!("Hello, World!"); }' > hello.rs
3# compile
4rustc main.rs
5# run
6./main
7

Cargo: system and package manager

Instead of saving the result of the build in the same directory as our code, Cargo stores it in the target/debug directory

bash
1# create a project
2cargo new <project-name>
3# build a project
4cargo build
5# build and run a project in one step
6cargo run
7# build a project without producing a binary to check for errors
8cargo check
9

To add libraries (crates), update Cargo.toml:

ini
1[dependencies]
2rand = "0.8"
3
bash
1# update dependencies
2cargo update
3

Variables

  • all immutable by default
rust
1let x = 5; // immutable
2let mut x = 5; // mutable
3
4const x = 5; // always immutable & typed
5

shadowing (reusing name)

  • must be same type
  • only works in same scope
rust
1let x = 5;
2let x = x + 5;
3println!({x}); // 10
4

Types

Integers

Signed and unsigned refer negative or positive

  • whether the number needs a sign (signed) or it will only ever be positive and can therefore be represented without a sign (unsigned).
  • default = i32
lengthsignedunsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Floats

  • all signed
  • f32 & f64
  • default type f64 because on modern CPUs roughly same speed as f32 but more precise.

Booleans

  • size = 1 bite
rust
1fn main() {
2    let t = true;
3
4    let f: bool = false; // with explicit type annotation
5}
6

Characters

char literals with single quotes, type is four bytes in size and represents a Unicode Scalar Value, meaning it can represent a lot more than just ASCII

Strings

  • literals which use double quotes.
  • all UTF-8
  1. String -> create or modify strings
  2. &str (string slice) -> read only (immutable) more on references later
rust
1let x: char = 'hello';
2let x: &str = "hello";
3

Compound Types

Compound types are types that can group multiple values into one.

Tuple ()

  • grouping a variety of types
  • fixed length: once declared, they cannot grow or shrink in size.
rust
1fn main() {
2    let tup: (i32, f64, u8) = (500, 6.4, 1);
3
4    let (x, y, z) = tup;
5    println!("The value of y is: {y}"); // 6.4
6
7    let five_hundert = tup.0
8    println!("value of index 0 is: {five_hundert}"); // 500
9}
10

Array []

  • elements of array must have same type.
  • arrays in Rust have a fixed length.
rust
1let months = ["January", "February", "March", "April", "May", "June", "July",
2              "August", "September", "October", "November", "December"];
3
4let a: [i32; 5] = [1, 2, 3, 4, 5];
5

Functions

rust
1 // Statements = intructions for  actions that dont return a value.
2let y = 6;
3// Expression evaluate to a resultant value.
4let y = {
5    let x = 3;
6    x + 1
7};
8
  • main function / entrypoint at top of file
  • returning values
rust
1fn main() {
2    let result = sum(5, 10);
3    println!("The sum is: {result}");
4}
5fn sum(a: i32, b: i32) -> i32 { //don't need to name return values, but we must declare their type after arrow ->
6    a + b // implicit return
7    // or
8    return a + b // explicit return
9
10}
11

Control Structure

if, else

rust
1fn main() {
2    let condition = true;
3    let number = if condition { 5 } else { 6 }; // if is an expression, we can use it on the right side of a let statement to assign the outcome to variable
4
5    if number % 4 == 0 {
6        println!("number is divisible by 4");
7    } else if number % 3 == 0 {
8        println!("number is divisible by 3");
9    } else if number % 2 == 0 {
10        println!("number is divisible by 2");
11    } else {
12        println!("number is not divisible by 4, 3, or 2");
13    }
14}
15

loops

rust
1// loop -> used to loop infintely until break
2loop {
3     break;
4}
5// while / conditional loop -> until false
6fn main() {
7    let mut number = 3;
8
9    while number != 0 {
10        println!("{number}!");
11
12        number -= 1;
13    }
14
15    println!("LIFTOFF!!!");
16}
17// for loop -> used for iterating
18fn main() {
19    for number in (1..4).rev() { // rev method, to reverse the range
20        println!("{number}!");
21    }
22    println!("LIFTOFF!!!");
23}
24

Ownership (managing computer memory)

Ownership Rust manages memory by ensuring each variable has a single owner at a time, automatically deallocating it when the owner goes out of scope.

  • python for example has "garbage collection" that regularly looks for no-longer-used memory as the program runs.
  • in other languages, the programmer must explicitly allocate and free the memory.
  • rust manages memory through a system of ownership with a set of rules that the compiler checks.

Stack & Heap

Stack stores values in the order it gets them and removes the values in the opposite order -> last in, first out

  • think: stack of books, new book layed on top is the first to get picked but.

Heap less organized: putting data on heap, requests certain amount of space, memory allocator finds an empty spot in the heap that is big enough, marks it as being in use, and returns a pointer, which is the address of that location of the data

  • think: of a table where you can place objects anywhere there's space. To find an object later, you need to remember its exact location on the table.
rust
1let x: i32 = 10; // Allocated on the stack
2let s = String::from("hello"); // Allocated on the heap
3

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.

copying variables

rust
1// stack
2let x = 5;
3let y = x;
4println!("x = {x}, y = {y}");
5
6// heap
7let s1 = String::from("hello");
8let s2 = s1.clone();
9println!("s1 = {s1}, s2 = {s2}");
10

Variable Scope

  • variables are only accessable if the parrent is in scope
rust
1{                      // s is not valid here, it's not yet declared
2
3    let s = "hello";   // s is valid from this point forward
4    let s = String::from("hello"); // s is valid from this point forward
5
6    // do stuff with s
7}                      // this scope is now over, and s is no longer valid
8

References / Borrowing

Ownership problem: ownership is transfer

  • when a function takes ownership of a value, the original variable can no longer be used unless the ownership is returned -> cumbersome and unnecessary

Solution: using References

  • References allow you to refer to a value without taking ownership
  • use "&" to create references that borrow data without taking ownership.

Borrowing -> accessing a variable's value through a reference

rust
1// reference example
2fn main() {
3    let s1 = String::from("hello");
4
5    let len = calculate_length(&s1); // & references s1
6
7    println!("The length of '{}' is {}.", s1, len);
8}
9
10fn calculate_length(s: &String) -> usize { // calculate_length borrows reference to s
11    s.len()
12}
13
14// mutable reference example
15fn main() {
16    let mut s = String::from("hello"); // mut makes s mutable
17
18    change(&mut s); // &mut references to s and shows mutablility
19}
20
21fn change(some_string: &mut String) {
22    some_string.push_str(", world");
23}
24

Structs

similar to tuple: pieces of struct can be different types but in struct each piece of data has a name to clarify purpose

rust
1// define a struct
2struct User {
3    active: bool,
4    username: String,
5    email: String,
6    sign_in_count: u64,
7}
8
9// to use struct create an instance of that struct by specifying values
10fn main() {
11    let mut user1 = User { // entire instance must be mutable; Rust doesn't allow us to mark only certain fields as mutable
12        active: true,
13        username: String::from("someusername123"),
14        email: String::from("someone@example.com"),
15        sign_in_count: 1,
16    };
17
18    user1.email = String::from("anotheremail@example.com"); // access specific value with dot-notation
19
20    // Creating Instances from Other Instances with Struct Update Syntax
21    let user2 = User {
22        active: user1.active,
23        username: user1.username,
24        email: String::from("another@example.com"),
25        sign_in_count: user1.sign_in_count,
26    };
27}
28
29// Function that returns a User instance
30fn build_user(email: String, username: String) -> User {
31    User {
32        active: true,
33        username, // type is already defined in parameter
34        email,
35        sign_in_count: 1,
36    }
37}
38

Tuple Structs

  • structs w/o names
rust
1struct Color(i32, i32, i32);
2struct Cursor(i32, i32, i32, i32);
3
4fn main() {
5    let black = Color(0, 0, 0);
6    let current_location = Cursor(0, 0, 0);
7}
8

Struct Methods

  • functions inside structs
  • impl stands for implementation aka. rust method
rust
1#[derive(Debug)] // Debug trait enables to print struct in a way we can see its value while we're debugging
2struct Rectangle {
3    width: u32,
4    height: u32,
5}
6
7// method definition
8impl Rectangle {
9    fn area(&self) -> u32 {
10        self.width * self.height
11    }
12}
13
14fn main() {
15    let rect1 = Rectangle {
16        width: 30,
17        height: 50,
18    };
19
20    println!(
21        "The area of the rectangle is {} square pixels.",
22        rect1.area()
23    );
24}
25

Enums

  • enums: a way of saying a value is one of a possible set of values
rust
1// define enum IpAddrKind
2enum IpAddrKind {
3    V4,
4    V6,
5}
6
7// instance of IpAddrKind
8let four = IpAddrKind::V4;
9let six = IpAddrKind::V6;
10

Option

Rust does not have nulls, but it does have an enum to encode the concept of a value being present or absent.

  • Option<T> defined by the standard library as:
rust
1enum Option<T> {
2    None,
3    Some(T),
4}
5

used in Rust for functions that may or may not return a result, allowing explicit handling of both scenarios through pattern matching (match)

Match

Match allows you to compare a value against a series of patterns and then execute code based on which pattern matches.

rust
1enum Coin {
2    Penny,
3    Nickel,
4    Dime,
5    Quarter,
6}
7
8fn value_in_cents(coin: Coin) -> u8 {
9    match coin {
10        Coin::Penny => 1,
11        Coin::Nickel => 5,
12        Coin::Dime => 10,
13        Coin::Quarter => 25,
14    }
15}
16

Option and Match

rust
1// Define a struct to represent a person
2struct Person {
3    name: String,
4    age: Option<u8>,  // Age can be Some(u8) or None
5}
6
7// Function to print a greeting message based on age
8fn greet(person: Person) {
9    match person.age {
10        Some(age) => println!("Hello, {}! You are {} years old.", person.name, age),
11        None => println!("Hello, {}! I don't know your age.", person.name),
12    }
13}
14
15fn main() {
16    // Create instances of Person
17    let person1 = Person {
18        name: String::from("Alice"),
19        age: Some(30),
20    };
21
22    let person2 = Person {
23        name: String::from("Bob"),
24        age: None,
25    };
26
27    // Call greet function with different persons
28    greet(person1);
29    greet(person2);
30}
31

Data Structures

CategoryTypes
SequencesVec, VecDeque, LinkedList
MapsHashMap, BTreeMap
SetsHashSet, BTreeSet
MiscBinaryHeap

Vectors

Vectors (Vec<T>) are dynamically sized, meaning they can grow or shrink at runtime as opposed to Tuples / Arrays.

  • vectors are either mutable or immutable
  • normal scope applies
rust
1// empty vector
2let mut v: Vec<i32> = Vec::new();
3// push into vector
4v.push(5);
5v.push(6);
6v.push(7);
7v.push(8);
8
9// get the thrid item with .get
10let third: Option<&i32> = v.get(2);
11match third {
12    Some(third) => println!("The third element is {third}"),
13    None => println!("There is no third element."),
14}
15
16// pre populated vector
17let mut x = vec![100, 32, 57];
18// iterating over vector
19for i in &mut x {
20    *i += 50;
21}
22println!("index 0 {}", x.get(0))
23

Vectors can store different types by using an enum:

rust
1enum SpreadsheetCell {
2    Int(i32),
3    Float(f64),
4    Text(String),
5}
6
7fn main() {
8    let mut row = vec![
9        // pre populate vector
10        SpreadsheetCell::Int(3),
11        SpreadsheetCell::Text(String::from("blue")),
12        SpreadsheetCell::Float(10.12),
13    ];
14
15    // Adding more values to the vector
16    row.push(SpreadsheetCell::Int(42));
17    row.push(SpreadsheetCell::Text(String::from("green")));
18    row.push(SpreadsheetCell::Float(7.89));
19
20    // Accessing and printing the values
21    for cell in &row {
22        match cell {
23            SpreadsheetCell::Int(value) => println!("Int: {}", value),
24            SpreadsheetCell::Float(value) => println!("Float: {}", value),
25            SpreadsheetCell::Text(value) => println!("Text: {}", value),
26        }
27    }
28}
29

HashMap

HashMap<K, V> stores a mapping of keys of type K to values of type V using a hashing function, which determines how it places these keys and values into memory.

rust
1use std::collections::HashMap; // import from std library
2
3let mut scores = HashMap::new();
4
5// save key, value pairs in hashmap
6scores.insert(String::from("Blue"), 10);
7scores.insert(String::from("Yellow"), 50);
8
9// overwriting values
10scores.insert(String::from("Blue"), 25); // "Blue" key's value is updated
11
12// accessing values
13let team_name = String::from("Blue");
14let score = scores.get(&team_name).copied().unwrap_or(0);
15
16// iterating
17for (key, value) in &scores {
18    println!("{key}: {value}");
19}
20
21

Error handling

errors categories: recoverable and unrecoverable errors.

  • unrecoverable errors are always symptoms of bugs, like trying to access a location beyond the end of an array, we want to immediately stop the program.
    • rust uses "panic!" macro that stops execution when the program encounters an unrecoverable error.
rust
1// calling the panic macro
2fn main() {
3    panic!("crash and burn");
4}
5// fuck around and find out
6fn main() {
7    let v = vec![1, 2, 3];
8    println!("{}", v[99]); // This will cause a panic
9}
10
  • recoverable error: like file not found error, we want to report the problem to the user and retry the operation.
    • rust has the type Result<T, E> for recoverable errors.
rust
1enum Result<T, E> {
2    Ok(T),
3    Err(E),
4}
5
6// example use
7use std::fs::File;
8use std::io::{self, Read};
9
10fn read_file(filename: &str) -> Result<String, io::Error> {
11    let mut file = File::open(filename)?;
12    let mut contents = String::new();
13    file.read_to_string(&mut contents)?;
14    Ok(contents)
15}
16
17fn main() {
18    match read_file("example.txt") {
19        Ok(contents) => println!("File contents: {}", contents),
20        Err(e) => eprintln!("Error reading file: {}", e),
21    }
22}
23

Organizing Larger Projects

organizing code into separate crates, modules, and packages becomes crucial for maintainability and readability

  • Crates: to separate Functionality, logically separate parts of your project into different crates, especially if they can be reused or tested independently.
    • crate = compilation unit in Rust
    • Cargo.toml file, you define dependencies and specify whether your package is a binary or library
  • Modules for Structuring: use modules to group related functionality together within a crate. Modules help manage namespaces and reduce the risk of naming conflicts.
rust
1mod module1 {
2    pub fn function1() {}
3}
4
5// Use the modules
6fn main() {
7    module1::function1();
8}
9
  • Dependency Management: Use Cargo.toml to manage dependencies across different crates. Dependencies can be specified at the crate level to control what parts of your project depend on which external libraries.
ini
1[package]
2name = "my_project"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7# dependencies listed here
8rand = "0.8.5"
9
10[[bin]]
11name = "my_binary"
12path = "src/main.rs"
13
14[[lib]]
15name = "my_library"
16path = "src/lib.rs"
17

example project structure:

text
my_project
├── Cargo.toml
└── src/
    ├── main.rs        # Root crate (binary)
    ├── lib.rs         # Library crate
    ├── module1.rs     # Module file for module1
    └── module2/
        ├── mod.rs     # Module file for module2
        └── submodule.rs  # Submodule file inside module2

usage:

rust
1// src/main.rs
2mod module1;
3mod module2;
4
5fn main() {
6    module1::function1();  // Accessing function from module1
7    module2::submodule::function_in_submodule();  // Accessing function from submodule in module2
8}
9

Further learning

Generics provide flexibility and reusability by allowing code to operate on multiple types. Traits define shared behavior, allowing different types to implement the same methods. Lifetimes ensure references are valid for as long as needed, preventing memory safety issues. Smart Pointers (Box, Rc, RefCell) offer advanced memory management capabilities, such as heap allocation, reference counting, and interior mutability. Patterns are a special syntax in Rust for matching against the structure of types, both complex and simple. Tests are Rust functions that verify that the non-test code is functioning in the expected manner.