- 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.eprintln!("{:#?}")
#[derive(Debug)]
- 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
- 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
-
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.elet guess: i32 = match guess.trim().parse()
- Rust does not care where you define the order of your function
- Can return a value earlier using
return
keyword
- 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
- 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;
}
};
-
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.
-
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!
- 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
- 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 withmut
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);
-
methods are defined within the context of a
struct
(or an enum or a trait object) and their first parameter is alwaysself
, 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 multipleimpl
blocks -
any methods without
&self
as first parameter will behave same withstatic 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
- 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, asmatch
expression will be evaluated in order
- 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
orsrc/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 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 permodule
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--
}
- vec! macro
- vectors should have the same data type for all items
vec.insert
is an equivalent ofreplace
if key-value already exists
- 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 withprintln!
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.
- 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 apanic!
call - rustc --explain E0308
unwrap
andexpect
methods are very handy when prototyping,
- no runtime/performance cost with the use of monomorphization
- can mixup generics parmater in a single
impl
orstruct
- 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 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
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
// usage
impl Summary for NewsArticle {}
- 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
- 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
-
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 ;
- how closure handles references or values
- Iterator adaptors are methods defined on the Iterator trait that don’t consume the iterator
-
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 asderef coercion
-
Without the
Deref
trait, the compiler can only dereference&
references -
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 asdestructor
in other language -
To
Drop
values manually, we can callstd::mem::drop
-
Rc<T>
orReference 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 ofRc::clone(a)
as this is Rust's convention -
RefCell<T>.borrow()
returnsRef<T>
andRefCell<T>.borrow_mut()
returnsRefMut<T>
-
RefCell<T>
keeps track of how manyRef<T>
andRefMut<T>
smart pointers are currently active -
RefCell<T>
lets us have many immutable borrows or one mutable borrow at any point in time
- 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
- 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>;
, theBox<Self>
syntax means the method is only valid when called on a Box holding the type. This syntax takes ownership ofBox<Self>
, invalidating the old state so the state value of the Post can transform into a new state
- it’s possible to mix and match
if let
,else if
, andelse if let
expressions if let Ok(age)
can use shadow variablewhile 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;
orlet (x, y, z) = (1, 2, 3);
- Function parameters can also be patterns - &(x, y): &(i32, i32)
match
arms must use refutable patternsmatch
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
- 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 structDog::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)