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