Skip to content

Condensed summary of key rust concepts from rust book in markdown to help getting started with rust

Notifications You must be signed in to change notification settings

matsjfunke/rust-minimal-docs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 

Repository files navigation

The following is my subjective summary of the rust book

  • for a more parctical learning checkout:

Table of Contents

  1. Installation / Update
  2. Compile / run
  3. Cargo: system and package manager
  4. Variables
  5. Types
  6. Compound Types
  7. Functions
  8. Control Structure
  9. Ownership (managing computer memory)
  10. Structs, structuring related data
  11. Enums
  12. Data Structures
  13. Error handling
  14. Organizing Larger Projects
  15. Further learning

Installation / Update

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

Compile / run

# create rust file
echo 'fn main() { println!("Hello, World!"); }' > hello.rs
# compile
rustc main.rs
# run
./main

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

# 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

Variables

  • 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

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
fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

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
  1. String -> create or modify strings
  2. &str (string slice) -> read only (immutable) more on references later
let x: char = 'hello';
let x: &str = "hello";

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.
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
}

Array []

  • 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];

Functions

 // 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

}

Control Structure

if, else

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");
    }
}

loops

// 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 (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.
let x: i32 = 10; // Allocated on the stack
let s = String::from("hello"); // Allocated on the heap

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

// 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}");

Variable Scope

  • 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

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

// 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");
}

Structs

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,
    }
}

Tuple Structs

  • 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);
}

Struct Methods

  • 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

  • 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;

Option

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

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);
}

Data Structures

Category Types
Sequences Vec, VecDeque, LinkedList
Maps HashMap, BTreeMap
Sets HashSet, BTreeSet
Misc BinaryHeap

Vectors

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

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}");
}

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.
// 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 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.
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
}

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.

About

Condensed summary of key rust concepts from rust book in markdown to help getting started with rust

Topics

Resources

Stars

Watchers

Forks

Languages