Learning Rust

This post is not a tutorial, it is just my notes during the time i learn Rust. Glad if you found it useful.
If you are finding how to run Rust on your machine, take a look at my previous article to see how to install Rust programming language.

Why did i choose Rust?

I am an Enginner who love programming and building reliable systems. My favorite language is Python, i have written a lot of application based on Python. However, Python is an interpreted language, it is slow. In order to build applications for a high performance system, we have to use compiler language. There are several compiler language such as C, C++, Java, etc but i found Rust is morden and easy to use. Rust can run on both Windows and Unix also.

One of my favorite developement case is building web application. I am good with Python Flask. It is a great framework but i found Rust is able to do the same job. For example, we have Rocket for the web framework, Diesel for the DB ORM and Tera for the template.

For three years in a row. Rust has been voted the most loud language on Stack Overflow’s developer survey. It runs blazingly fast, prevents set falls and guarantees threat safety. It’s a completely open source language and used widely across the world for systems programming.

Learning Rust

1. Cargo

Cargo is Rust’s build system and package manager. Cargo take care of building your code, downloading the libraries your code depends on and building those libraries.

To create new project with Cargo

1
cargo new hello_world --bin

To run a project with Cargo

1
cargo run

2. Variables and mutability

In Rust, variables are immutable by default. That means once a variable is declared, you cannot change its value. This encourages you to write your code in a way that takes advantage of the safety and concurrency that Rust offers.

1
2
let x = 10;     // immutable variable
let mut y = 20; // mutable variable

3. Data types

Rust is a statically typed language, which means that it must know the types of all variables at compile time. The compiler can usually infer what type we want to use based on the value and how we use it.

There are 2 data types:

  • Scalar - represents a single value. Rust has four primary scalar types. Integers, floating-point numbers, booleans, and characters.
  • Compound - group multiple values of other types into one type. Rust has two primitive compound types: Tuples and Arrays.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let a = 10;
let b :i32 = 5;

// float types
let x = 4.0; // f64
let y: f32 = 5.0 // f32

// boolean type
let f1 = true;
let f2: bool = false;

// character type (also support unicode)
let c = 'K';

// tuple
let tup: (i32, u64, u8) = (100, 2.5, 1);

// array
let animals = ["Tiger", "Lion", "Horse", "Mouse", "Monkey"];

To access values in compound data type

1
2
3
4
5
6
7
8
// access tuple
let v1 = tup.0
let v2 = tup.1
let v3 = tup.2
// or
let (v1, v2, v3) = tup;
// access array
let lion = animals[1];

4. Function

Rust code uses snake case as the conventional style for function and variable names. In snake case, all letters are lowercase and underscores separate words. Function definitions in Rust start with fn and have a set of parentheses after the function name. The curly brackets tell the compiler where the function body begins and where it ends.

1
2
3
fn sum(x:i32, y:i32) -> i32 {
x + y // no semicolon here so value will be returned
}

5. if/else condition

Just like other languages

1
2
3
4
5
6
7
8
9
let x = 6;

if x % 4 == 0 {
println!("X devisible by 4");
} else if x % 3 == 0 {
println!("X devisible by 3");
} else {
println!("X devisible by 4 and 3");
}

6. Loops

We can use while loop and for loop in Rust

While loops:

1
2
3
4
5
6
7
8
9
10
11
// simple loop without contidion
loop {
println!("This is an infinite loop!");
}

// while loop with condition
let mut i = 1;
while i < 10 {
println!("i = {}", i);
i = i + 1;
}

For loops:

1
2
3
4
5
let array = [1, 2, 3, 4, 5];

for e in array.iter() {
println!("Value is {}", e);
}

Note: we can exit the loop with break keyword or go to the next iteration with continue keyword.

7. Ownership in Rust

7.1. Principles

Stack: The stack stores values in the order it gets them, and removes the values in the opposite order. This is referred to as last in, first out. The stack is fast because of the way it accesses the data. It never has to search for a place to put new data or a place to get data from because that place is always the top. All data on the stack must take up a known, fixed size.

Heap: The heap is less organized. When we put data onto the heap, we ask for some amount of space. The operating system finds an empty spot somewhere in the heap that is big enough, marks it as being in use, and returns to us a pointer, which is the address of that location. Accessing the data in the heap is slower than accessing the data on the stack because we have to follow a pointer to get there.

In Rust, data types like the integers, booleans, and strings, are stored in the stack whereas for more complicated data types we have the heap. In other programming languages, like C and C++, they have pointers and references to manually manage pointers. If you create two pointers which point to the same piece of data, and then you dereference both of them, you could cause memory corruption issues. High level programming languages like JAVA, Python, and Ruby use something called as a garbage collector. But in Rust, we have ownership.

7.3. Ownership

The ownership rule in Rust

  • Each value in Rust has a variable that’s called its owner.
  • There can only one owner at a time.
  • When the owner goes out of scope, the value gets dropped.

Example

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let flag = true;

if flag {
let x = 10;
println!("X = {}", x); // this is fine
}

// following line causes the error "not found in this scope"
// during compile time
println!("X = {}", x);
}

The onwership of a variable follows the same pattern every time: assigning a value to another variable moves it. When a variable that includes data on the heap goes out of scope, the value will be cleaned up by drop unless the data has been moved to be owned by another variable.

7.4. Memory allocation

In other languages, the programmer has to free up the memory after using or it can be done with the Garbage Collector (GC). In Rust, with the ownership, the memory is automatically returned once the variable goes out of scope.

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = String::from("My name is Khanh");

// this line is fine
println!("{}", s1);

// let's copy s1 to s2
let s2 = s1;

// following line causes the error "use of moved value"
// during compile time
println!("{}", s1);
}

In example above, as soon as s2 is created, s1 goes out of scope and memory gets freed up. In Rust, we call it is move while in other languages call shallow copy.

If we don’t want to “move” the variable, we can make a deep copy by using s2 = s1.clone().

7.4. References and borrowing

Passing a variable into a function is same as assigning it another variable; the value will be “move” and not available to use anymore. If we don’t want that happends, use REFERENCE.

Following example defines and uses a calculate_length function that has a reference to an object as a parameter instead of taking ownership of the value.

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

The &s1 syntax lets us create a reference that refers to the value of s1 but does not own it. Because it does not own it, the value it points to will not be dropped when the reference goes out of scope.

Note: mutable reference has a limitation: only one mut reference is allowed in the same scope.

8. Matching

In Rust, match is quite similar to switch - case in other languages. It is straightforward to use.

1
2
3
4
5
6
7
8
9
10
11
fn main(){
let n = 3;

match n {
1 => println!("Your value is {}", n),
2 => println!("Your value is {}", n),
3 => println!("Your value is {}", n),
4 => println!("Your value is {}", n),
_ => println!("Your value is not here")
}
}

9. Structs

A struct or structure is a custom data type that lets us name and package together multiple rated values that make up a meaningful group.

Example of using struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// to define a struct
struct User {
username: String,
password: String,
age: i32;
active: bool,
}

// to create a variable from a struct
let user1 = User{
username: String::from("khanh"),
password: String::from("a_secure_password"),
age: 27,
active: true,
}

// to access values inside a struct
println!("Your username is {}", user1.username);

10. Enums

Enums allow us to define a type by enumerating its possible values.

Example of using enum

1
2
3
4
5
6
7
8
9
enum IPAddress {
v4(String),
v6(String),
}

fd main() {
let ipv4 = IPAddress::v4(String::from("127.0.0.1"));
let ipv6 = IPAddress::v6(String::from("::1"));
}

TO BE CONTINUED…

Share Comments