Rust Book
The following is my subjective summary of the rust book
- for a more parctical learning checkout:
- rustlings for a commandline course
- rust-by-example for learning through exercises
Installation / Update
Install Rust, with rustup
, which manages Rust versions and associated tools:
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
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
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:
1[dependencies]
2rand = "0.8"
3
1# update dependencies
2cargo update
3
Variables
- all immutable by default
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
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
length | signed | unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
Floats
- all signed
- f32 & f64
- default type f64 because on modern CPUs roughly same speed as f32 but more precise.
Booleans
- size = 1 bite
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
- String -> create or modify strings
- &str (string slice) -> read only (immutable) more on references later
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.
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.
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
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
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
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
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.
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
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
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
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
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
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
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
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:
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.
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
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
Category | Types |
---|---|
Sequences | Vec, VecDeque, LinkedList |
Maps | HashMap, BTreeMap |
Sets | HashSet, BTreeSet |
Misc | BinaryHeap |
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
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:
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.
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.
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 has the type
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.
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.
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:
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:
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.