Skip to content

Instantly share code, notes, and snippets.

@msuganthan
Last active February 23, 2025 16:30
Show Gist options
  • Select an option

  • Save msuganthan/85e3d663d65a835f4bc37d2138c3ad2e to your computer and use it in GitHub Desktop.

Select an option

Save msuganthan/85e3d663d65a835f4bc37d2138c3ad2e to your computer and use it in GitHub Desktop.
Rust notes

Ownership

  • Set of rules that govern memory management
  • Rules are enforced at compile time
  • Any violation would result in compile time error.

Three rules

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

Copy vs Move

  • Scalar values with fixed sizes will automatically get copied in the stack, copying here is cheap.
  • Dynamically sized data won't get copied, but moved copying would be too expensive.

Perventing Issues

  • Ownership prevents memory safety issues:
    • Dangling pointer
    • Double-free
      • Trying to free memory that has already been freed
    • Memory Leaks
      • Not freeing memory that should have been freed.

Borrowing

  • Way of temporarily access data without taking ownership of it.
  • When borrowing, you're taking a reference(pointer) to that data, not the data itself.
  • Prevention of dangling pointers and data races
  • Data can be borrowed immutabily and mutably
  • There are certain rules when borrowing which we have to comply with, otherwise the program won't compile.

Rules of References

  • At any given time, you can have either one mutable reference or any number of immutable references(but not both)
  • References must always be valid

String vs &str(String slice)

  • A String is a heap-allocated string type that owns it content and is mutable
  • A &str is an immutable sequence of UTF-8 bytes in memory, it does not own the underlying data and is immutable
  • Think of &str as a view on a sequence of characters(stored as UTF-8 bytes) in memory.
  • Use &str if you just want to a view of a string
  • &str is more lightweight and efficient than String
  • Use String if you need to own the data and be able to mutate it

String literal

  • A String literal is a sequence of characters enclosed in double quotes
  • Fixed size, compile-time known sequence of UTF-8 bytes
  • The types is & static str, which indicates the data is stored in static storage, meaning it is valid throughout the entire lifetime of the program
  • The data is hardcoded into the executable and stored in read-only memory, meaning they are immutable

Array

  • Fixed-size collection of elements of the same data type stored as contigous block in stack memory
  • Signature of array is [T, length] which indicates the length is fixed at compile time
  • Arrays can neither grow nor shrink, they must retain their size.

Slice

  • Reference to contiguous sequence of elements in a collection
  • Provide a way to borrow part of a collection without taking ownership of the entire collection
  • Can be created from arrays, vectors, Strings, and other collections implementing the Deref trait

Tuple

  • Way to store related pieces of information in a single variable
  • Collection of values of different types grouped together as a single compound value(type composed of other types)
  • Stored as a fixed-size contiguous block of memory on the stack
  • Signature is (T, T, T,...)

Struct

  • Compound type allowing to group together values of different types into a named data structure
  • Similar to tuples, but each value has a name so values can be accessed through this name.
  • Have to be instantiated with data, think of it like the struct is the template for the instances you create from it.

Tuple Structs

  • Like normal structs but using tuple-like syntax for defining their fields
  • Basically a named tuple
  • Instantiated by paranthesis instead of curly braces
  • Accessed through point notation

Unit-Like Structs

  • Structs without any fields
  • Used when working with traits.
  • Doesn't store any data.

The Option Enum

  • Option is an enum that represents a value that may or may not be present
  • Known in other languages as null, refering to the absence of a value
  • Used to handle class where a function or method might fail to return a value.

Traits

  • Set of methods that can be implemented for mulitple types in order to provide common functionality and behavior between them.
  • Traits consist of method signatures only, which then have to be implemented by the target type.
  • Similar to "classes" in other languages, not quite the same though
  • Defines shared behavior in an abstract way.

Derivable Traits

  • Trait that can be automatically implemented for a struct or an enum by the Rust compiler
  • Called "derivable" because they can be derived automatically
  • Most common derivable traits:
  • Debug: Allowing to out content via "{:?}"
  • Clone: Enables type to be duplicated with "clone()" method
  • Copy: Enables type to copied implicity, without requiring explicit clone() method.
  • PartialEq: Enables comparison

Traits as Parameters

  • Traits can be used as parameters for functions.
  • The function notify() takes as argument any type that has implemented the Summary trait
pub fn notify(item: &impl Summary) {
 println!("Breaking news! {} ", item.summarize());
}

Trait Bounds

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

Similar to example using "impl Summary" but more verbose. Trait bounds are declared like generics, after name of the function. Use trait boundsif you have lots of parameters to avoid this:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

pub fn notify<T: Summary>(item1: &T, item2: &T) {

Where Clauses

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

If you have a function that makes heavy use of trait bounds, we can use a where clause to make the code cleaner:

fn some_function<T, U>(t: &T, u: &U) -> i32 where 
 T: Display + Clone,
 U: Clone + Debug,
 {

Trait Objects

  • Using impl Trait doesn't work when returning mulitple types
  • Different implementations of the traits probably use different amount of memory, but sizes of types must be known at compile time.
  • In this case, trait objects can be used
  • A trait object is essentially a pointer to any type that implements the given trait, where the precise types can only be known at runtime.

Dynamic Trait Objects

trait Animal { }

struct Dog;
struct Cat;

impl Animal for Dog {}
impl Animal for Cat {}

fn return_animal(s: &str) -> &dyn Animal {
 match s {
  "dog" => &Dog {},
  "cat" => &Cat {},
  _ => panic!(),
 }
}

fn main() {
 let animal1 = return_animal("cat");
 let animal2 = return_animal("dog");
}

Here we have a function which returns a type that implements Animal trait. This coule be dog or cat, As the trait object is behind a pointer, the size is known at compile time, which is usize(size of the pointer).

This allows for more flexible code as the exact return type doesn;t have to known at the compile time as long as the size is fixed.

Static Dispatch

  • Resolves method calls at compile time
  • Compiler generates function code for each contrete type that implements trait
  • Calls appropriate function based on concrete types
  • Faster and more efficient than dynamic dispatch, but doesn't provide great flexibility.
trait Animal {
 fn say_hi(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
 fn say_hi(&self) {
  println!("Wolf");
 }
}

impl Animal for Cat {
 fn say_hi(&self) {
  println!("Meow");
 }
}

fn main() {
 let dog = Dog;
 let cat = Cat;
 
 dog.say_hi();
 cat.say_hi();
}

The compiler generates methods for each concrete type and the called method say_hi() can be resolved because it is known at compile time which method for type has to be resolved.

Dynamic Dispatch

  • Specific methods to be called is determined at runtime
  • Works by creating a reference or smart pointer to a trait object using &dyn or Box<dyn >
  • When trait object is created, compiler will build a vtable for that trait
  • vtable is a table that contains a pointer to the implementation of each method in the trait for the specific type of the object that the reference points to.
  • Compiler will do a lookup in a vtable to determine which method should be called for which type that implements that given trait.
  • This lookup will cause overhead but allows for more flexible code.

Box

  • Small pointer that allows to store data on the heap rather than the stack
  • Use Box when ou have atype whose size can't be known at compile time
  • Returns a pointer to the data stored on the heap

& vs Box

  • Memory: Box allocates data on heap and owns it, also responsible for deallocation when value goes out of scope, reference only points to value already in memory
  • Lifetime: Box can be possed across scopes, reference has limited lifetime
  • Box can be close, reference not
  • Box can be used in pattern matching

Associated type

  • Allow to specify a type that is associated with the trait
  • When implementing the trait for a specific type we have to specify the concrete type
  • Basically a type placeholder that the trait methods can use in their signature
  • Similar to generic type but are more flexible because they allow a trait to have different associated types for different implementing types
trait MyTrait {
 type MyType;
 
 fn get_my_type(&self) -> Self::MyType;
}

Here we define a trait they has an assocaited type and a method that returns a value of this type.

struct MyStruct {}

impl MyTrait for MyStruct {
 type MyType = i32;
 
 fn get_my_type(&self) -> Self::MyType {
  return 42;
 }
}

When implementing the trait for a specific type(MyStruct), then we have to give the associated type MyType a concrete type, in this case i32

Vectors

  • Like arrays but dynamically sized, meaning can grow and shrink
  • Allocated on the heap as contiguous block of memory
  • All elements must have the same type
  • Special macro: vec!

HashMaps

  • Data structure to store key-value pairs
  • Allocated on the heap as it is dynamically sized, can grow and shrink
  • Allows for efficient lookup, insertion and deletion of data
  • Each key is hashed to a unique index in underlying array.

Type Coercion - as

  • Type conversion also called type casting is coercing primitive types that can be performed by as keyword
  • as conversions can be chained
  • When casting to an unsigned type T, T::MAX + 1 is added or subtracted until the value fits into the new type
  • Use unsafe methods can lead to undefined behavior

From/Into Conversion

  • From and Into traits are used for type conversions between different types without requiring explicit casts
  • Part of standard library
  • Can be implemented for custom types
  • Implementing From for a type will give us Into Implementation for the given type for free

panic!

  • Simplest form of error handling is to use the panic! macro
  • panic! will print out the error message, unwind the stack and finally exit the program
  • In multi-threaded programs it will exit the thread in which the panic! occurs, not the whole program

Result

  • Result is an enum type that represents the outcode of an operation that could potentially fail
  • Two possible variants:
    • Ok(T): A value T was found
    • Err(e) An error was found with a value e
  • The expected outcode is Ok, the unexpected outcome is Err
  • Since Result is an enum, the possible variants can be matched using a match pattern

unwrap

  • The unwrap() method takes as input a value of type Result and takes out the value which is wrapped inside Ok(T) in case of success or panics in case of an error.

?

  • The ? operator is a shorthand way to propagate errors or unwrap Ok() results
  • Basically the same as unwrap() but instead of panic returns an error
  • Replaces an entire match statement
  • Can be used in the main() function

Cargo

  • Official package manager and building tool
  • Helps automate tasks such as creating new projects, building, running, testing code and managing dependencies
  • Crate is a compilation unit of Rust source code
  • crates.io repository for Rust packages

Three Rules of Lifetime Elision

  • Compiler assigns a lifetime parameter to each parameter that's a reference
  • If there is exactly one input lifetime parameter that lifetime is assigned to al output lifetime parameters
  • If there are multiple lifetime parameters but one of them is &self or &mut self, the lifetime of self is assigned to all output lifetime parameters

Static Lifetime

  • Refers to a lifetime that lasts for the entire duration of the program's execution
  • Any reference or borrowed value with static lifetime can be safely used throughout the program
  • Can be coerced to a shorter lifetime if needed

Closures

  • Anonymous functions that are able to capture values from the scope in which they are defined
  • Can be defined inline
  • Don't require type annotations
  • Can take ownership of a value by using move keyword.

Fn Traits

  • Trait that defines signatures for closures/functions
  • Describes types, number of arguments and return type
  • Three different traits:
  • FnOnce
    • Closure that can be called once
    • Take ownership of captured values
  • FnMut
    • Might mutate captured values
    • Can be called more than once
  • Fn
    • Doesn't take ownership of captured values
    • Doesn't mutate anything
    • Might not even capture anything from its environment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment