<!-- <b style="color:#BE77FF"></b> <img src="" style="width:5%"/> > **File:** `xxx.rs` --> # Rust Learning Notes This article is the learning notes of <b style="color:#BE77FF">Rust</b> programming language written by me. The learning materials is based on [this](https://www.youtube.com/watch?v=rQ_J9WH6CGk) lecture. <img src="https://hackmd.io/_uploads/BJHuyYqTgg.png" style="width:5%"/> > Rust logo © The Rust Foundation. Used under the Rust Logo Policy. > This is a personal study note, not affiliated with the Rust Foundation. The GitHub repository related to this article is [here](https://github.com/Hmc-1209/RustPtcs). <br> ## Installation & Running the First Rust Program ### Install Rust To install Rust on our own computer, the easiest way is to use the following command if the operating system is Linux/Unix alike. ``` curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` To check out that Rust is successfully installed, simply type `rustup --version`, if the result shows up with a version number, it indicates a success installation. After the installation, the commands `rustc --version` and `cargo --version` should also works, as they are the <b style="color:#BE77FF">compiler</b> and <b style="color:#BE77FF">package manager</b> for Rust. <hr> ### Running the First Rust Program To run Rust Program, the first method is simply create a `.rs` file. The sample code could be something like below: > **File:** `hello.rs` ```rust fn main() { println!("Hello World!"); } ``` The code shows how to define a function in Rust, and inside of main function, there's a print command that print out the string "Hello World!". #### Using Cargo The above method require us to manually compile before execution every time we update the code by using the command `rustc hello.rs`, and than execute the output file using command `./hello`. However, Rust also provides a build system and packager manager <b style="color:#BE77FF">Cargo</b> that automatically compile and run the program for us. The Cargo usage is as follow: - Create a new project using the command `cargo new PROJECT_NAME`. - Write code into the `main.rs` created by cargo. - Run the command `cargo run` to let it comiple and execute the file for us. <img src="https://hackmd.io/_uploads/rkBf2Ycagg.png" style="width:20%"/> <br> ## Primitive Data Types ### Integer Rust has signed(+ and -) and unsigned(only +) integers, and are defined using the following type: |Signed|`i8`|`i16`|`i32`|`i64`|`i128`| |--|--|--|--|--|--| |Unsigned|`u8`|`u16`|`u32`|`u64`|`u128`| The number following represents the size of int. For example `i32` means it can store integer in range from <b>-2³¹</b> to <b>2³¹-1</b>. To define a variable with value and type, consider the following code: > **File:** `integer.rs` ```rust fn main() { let x: i32 = -10; let y: u64 = 100; println!("Signed integer: {}", x); println!("Unsigned integer: {}", y); } ``` <hr> ### Float Float in Rust has only two types: `f32` and `f64`. The showcase of code looks like below, almost the same as the integer. > **File:** `float.rs` ```rust fn main() { let pi: f64 = 3.14; println!("Value of pi: {}", pi); } ``` <hr> ### Boolean The boolean in Rust has only one type, `bool`. The usage is as follow: > **File:** `boolean.rs` ```rust fn main() { let today_is_workday: bool = true; println!("Today is workday? {}", today_is_workday); } ``` <hr> ### Character Character type in Rust also only has one option: `char`, and the usage is like: > **File:** `character.rs` ```rust fn main() { let letter: char = 'H'; println!("First character of last name: {}", letter); } ``` <br> ## Compound Data Types ### Array Array are a fixed collection of elements, and the data stored in it should be the same type. There are slightly differences when defining an array in Rust in comparison to other programming languages. As it is Rust, we again needs to define the type of it, but we wrap it into a square brackets with two values, seperating using semicolon. The first element represents the <b style="color:#BE77FF">type of elements</b> in the array, and the second element represents the <b style="color:#BE77FF">array length</b>. Arrays do not implement the Display trait, but we can use the <b style="color:#BE77FF">Debug trait</b> to print them and other more complex data types. > **File:** `array.rs` ```rust fn main() { let nums: [i32; 5] = [1, 2, 3, 4, 5]; println!("Numbers: {:?}", nums); let fruits: [&str; 3] = ["Apple", "Banana", "Orange"]; println!("Fruits: {:?}", fruits); println!("First ruit: {}", fruits[0]); println!("Second ruit: {}", fruits[1]); println!("Third ruit: {}", fruits[2]); } ``` > [!Note] > &str is a string slice (a reference to a string), while String owns its data. <hr> ### Tuple The tuple in Rust can store elements with multiple types. In tuple, it can also save other compound data type elements, like the following show case: > **File:** `tuple.rs` ```rust fn main() { let human = ("Bob", 20, 175, 55.4, false, [90, 95, 81, 83, 75]); println!("Human tuple: {:?}", human); } ``` <hr> ### Slices Slices can be think of reference in Rust. If data needed to be accessed efficiently and do not want it to be accidentally modified, using slices would be a good choice. Below are the example code of slices in Rust. > **File:** `slices.rs` ```rust fn main() { let numbers: &[i32] = &[1, 2, 3, 4, 5]; println!("Number slices: {:?}", numbers); let animals: &[&str] = &["Dog", "Cat", "Mouse"]; println!("Animal slices: {:?}", animals); let books: &[&String] = &[&"IT".to_string(), &"Phy".to_string(), &"Geo".to_string()]; println!("Animal slices: {:?}", books); } ``` <hr> ### String String in Rust is <b style="color:#BE77FF">growable</b> and <b style="color:#BE77FF">mutable</b>, it own the data itself, and it is stored on <b style="color:#BE77FF">heap</b>. Below is a case showing how to define a string and extend the contents. > **File:** `string.rs` ```rust fn main() { let mut name: String = String::from("Danny "); name.push_str("Ho"); println!("My name is: {}", name); } ``` Using `.push_str()`/`.push()` to extend the string with string/char, only on mutable variables. > [!Note] > All variables in Rust is immutable by default, so we cannot change it if we just define it using <b style="color:#BE77FF">let</b>, we need to add <b style="color:#BE77FF">mut</b> into the definition code. <hr> ### String Slice String slice is a <b style="color:#BE77FF">reference</b> of a string. It did not own the data, only providing a reference to find the string. It is stored on <b style="color:#BE77FF">stack</b>. > **File:** `string_slice.rs` ```rust fn main() { let string: String = String::from("Hello, World!"); let slice: &str = &string; let slice2: &str = &string[0..5]; println!("{}", slice); println!("{}", slice2) } ``` <hr> > [!Important] > Rust clean variables automatically, so outside the scope(in the above cases, main function) those variables are not recognized by Rust. <br> ## Functions Rust entry point function is <b style="color:#BE77FF">main</b>. If we create function that has the name more than one word, we should follow the <b style="color:#BE77FF">snake case</b> rule (e.g. `test_function`, `get_phone_number`). Variables declared outside of the main function must use <b style="color:#BE77FF">const</b> for immutable compile-time constants, or <b style="color:#BE77FF">static</b> for global variables with a fixed memory location. > **File:** `function.rs` ```rust fn main() { hello_world(); print_out_human_data("Bob", 175.5, 25); } fn hello_world() { println!("Hello, Rust!"); } fn print_out_human_data(name: &str, height: f32, age: i32) { println!("The name is {}, height is {} cm and are now {} years old.", name, height, age); } ``` <hr> ### Block Expression Another interesting feature in Rust is that we can define a variable using a block expression, allowing us to calculate its value with variables declared inside the block: > **File:** `block_expression.rs` ```rust fn main() { let x = { let price: i32 = 5; let qty: i32 = 10; price * qty }; println!("Total price: {}", x); }; ``` <hr> ### Function with return The return from function can be written as the following format, using the `->` to define the returned data type. > **File:** `function_with_return.rs` ```rust fn main() { let x = add(2, 3); let bmi = calculate_bmi(70.5, 1.802); println!("The add result is: {}", x); println!("The BMI value is: {}",bmi); } fn add(a: i32, b: i32) -> i32 { a + b } fn calculate_bmi(weight_kg: f64, height_m: f64) -> f64 { weight_kg / (height_m * height_m) } ``` <br> ## Ownership In Rust, there is a special mechanism called ownership. Basically there are three main concepts of ownership: - Every value in Rust has an <b style="color:#BE77FF">owner</b>. - There can only be <b style="color:#BE77FF">one owner at a time</b>. - When the owner goes <b style="color:#BE77FF">out of scope</b>, the value will be <b style="color:#BE77FF">dropped</b>. This mechanism helps manage memory in a clear and efficient way, reducing the chance of keeping unused variables in the program. The following code demonstrates how function parameters relate to the original variables. If we want to keep ownership of s1, we should pass it as a reference instead. > **File:** `ownership.rs` ```rust fn main() { let s1: String = String::from("Rust"); let len = calculate_length(&s1); println!("The length of {} is {}", s1, len); } fn calculate_length(s: &String) -> usize { s.len() } ``` If we try to create another variable and assign it with the current existing variable, the original one will <b style="color:#BE77FF">no longer exist</b>, thus the following code will <b style="color:#BE77FF">lead to an error</b>. ```rust fn main() { let s1: String = String::from("Hello Rust!"); let s2: String = s1; println!("{}", s1); // <-- Error! s1 no longer exist. } ``` > [!Note] > The error happens when the variable is <b style="color:#BE77FF">non-Copy type</b>, but types like <b style="color:#BE77FF">i32</b>, <b style="color:#BE77FF">bool</b>, <b style="color:#BE77FF">char</b> are <b style="color:#BE77FF">copy type</b>, which will not lead to this error. <br> ## Borrowing & References In Rust, variables borrowing to others means itself cannot been read/modified/borrowed anymore. If we want to make actions on it, we will have to do that on it's borrower. Here's a simple code demonstrating how to borrow value from others. > **File:** `borrowing.rs` ```rust fn main() { let mut x: i32 = 5; let y: &mut i32 = &mut x; *y += 1; // println!("{}", x); <-- Cannot read if already borrow to others println!("{}", y); } ``` > [!Tip] > Using * lets us reach the real value behind a reference so we can read or change it. <hr> ### Structure In Rust, a structure, enum, or trait needs to be defined using two types of blocks: the data definition (struct/enum) and the implementation block (impl) where methods or associated functions are defined. Here's an example of code referencing within a structure. > **File:** `struct.rs` ```rust fn main() { let mut account: BankAccount = BankAccount { owner: "Danny".to_string(), balance: 100.0, }; account.check_balance(); account.withdraw(45.3); account.check_balance(); } struct BankAccount { owner: String, balance: f64, } impl BankAccount { fn withdraw(&mut self, amount: f64) { println!("Withdrawing from {} with balance {}.", self.owner, amount); self.balance -= amount; } fn check_balance(&self) { println!("Account {} has a balance of {}", self.owner, self.balance); } } ``` <br> ## Constants Constants cannot be declared with `mut`. Also, the best practice is to defined the constant with <b style="color:#BE77FF">uppercase</b>. Moreover, we can define constant as <b style="color:#BE77FF">global</b>, and read them in any function. > **File:** `constant.rs` ```rust fn main() { let x : i32 = 5; const Y: i32 = 10; println!("x is {}", x); println!("Y is {}", Y); println!("Z is {}", Z); } const Z: f64 = 15.0; ``` <br> ## Shadowing Shadowing allows a variable to be <b style="color:#BE77FF">redefined</b>. Unlike marking a variable as mutable, shadowing can also change the variable’s data type. When a variable is shadowed, the original variable’s memory is <b style="color:#BE77FF">released</b>, and a <b style="color:#BE77FF">new one is allocated</b> for the new data. > **File:** `xxx.rs` ```rust fn main() { let x: i32 = 5; let x: i32 = x + 1; println!("x is {}", x); { let x: i32 = x * 2; println!("x is {}", x); } } ``` <br> ## Conditions The condition statements in Rust work similarly to other programming languages using `if`, `else if` and `else`. It is also possible to write <b style="color:#BE77FF">inline conditional expressions</b> to assign value directly to variables. The example below demonstrates both forms of condition suage. > **File:** `conditions.rs` ```rust fn main() { let weather: String = String::from("Rainy"); if weather == "Rainy".to_string() { println!("Remember to bring the unbrella! It is raining outside."); } else if weather == "Sunny".to_string() { println!("Remember to bring the unbrella! The sun is big."); } else { println!("You don't need the umbrella now!"); } let bring_umbrella: bool = if weather == "Rainy" || weather == "Sunny" {true} else {false}; println!("Bring umbrella: {}", bring_umbrella); } ``` <br> ## Looping Mechanisms The normal loop in Rust defined just like other programming languages, and can be used in inline definition. Below is a short demonstration of the loop mechanism. > **File:** `loop.rs` ```rust fn main() { let mut x: i32 = 10; let result: i32 = loop { x += 1; if x >= 20 { break x - 10; } }; println!("Result: {}", result); } ``` ### Label loop A label loop uses a `'label:` before a loop. It allows us to control outer loops when using break or continue. > **File:** `loop-label.rs` ```rust fn main() { let mut count = 0; '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; } } ``` <hr> ### While loop The while loop in Rust is pretty straight forward. Simply write `while` followed by the <b style="color:#BE77FF">condition</b>, the loop will automatically break when the condition no longer matches. > **File:** `while.rs` ```rust fn main() { let mut num: i32 = 3; while num > 0 { println!("Num: {num}"); num -= 1; } } ``` <hr> ### Looping through collection Using `for` loop in Rust can iterate elements in an array like Python. Here an example: ```rust fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; let b: [char; 5] = ['1', '2', '3', '4', '5']; for num in a { println!("{}", num); } for chr in b { println!("{}", chr); } } ``` <br> ## Structure Struct in Rust can be defined using the following code. The demonstration shows two ways to <b style="color:#BE77FF">create instance</b> using struct, and the <b style="color:#BE77FF">clone</b> mechanism of struct. > **File:** `struct.rs` ```rust fn main() { #[derive(Clone)] struct Book { title: String, author: String, pages: u32, available: bool } fn build_book_data(title: String, author: String, pages: u32, available: bool) -> Book { Book { title, author, pages, available } } // First method let book1: Book = Book { title: "Book1".to_string(), author: "Author1".to_string(), pages: 3, available: true }; // Second method let book2: Book = build_book_data("Book2".to_string(), "Author2".to_string(), 5, false); // If the instance want to inherit data from others let book3: Book = Book { title: "Book3".to_string(), ..book1.clone() }; println!("{} has author {}, {} pages and are now available: {}", book1.title, book1.author, book1.pages, book1.available); println!("{} has author {}, {} pages and are now available: {}", book2.title, book2.author, book2.pages, book2.available); println!("{} has author {}, {} pages and are now available: {}", book3.title, book3.author, book3.pages, book3.available); } ``` > [!Note] > The `#[derive(Clone)]` attribute before a struct is required only when we want to create new instances by cloning data from another instance. This is because of Rust’s ownership and borrowing rules. The `.clone()` method creates a duplicate of the data, ensuring that the original instance remains unchanged. <hr> ### Unit-like Struct A unit-like struct is used when we want to mark the type of an instance without storing any data. > **File:** `unit-like-struct.rs` ```rust struct Logger; impl Logger { fn log(&self, msg: &str) { println!("[LOG] {}", msg); } } fn main() { let logger = Logger; logger.log("Program started!"); } ``` <br> ## Enum An enum is a versatile tool used to represent a type that can take on one of <b style="color:#BE77FF">several possible variants</b>. An enum can be used as the type of a variable, for example inside a struct. > **File:** `enums.rs` ```rust fn main() { #[derive(Debug)] enum IPAddr { V4(u8, u8, u8, u8), V6(String), } let home_ip: IPAddr = IPAddr::V4(127, 0, 0, 1); let loopback_ip: IPAddr = IPAddr::V6(String::from("::1")); println!("Home IP: {:?}", home_ip); println!("Loopback IP: {:?}", loopback_ip); } ``` > [!Note] > The `#[derive(Debug)]` attribute before enum is required since we did not customize a display trait. So we need to print out it using debug trait: `{:?}`. <br> ## Error Handling In Rust, there are two important enums commonly used for handling normal and error behaviors: `Option` and `Result`. The `Option` enum is used when a value may or may not exist. If a value exists, it returns Some(value); if not, it returns None. Its structure looks like: ```rust enum Option<T> { // Define the generic Option type Some(T), // Represent a value None, // Represent no value } ``` The `Result` enum is used when an operation can either succeed or fail. It returns Ok(value) when successful, or Err(error) when something goes wrong. Its structure looks like: ```rust enum Result<T> { // Define the generic Option type Ok(T), // Represent a value Err(E), // Represent an error } ``` To demonstrate the usage of error handling, consider the code shown below. In this example, there are two functions that return the Option and Result types respectively. Depending on the return type, we can use match to handle each possible outcome accordingly. > **File:** `option-result.rs` ```rust fn divide_fnc_option(x: f32, y: f32) -> Option<f32> { if y == 0.0 { None } else { Some(x / y) } } fn divide_fnc_result(x: f32, y: f32) -> Result<f32, String> { if y == 0.0 { Err("Cannot divide by 0".to_string()) } else { Ok(x / y) } } fn main() { match divide_fnc_option(5.0, 3.0) { None => println!("Cannot divide by 0"), Some(x) => println!("Some number: {}", x), } match divide_fnc_option(5.0, 0.0) { None => println!("Cannot divide by 0"), Some(x) => println!("Some number: {}", x), } match divide_fnc_result(0.5, 0.1) { Err(e) => println!("Error: {}", e), Ok(x) => println!("Result: {}", x), } match divide_fnc_result(0.5, 0.0) { Err(e) => println!("Error: {}", e), Ok(x) => println!("Result: {}", x), } } ``` > [!Tip] > Notice that in a `Result` return type, we must provide both the `Ok` and `Err` value types explicitly. <br> ## Vector Vector can dynamically increase or decrese the elements stored in it. We can define it using `Vec<TYPE>`, and the read action are just like arrays. > **File:** `vector.rs` ```rust fn main() { let mut v1: Vec<i32> = Vec::new(); // Defining with empty vector let mut v2: Vec<i32> = vec![1, 2, 3]; // Defining with value v1.push(1); println!("The vector v1: {:?}.", v1); println!("The third element in v2: {}.", &v2[2]); let forth = v2.get(4); match forth { Some(x) => println!("The third element in v2 is {}.", x), None => println!("There is no forth element in v2."), } } ``` <br> ## UTF-8 Strings In Rust, if we use the `+` operator to add two strings together, the latter will be passed as reference, so we should add `&` to it. Another way to add two strings is `format!`. It will return a `String` element, so we can then save it as a string. > **File:** `utf-9-strings.rs` ```rust fn main() { let mut s1 = String::from("Hello, "); let s2 = String::from("world!"); s1 = s1 + &s2; println!("{}", s1); let s3: String = String::from("Hi "); let s4: String = String::from("there!"); println!("{}", format!("{}{}", s3, s4)); } ``` <br> ## Hashmap Hashmap stores data with <b style="color:#BE77FF">key</b> and <b style="color:#BE77FF">value</b> pair. The following code demonstrates how to use a hashmap. > **File:** `hashmap.rs` ```rust use std::collections::HashMap; fn main() { let mut scores: HashMap<String, i32> = HashMap::new(); scores.insert(String::from("Blue"), 10); let team = String::from("Blue"); let score = scores.get(&team).copied().unwrap_or(0); for (key, value) in &scores { println!("{} team score: {}", key, value); } } ```