Skip to content

Instantly share code, notes, and snippets.

@kulapoo
Last active April 29, 2023 08:50
Show Gist options
  • Save kulapoo/bb0554983c448b8a3f81c793f833650a to your computer and use it in GitHub Desktop.
Save kulapoo/bb0554983c448b8a3f81c793f833650a to your computer and use it in GitHub Desktop.
My Official Rust Book Summary

General

  • rustup = nvm
  • Cargo = npm
  • **creates.io"" = npm site
  • mut means mutalble
  • & = can access pieace of data without needing to copy that data into memory multiple times (&mut for mutable reference)
  • An arm has two parts: a pattern and some code
  • monomorphization in rust????
  • {:#?} is for debugging along with println! macro i.e println!("{:#?}")

Attributes

  • #[derive(Debug)]

Prelude


Constants

  • can be declared in any scope, including the global scope
  • declare constants using the const keyword instead of the let keyword, and the type of the value must be annotated
  • may be set only to a constant expression, not the result of a function call or any other value that could only be computed at runtime.
  • valid for the entire time a program runs, within the scope they were declared in

Shadowing

  • can repeat same variable name with the use of let keyword
  • can cast variable, which can saves us to come up on diff name (amount_str, amount_int)
  • cant mutate variable type

Data Types

  • there are mathematical operations available only for floating point types

  • has subsets of scalar (boolean, numbers, characters) and compound (array and tuples)

  • floating point default type is f64

  • char type represents unicode value

  • Array has fixed lenght unlike with other language

  • Array is useful when u want your data allocated on the stack rather than the heap

  • tuple - The tuple without any values has a special name, unit. This value and its corresponding type are both written () and represent an empty

  • parse can cast value to diff type i.e let guess: i32 = match guess.trim().parse()


Functions

  • Rust does not care where you define the order of your function
  • Can return a value earlier using return keyword

Expression and Statements

  • expressions always returns value, statement does not return a value
  • function, macro, and {} is an expression
  • expression do not include ending semicolons
  • expression cannot return different types when using with if expression

Control flow

  • if else, loop, for in, while loop
  • break inside loop can return value out of the loop i.e -> break counter + 2
  • loop_labels
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
  • expression examples with control flow
// if
let number = if condition { 5 } else { 6 };


// loop
let mut counter = 0;

let result = loop {
    counter += 1;

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

Ownership

chapter 4.1

  • 3 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.
  • Passing a variable to a function will move or copy, just as assignment does

  • heap is dynamic, stack fixed size and data type "last in, first out."

  • Rust calls drop (freeing the memory from stack or heap) automatically at the closing curly bracket.

  • move - ignoring previous assigned variables in favor of latest.

     let x = String::from("hello");
     let y = x
    
    
    println!("{}", x) // throws error
  • Returning values can also transfer ownership

  • When a variable that includes data on the heap goes out of scope, the value will be cleaned up by drop unless ownership of the data has been moved to another variable

  • Rust will never automatically create "deep" copies of your data.

References and Borrowing

chapter 4.2

  • A reference is like a pointer in that it’s an address we can follow to access the data stored at that address; that data is owned by some other variable.

  • References are immutable by default just as variables

  • Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference

  • ampersands represent references, and they allow you to refer to some value without taking ownership of it.

  • borrowing a term in rust for creating a reference

  • curly braces allow multiple mutable references

    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
  • Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;  // throws error, the first mutable borrow is in r1 and must last until it’s used in the println! This relates to "Data race"

    println!("{}, {}", r1, r2);
  • A data race - is similar to a race condition and cause undefined behavior and can be difficult to diagnose and fix when you’re trying to track them down at runtime
    this happens when these three behaviors occur:

    • Two or more pointers access the same data at the same time.
    • At least one of the pointers is being used to write to the data.
    • There’s no mechanism being used to synchronize access to the data.

  • At any given time, you can have either one mutable reference or any number of immutable references.
  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);

  • work around on having both mutable reference and immutable references.
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);

dangle reference

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

The Slice Type

chapter 4.3

  • slices - A string slice is a reference to part of a String
    let s = String::from("hello world");
    let five = 5;
    let hello = &s[..five];
    let worl_slice = &s[five..10];
    let world = &s[6..11];

    println!("{}, {}", hello, world);
    println!("{}", worl_slice);
  • The type that signifies “string slice” is written as &str
  • &str is an immutable reference
  • String slice range indices must occur at valid UTF-8 character boundaries. Otherwise will throw error

Struct

chapter 5.1

  • We can use structs to add meaning by labeling the data
  • types of struct
    • object struct
    • tuple struct - ie struct WriteMessage(String);
    • unit struct - struct without definition
  • field init - avoid field repetition
  • changing at least one field value must be mutable or with mut keyword in its declaration
  • The &self is actually short for self: &Self. Within an impl block, the type Self is an alias for the type that the impl block is for
  • struct update syntax - create a new instance of a struct that includes most of the values from another instance, but changes some (like js Object.assign(oldObject, newObject))
  • struct update syntax uses = like an assignment and moves the data
  • the base struct or ..variable must always be the last field i.e
    let p1 = Person {name: String::from("John"), age: None };
    let p2 = Person {name: String::from("Tae"), ..p1 };
p1.distance(&p2);
(&p1).distance(&p2);

Method syntax

chapter 5.3

  • methods are defined within the context of a struct (or an enum or a trait object) and their first parameter is always self, which represents the instance of the struct the method is being called on

  • &self is actually short for self: &Self

  • Each struct is allowed to have multiple impl blocks

  • any methods without &self as first parameter will behave same with static function in other language

  • Can choose to give a method the same name as one of the struct’s fields

  • Methods let you specify the behavior that instances of your structs have


Enums

chapter 6

  • Can define any data type inside an enum variant: strings, numeric types, structs or another enum
	enum Shape {
		Triangle { base: f64, height: f64 },
		Square { side: f64 },
	}

	enum Message {
		Quit, // normal enum variant
		Move { x: i32, y: i32 }, // struct
		Write(String), // tuple
		ChangeColor(i32, i32, i32), // tuple
		Shape(Shape) // nested enum
	}
  • Catch-all Patterns and the _ Placeholder or can be the variable we are trying to match
  	let dice_roll = 9;
        match dice_roll {
            3 => add_fancy_hat(),
            7 => remove_fancy_hat(),
            other => move_player(other), // or _ if not going to use
        }
  • The " _" pattern will match any value. By putting it after our other arms, the "_" will match all the possible cases that aren’t specified before it
  • It is important to put the catch all pattern last, as match expression will be evaluated in order

Packages and Crates

chapter 7.1

  • A crate can come in two form binary or library
  • The crate root is a source file that the Rust compiler starts from and makes up the root module of your crate i.e src/lib.rs or src/main.rs
  • A package is a bundle of one or more crates that provides a set of functionality
  • A package can have multiple binary crates by placing files in the src/bin directory

Modules

chapter 7.2

  • Modules can also hold definitions for other items, such as structs, enums, constants, traits, and functions
  • Module cheatsheet
  • absolute path is the Rust general preference rather than relative because it’s more likely we’ll want to move code definitions and item calls independently of each other
  • All items (functions, methods, structs, enums, modules, and constants) are private to parent modules by default. If you want to make an item like a function or struct private, you put it in a module
  • Items in a parent module can’t use the private items inside child modules, but items in child modules can use the items in their ancestor modules
  • only enums variants / fields are public by default
  • use keyword will only take effect per module definition
  • When bringing in structs, enums, and other items with use, it’s idiomatic/recommended to specify the full path
  • Specifying the parent module when calling the function makes it clear that the function isn’t locally defined while still minimizing repetition of the full path i.e
    use std::fmt;
    use std::io;

    fn function1() -> fmt::Result {
        // --snip--
    }

    fn function2() -> io::Result<()> {
        // --snip--
    }
  • Providing New Names with the as Keyword - example
  • Re-exporting Names with pub use - example

Vectors

chapter 8.1

  • vec! macro
  • vectors should have the same data type for all items
  • vec.insert is an equivalent of replace if key-value already exists

Strings

chapter 8.2

  • String and string slices are UTF-8 encoded
  • String::from("Hola") is total of 4 bytes, each of these letters takes 1 byte when encoded in UTF-8
  • format! macro is same with println! macro but returns value instead of writing to stdin
  • better use format! macro when concatinating multiple strings
  • Valid Unicode scalar values may be made up of more than 1 byte.
  • Concatenation will own the variable involved.
  • Use ranges to create string slices with caution, because doing so can crash your program (alternatively, find a create for this whenever necessary)
let hello = "Здравствуйте";

let s = &hello[0..1]; // this code will panic as some **Unicode scalar** values may be made up of more than 1 byte.

Error handling

chapter 9

  • Rust groups errors into two major categories: recoverable and unrecoverable errors
  • A backtrace is a list of all the functions that have been called to get to where error occured. Reading the backtrace is to start from the top and read until you see files you wrote
  • Choose expect rather than unwrap to give more context about why the operation is expected to always succeed
  • Rust doesn’t have exceptions. Instead, it has the type Result<T, E> for recoverable errors and the panic! macro that stops execution when the program encounters an unrecoverable error
  • When failure is expected, it’s more appropriate to return a Result than to make a panic! call
  • rustc --explain E0308
  • unwrap and expect methods are very handy when prototyping,

Generics

chapter 10.1

  • no runtime/performance cost with the use of monomorphization
  • can mixup generics parmater in a single impl or struct
  • can put a constraints into impl
// other instances of Point<T> where T is not of type f32
// will not have this method defined
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Traits

chapter 10.2

  • Traits are similar to a feature often called interfaces in other languages, although with some differences (mostly behaviors or methods)
  • Traits user must bring the trait into scope as well as the types
  • Traits method can have default implementation
  • Traits are not allowed to be
  • Trait default implementation trait-sample
  • Trait default implementation can call other traits methods
  • generic lifetime parameters - define the relationship between the references so the borrow checker can perform its analysis
  • Unit-like structs can be useful in situations in which you need to implement a trait on some type but don’t have any data that you want to store in the type itself
  • Trait bound can implement methods conditionally for types that implement the specified traits
  • Blanket implementations - Implementations of a trait on any type that satisfies the trait bounds
  • Clearer Trait Bounds with where Clauses

trait sample

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

Lifetimes

chapter 10.3

  • Every reference in rust has a lifetime , which is the scope for which that reference is valid
  • examples of lifetime referrence
    &i32        // a reference
    &'a i32     // a reference with an explicit lifetime
    &'a mut i32 // a mutable reference with an explicit lifetime
  • Borrow checker compares scopes to determine whether all borrows are valid
  • Lifetime annotations describe the relationships of the lifetimes of multiple references to each other without affecting the lifetimes, it don’t change how long any of the references live
  • We can define structs to hold references, but in that case we would need to add a lifetime annotation on every reference in the struct’s definition
  • borrow checker could infer some lifetimes from a function
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

// old rust needs to do this - fn first_word<'a>(s: &'a str) -> &'a str {
  • The patterns programmed into Rust’s analysis of references are called the lifetime elision rules
  • compiler lifetime elision rules
    • The first rule is that the compiler assigns a lifetime parameter to each parameter that’s a reference
    • The second rule is that, if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters: fn foo<'a>(x: &'a i32) -> &'a i32 .
    • if there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters

Writing Automated Tests

chapter 11

  • At its simplest, a test in Rust is a function that’s annotated with the test attribute
  • You can’t use the #\[should_panic\] annotation on tests that use Result<T,E>
  • #[should_panic(expected = "less than or equal to 100")] attribute can accept paramters
  • if we call println! in a test and the test passes, we won’t see the println! output in the terminal;
  • Running a Subset of Tests by Name
  • #\[cfg(test)\] - annotation on the tests module tells Rust to compile and run the test code only when you run cargo test, not when you run cargo build

Iterators and Closure (FP features)

chapter 13

  • Closures are anonymous functions you can save in a variable or pass as arguments to other functions

  • Closures can capture values from the scope in which they’re defined

  • Closures don’t usually require you to annotate the types of the parameters (optional)

  • Closures capturing values from env in three ways

    • borrowing immutably
    • borrowing mutably
    • taking ownership
  • Closure will decide which of these "captures" to use based on what the body of the function does with the captured values

  • | param1, param2 | - closure parameter list

  • syntax

    fn  add_one_v1   (x: u32) -> u32 { x + 1 } // this is function
    let add_one_v2 = |x: u32| -> u32 { x + 1 };
    let add_one_v3 = |x|             { x + 1 };
    let add_one_v4 = |x|               x + 1  ;


Smart Pointers

  • Smart pointer pattern is a general design pattern used frequently in Rust

  • Smart pointer does not have performance overhead

  • While references only borrow data, Smart pointers own the data they point to

  • Smart pointers are usually implemented using structs. Unlike an ordinary struct, smart pointers implement the Deref and Drop traits

  • Interior mutability, a pattern where an immutable type exposes an API for mutating an interior value

  • Rference cycles, can leak a memory

  • Box variables deallocation happens both box variable (stored on the stack) and the data it points to (stored on the heap).

  • Deref trait allows Box<T> values to be treated like references

  • Rust run this code *(y.deref()) behind the scenes whenever it does the *variable dereference. Also know as deref coercion

  • Without the Deref trait, the compiler can only dereference & references

  • Deref coercion

  • Deref coercion demo

// MyBox implemented the Deref trait

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m); // with deref coercion through rust compiler
    hello(&(*m)[..]); -> if without deref coercion
}
  • Deref coercion is resolved at compile time so there is no runtime penalty

  • Drop is same as destructor in other language

  • To Drop values manually, we can call std::mem::drop

  • Rc<T> or Reference counting, keeps track of the number of references to a value to determine whether or not the value is still in use. If there are zero references to a value, the value can be cleaned up without any references becoming invalid

  • use Rc::clone(&a) instead of Rc::clone(a) as this is Rust's convention

  • RefCell<T>.borrow() returns Ref<T> and RefCell<T>.borrow_mut() returns RefMut<T>

  • RefCell<T> keeps track of how many Ref<T> and RefMut<T> smart pointers are currently active

  • RefCell<T> lets us have many immutable borrows or one mutable borrow at any point in time


Fearless Concurrency

  • By leveraging ownership and type checking, many concurrency errors are compile-time errors in Rust
  • 3 common concurrency problems:
    • Race conditions, where threads are accessing data or resources in an inconsistent order
    • Deadlocks, where two threads are waiting for each other, preventing both threads from continuing
    • Bugs that happen only in certain situations and are hard to reproduce and fix reliably
  • Note that when the main thread of a Rust program completes, all spawned threads are shut down
  • main thread always wait for spawn thread
  • mpsc - responsible for making channels to different threads. many senders but only one receiver, stands for "multiple producer, single consumer"
  • recv or receiver from mpsc tuple 2nd parameter will block the main thread
  • Mutex is described as guarding the data it holds via the locking system
  • MutexGuard return from mutex.lock() is a smart pointer
  • Mutex implements deref trait to automatically release the lock

OOP

  • Trait object points to both an instance of a type implementing our specified trait and a table used to look up trait methods on that type at runtime
  • Trait object must use a pointer
  • The advantage of using trait objects and Rust’s type system to write code similar to code using duck typing is that we never have to check whether a value implements a particular method at runtime or worry about getting errors if a value doesn’t implement a method but we call it anyway. Rust won’t compile our code if the values don’t implement the traits that the trait objects need
  • fn request_review(self: Box<Self>) -> Box<dyn State>;, the Box<Self> syntax means the method is only valid when called on a Box holding the type. This syntax takes ownership of Box<Self>, invalidating the old state so the state value of the Post can transform into a new state

Patterns

  • it’s possible to mix and match if let, else if, and else if let expressions
  • if let Ok(age) can use shadow variable
  • while let conditional loop allows a while loop to run for as long as a pattern continues to match
  • Every time you've used a let statement like this you've been using patterns - let PATTERN = EXPRESSION; or let (x, y, z) = (1, 2, 3);
  • Function parameters can also be patterns - &(x, y): &(i32, i32)
  • match arms must use refutable patterns
  • match expression always start a new scope, variables declared as part of a pattern inside the match expression will shadow those with the same name outside the match construct, as is the case with all variables
  • Ignoring Remaining Parts of a Value with ..
  • Match guard is an additional if condition, specified after the pattern in a match arm, that must also match for that arm to be chosen
  • Match guard is not a pattern so it does not introduce a new variable
  • The at operator "@" lets us create a variable that holds a value at the same time as we’re testing that value for a pattern match

Advance Rust

  • Calling Methods with the Same Name
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
  • Same method name - Rust could figure out which implementation of a trait to use based on the type of self
  • <Dog as Animal>::baby_name() - This will call associated (without self param) method instead of struct Dog::baby_name
  • <Type as Trait>::function(receiver_if_method, next_arg, ...); - fully qualified syntax of any method in rust, can use anywhere.
  • Newtype patterns = tupleStruct(T)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment