Rust: The Complete Developer's Guide by Stephen Grider
- 01-foundations/deck
- section 1 and 2
- 03-rust-memory-system
- bank
- section 3 and 4
- comparison-js-rust: comparing javascript and rust memory system
- bank
- 05-enums/media
- section 5 and 6
- 07-errors-results/logs
- 08-iterator/iter
- 09-lifetimes/lifetimes
- 10-generics-traits
- generics
- first part of section 10
- traits
- second part of section 10
- generics
Click to Contract/Expend
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo --version
# cargo 1.86.0 (adf9b6ad1 2025-02-28)
rustc --version
# rustc 1.86.0 (05f9846f8 2025-03-31)cargo new <project name>cargo runs
mkdir 01-foundations
cd 01-foundations
cargo new deckcd deck
cargo run
cargo run -q # without debugging messagesstruct= classletis not actually variable, but binding. Binding is immutable
// good debugging code with formatting
println!("Heres your deck: {:#?}", deck);crate= package
- Included with every project without any additional install
- Docs at
https://doc.rust-lang.org/stable/std/
- Have to be install into our project with
cargo add <crate name> - Crate listing at
https://crates.io/ - Docs also at
https://docs.rs/
# 01-foundations/deck
cargo add randmkdir 03-rust-memory-system
cd 03-rust-memory-system
cargo new bank
cd bankfn main() {
let account = Account::new(1, String::from("Noah"));
print_account(account);
print_account(account); // it gets the mysterious error
}-
Every value is 'owned' by a single variable, struct, vector, etc at a time
-
Reassigning the value to another variable, passing it to a function, putting it into a vector, etc, moves the value. The old variable can't be used anymore!
-
You can create many read-only references to a value that exist at the same time
-
You can't move a value while a ref to the value exists
-
You can make a writeable (mutable) reference to a value only if there are no read-only references currently in use. One mutable ref to a value can exist at a time
-
You can't mutate a value through the owner when any ref (mutable or immutable) to the value exists
-
Some types of values are copied instead of moved (numbers, bools, chars, arrays/tuples with copyable elements) - break rules of ownership (=it works as similar as usual programming languages)
-
When a variable goes out of scope, the value owned by it is dropped (cleaned up in memory)
-
Values can't be dropped if there are still active references to it
-
References to a value can't outlive the value they refer to
- These rules will dramatically change how you write code (compared to other languages)
12. When in doubt, remember that Rust wants to minimize unexpected updates to data
- Lesson 1. make the engine read-only
- Lesson 2. each objects has their own properties
Above all, Rust wants to avoid 'unexpected updates'
mkdir 05-enums
cd 05-enums
cargo new media
cd media- Rust doesn't have
null,nil, orundefined - Instead, we get a built-in enum called
Option - Has two variants -
SomeandNone - if you want to work with
Optionyou have to use
pattern matching (theif letthing) or a match statement - Forces you to handle the case in which you have a value
and the case in which you don't
enum Option {
Some(u32),
None,
}item.unwrap()- if
itemis a None, panics! - Use for quick debugging or examples
- if
item.expect("There should be a value here")- if
itemis a None, prints the provided debug message and panics! - Use When we want to crash if there is no value
- if
item.unwrap_or(&placeholder)- if
itemis a None, returns the provided default value - Use When it makes sense to provide a fallback value
- if
- most appropriate when you have a really large file with a lot of stuff going on
- Most appropriate when you want to separate module to organise code,
but it doesn't need to span several files
- most appropriate when you have a large module
- Has a couple of confusing parts
pub: exportmod: importsuper: parent
mkdir 07-errors-results
cd 07-errors-results
cargo new logs
cd logsenum Result {
Ok(value),
Err(error)
}fn device(a: f64, b: f64) -> Result<f64, Error>
enum Result <T, E> {
Ok(T),
Err(E)
}// empty tuple
Ok(())- Stack
- Fast, but limited size (2-8MB)
- Heap
- Slow, but can grow to store a lot of data
- Data: (called Data Segment or Rodata Secment or Static Segment)
- Stores literal values that we write into our code
Stackstores metadata about a datastructureHeapstores the actual data- Avoids running out of memory in the stack if the datastructure grows to hold a lot of data
let nums = vec![1, 2, 3, 4, 5]- If a data structure owns another data structure, the child's metadata will be placed on the heap
let vec_of_numbers = vec![
vec![1, 2, 3, 4, 5]
]-
String- Use anytime we want ownership of text
- Use anytime we want text that can grow or shrink
-
&String: String reference- Rarely used!
- Rust will automatically turn
&Stringinto&strfor you
-
&str: String slice- Use anytime you don't want to take ownership of text
- Use anytime you want to refer to a
portionof a string owned by something else
let color = String::from("red"); let c = color.as_str();
-
Reason #1:
&strlets you refer to text in thedata segmentwithout aheapallocation-
case 1:
- slightly better performance.
let color = "red";
-
case 2:
"String::from("red").as_str()let color = String::from("red"); let color_ref = &color;
-
-
Reason #2:
&strlets youslice(take a portion) of text that is already on the heaplet color = String::from("blue"); let portion = &color[1..4]; // "lue"
- without
&str: there are extra allocations involved. (not good in performance)
let color = String::from("blue"); let portion = String::from( color.chars().skip(1).collect::<String>(); ); let portion_ref = &portion;
- without
Summary
| Name | When to use | Use memory in... | Notes |
|---|---|---|---|
String |
When you want to take ownership of text data. When you have a string that might grow or shrink |
Stack and Heap | |
&String |
Usually never | Stack | Rust automatically turns String into a &str for you |
&str |
When you want to read all or a portion of some text owned by something else | Stack | Refers directly to heap-allocated or data-allocated text |
text = "how are you"
word_list = text.split(" ")
# stores "how" , "are", "you"let text = "how are you"
let split_text = text.split(" ");
// stores &str, &str, &str`text_that_was_read` does not live long enough?: Try Operator
fn main() -> Result<(), Error> {
let text = fs::read_to_string("logs.txt")?;
}- Use a
matchorif letstatement- When you're ready to meaningfully deal with an error
- example.rs
- Call
unwrap()orexpect("why this paniced")on the Result- Quick debugging, or if you wawnt to crash on an Err()
- Use the try operator(
?) to unwrap or propagate the Result- When you don't have any way to handle the error in the current function
- example.rs
mkdir 08-iterator
cd 08-iterator
cargo new iter
cd iter| Name | Description |
|---|---|
| shorten_strings() | 90-iter_mut.rs |
| move_elements() | 94-into_iter.rs |
| print_element() | 88-vector-slices.rs |
| to_uppercase() | 92-collect.rs |
| explode() | 95-inner-maps.rs |
| find_color_or() | 97-find-map_or.rs |
iterator is lazy. Nothing happens until...
- A) You call
next() - B) You use a function that called
next()automaticallyiterator consumerssuch asfor_each(),collect(), etc
However, map() is not consumer but iterator adaptor
and it doesn't call next() automatically
elements.iter().map(|el| format!("{} {}", el, el)); // error// expect full vector
fn print_elements(elements: &Vec<String>) {}
// expect slice (full vector or part of vector)
fn print_elements(elements: &[String]) {}
fn main() {
let colors = vec![
String::from("red"),
String::from("green"),
String::from("blue"),
];
print_elements(&colors[1..3]);
// print_elements(&colors);
}iter: read-only referenceiter_mut(): mutable referenceinto_iter(): ownership, unless called on a mutable ref to a vector
- collect decides the return type automatically following examples
// 1. collect() will follow the return type of this function
fn to_uppercase(elements: &[String]) -> Vec<String> {
elements.iter().map(|el| el.to_uppercase()).collect()
}
// 2. collect() will follow the type specified for uppercased
fn to_uppercase(elements: &[String]) -> Vec<String> {
let uppercased: Vec<String> = elements.iter().map(|el| el.to_uppercase()).collect();
uppercased
}
// 3. Turbofish, and Stephen's preferred way. it's obvious next to collect()
fn to_uppercase(elements: &[String]) -> Vec<String> {
elements
.iter()
.map(|el| el.to_uppercase())
.collect::<Vec<String>>()
}Vec<String> can be used like Vec<_> as collect() knows the the return type in the previous chain
fn to_uppercase(elements: &[String]) -> Vec<String> {
let uppercased: Vec<_> = elements.iter().map(|el| el.to_uppercase()).collect();
uppercased
}
fn to_uppercase(elements: &[String]) -> Vec<String> {
elements
.iter()
.map(|el| el.to_uppercase())
.collect::<Vec<_>>()
}mkdir 09-lifetimes
cd 09-lifetimes
cargo new lifetimes
cd lifetimesstruct Account {
balance: i32
}
struct Bank<'a> {
primary_account: &'a Account
}fn longest<'a>(str_a: &'a str, str_b: &'a str) -> &'a str {
if str_a.len() > str_b.len() {
str_a
} else {
str_b
}
}- when there are more than two ref arguments, Rust will assume the return would be one of the arguments
- Rust will not analyse the body of your function to figure out whether the return ref is pointing at the first or second arg
fn next_language(languages: &[String], current: &str) -> &str {}- To clarify which ref the return ref is pointing at, we have to add lifetime annotations
ain'ais just a identifier, so it can be'LifetimeAnnotation, but in developer convention, it usually be'a
fn next_language<'a>(languages: &'a [String], current: &str) -> &'a str {}You can omit annotations in two scenarios.
-
Function that takes one ref + any number of values + return a ref
fn last_language(languages: &[String]) -> &str fn generate(set: &[i32], range: i32) -> &str fn leave(message: &Message, text: String) -> &str
-
Method that takes
&selfand any number of other refs + returns a ref. Rust assumes the returned ref will point at&selfstruct Bank { name: String } impl Bank { fn get_name(&self, default_name: &str) -> &str { &self.name } }
mkdir 10-generics-traits
cd 10-generics-traits
cargo new generics
cd genericscargo add num-traitsA trait is a set of methods
- it can contain abstract methods which don't have an implementation
- it can contain default methods, which have an implementation
trait Vehicle {
fn start(&self);
fn stop(&self) {
println!("Stopped");
}
}A struct/enum/primitive can implement a trait
- The implementor has to provide an implementation for all of the abstract methods
- The implmentor can optionally override the default methods
struct Car {};
impl Vehicle for Car {
fn start(&self) {
println!("Start!!!");
}
}Type T must be something that implements the Vehicle trait
fn start_and_stop<T: Vehicle>(vehicle: T) {
vehicle.start();
vehicle.stop();
}
fn main() {
let car = Car {};
start_and_stop(car);
}cargo new traits
cd traits