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
- Compile / run
- Cargo: system and package manager
- Variables
- Types
- Compound Types
- Functions
- Control Structure
- Ownership (managing computer memory)
- Structs, structuring related data
- Enums
- Data Structures
- Error handling
- Organizing Larger Projects
- Further learning
Install Rust, with rustup
, which manages Rust versions and associated tools:
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Follow the on-screen instructions to complete the installation.
# Update Rust
rustup update
# verify installation / check version
rustc --version
# create rust file
echo 'fn main() { println!("Hello, World!"); }' > hello.rs
# compile
rustc main.rs
# run
./main
Instead of saving the result of the build in the same directory as our code, Cargo stores it in the target/debug directory
# create a project
cargo new <project-name>
# build a project
cargo build
# build and run a project in one step
cargo run
# build a project without producing a binary to check for errors
cargo check
To add libraries (crates), update Cargo.toml:
[dependencies]
rand = "0.8"
# update dependencies
cargo update
- all immutable by default
let x = 5; // immutable
let mut x = 5; // mutable
const x = 5; // always immutable & typed
shadowing (reusing name)
- must be same type
- only works in same scope
let x = 5;
let x = x + 5;
println!({x}); // 10
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 |
- all signed
- f32 & f64
- default type f64 because on modern CPUs roughly same speed as f32 but more precise.
- size = 1 bite
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
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
- literals which use double quotes.
- all UTF-8
- String -> create or modify strings
- &str (string slice) -> read only (immutable) more on references later
let x: char = 'hello';
let x: &str = "hello";
Compound types are types that can group multiple values into one.
- grouping a variety of types
- fixed length: once declared, they cannot grow or shrink in size.
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}"); // 6.4
let five_hundert = tup.0
println!("value of index 0 is: {five_hundert}"); // 500
}
- elements of array must have same type.
- arrays in Rust have a fixed length.
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
let a: [i32; 5] = [1, 2, 3, 4, 5];
// Statements = intructions for actions that dont return a value.
let y = 6;
// Expression evaluate to a resultant value.
let y = {
let x = 3;
x + 1
};
- main function / entrypoint at top of file
- returning values
fn main() {
let result = sum(5, 10);
println!("The sum is: {result}");
}
fn sum(a: i32, b: i32) -> i32 { //don’t need to name return values, but we must declare their type after arrow ->
a + b // implicit return
// or
return a + b // explicit return
}
fn main() {
let condition = true;
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
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
// loop -> used to loop infintely until break
loop {
break;
}
// while / conditional loop -> until false
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
// for loop -> used for iterating
fn main() {
for number in (1..4).rev() { // rev method, to reverse the range
println!("{number}!");
}
println!("LIFTOFF!!!");
}
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 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.
let x: i32 = 10; // Allocated on the stack
let s = String::from("hello"); // Allocated on the heap
- 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.
// stack
let x = 5;
let y = x;
println!("x = {x}, y = {y}");
// heap
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
- variables are only accessable if the parrent is in scope
{ // s is not valid here, it’s not yet declared
let s = "hello"; // s is valid from this point forward
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
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
// reference example
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // & references s1
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize { // calculate_length borrows reference to s
s.len()
}
// mutable reference example
fn main() {
let mut s = String::from("hello"); // mut makes s mutable
change(&mut s); // &mut references to s and shows mutablility
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
similar to tuple: pieces of struct can be different types but in struct each piece of data has a name to clarify purpose
// define a struct
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
// to use struct create an instance of that struct by specifying values
fn main() {
let mut user1 = User { // entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};
user1.email = String::from("[email protected]"); // access specific value with dot-notation
// Creating Instances from Other Instances with Struct Update Syntax
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("[email protected]"),
sign_in_count: user1.sign_in_count,
};
}
// Function that returns a User instance
fn build_user(email: String, username: String) -> User {
User {
active: true,
username, // type is already defined in parameter
email,
sign_in_count: 1,
}
}
- structs w/o names
struct Color(i32, i32, i32);
struct Cursor(i32, i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let current_location = Cursor(0, 0, 0);
}
- functions inside structs
- impl stands for implementation aka. rust method
#[derive(Debug)] // Debug trait enables to print struct in a way we can see its value while we’re debugging
struct Rectangle {
width: u32,
height: u32,
}
// method definition
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
- enums: a way of saying a value is one of a possible set of values
// define enum IpAddrKind
enum IpAddrKind {
V4,
V6,
}
// instance of IpAddrKind
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
Rust does not have nulls, but it does have an enum to encode the concept of a value being present or absent.
- Option defined by the standard library as:
enum Option<T> {
None,
Some(T),
}
used in Rust for functions that may or may not return a result, allowing explicit handling of both scenarios through pattern matching (match)
Match allows you to compare a value against a series of patterns and then execute code based on which pattern matches.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
Option and Match
// Define a struct to represent a person
struct Person {
name: String,
age: Option<u8>, // Age can be Some(u8) or None
}
// Function to print a greeting message based on age
fn greet(person: Person) {
match person.age {
Some(age) => println!("Hello, {}! You are {} years old.", person.name, age),
None => println!("Hello, {}! I don't know your age.", person.name),
}
}
fn main() {
// Create instances of Person
let person1 = Person {
name: String::from("Alice"),
age: Some(30),
};
let person2 = Person {
name: String::from("Bob"),
age: None,
};
// Call greet function with different persons
greet(person1);
greet(person2);
}
Category | Types |
---|---|
Sequences | Vec, VecDeque, LinkedList |
Maps | HashMap, BTreeMap |
Sets | HashSet, BTreeSet |
Misc | BinaryHeap |
Vectors (Vec) 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
// empty vector
let mut v: Vec<i32> = Vec::new();
// push into vector
v.push(5);
v.push(6);
v.push(7);
v.push(8);
// get the thrid item with .get
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
// pre populated vector
let mut x = vec![100, 32, 57];
// iterating over vector
for i in &mut x {
*i += 50;
}
println!("index 0 {}", x.get(0))
Vectors can store different types by using an enum:
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
fn main() {
let mut row = vec![
// pre populate vector
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
// Adding more values to the vector
row.push(SpreadsheetCell::Int(42));
row.push(SpreadsheetCell::Text(String::from("green")));
row.push(SpreadsheetCell::Float(7.89));
// Accessing and printing the values
for cell in &row {
match cell {
SpreadsheetCell::Int(value) => println!("Int: {}", value),
SpreadsheetCell::Float(value) => println!("Float: {}", value),
SpreadsheetCell::Text(value) => println!("Text: {}", value),
}
}
}
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.
use std::collections::HashMap; // import from std library
let mut scores = HashMap::new();
// save key, value pairs in hashmap
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// overwriting values
scores.insert(String::from("Blue"), 25); // "Blue" key's value is updated
// accessing values
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
// iterating
for (key, value) in &scores {
println!("{key}: {value}");
}
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.
// calling the panic macro
fn main() {
panic!("crash and burn");
}
// fuck around and find out
fn main() {
let v = vec![1, 2, 3];
println!("{}", v[99]); // This will cause a panic
}
- 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.
enum Result<T, E> {
Ok(T),
Err(E),
}
// example use
use std::fs::File;
use std::io::{self, Read};
fn read_file(filename: &str) -> Result<String, io::Error> {
let mut file = File::open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_file("example.txt") {
Ok(contents) => println!("File contents: {}", contents),
Err(e) => eprintln!("Error reading file: {}", e),
}
}
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.
mod module1 {
pub fn function1() {}
}
// Use the modules
fn main() {
module1::function1();
}
- 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.
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
# dependencies listed here
rand = "0.8.5"
[[bin]]
name = "my_binary"
path = "src/main.rs"
[[lib]]
name = "my_library"
path = "src/lib.rs"
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:
// src/main.rs
mod module1;
mod module2;
fn main() {
module1::function1(); // Accessing function from module1
module2::submodule::function_in_submodule(); // Accessing function from submodule in module2
}
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.