Javascript is required
/rust/rust-basics.md

Rust

Rustup

Rustup is the Rust installer and the Rust version manager.

Installing Rustup:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Cargo

Cargo is the Rust build tool and package manager.

Primary use cases:

  • cargo build: Build Rust project
  • cargo run: Run Rust project
  • cargo test: Run Rust project testing
  • cargo doc: Build project documentation
  • cargo publish: Publish a library to crates.io
  • cargo new project-name: Create a new Rust project
  • cargo add dependency-name: Add a new dependency to the project.

Checking Cargo version:

cargo --version

VSCode

Install extension rust-analyzer.

Creating a new project

cargo new hello-rust

Project structure

  • ./Cargo.toml: Manifest file for Rust. It’s where to keep metadata for the project, as well as dependencies.
  • ./src/main.rs: Rust application entypoint.

Adding dependencies

cargo add ferris-says

Adds to Cargo.toml:

[dependencies]
ferris-says = "0.3.1"

Install the dependency:

cargo build

Similarly to NodeJS, a Cargo.lock is created.

Importing the new dependency within Rust files:

use ferris_says::say;

It's now possible to use the say method exported from ferris_says module.

Variables

In Rust variables are defined using let and const keywords.
By default both let and const variable types are unmutable.
It's possible to make a variable mutable trought let mut keywords in order to make easy to read if a variable will change during the code flow.
Const variables are always unmutable similarly to C/C++ and always require to define the constant data type; they might be used into the global scope.
Naming convention is to define let variables as snake_case lowercase and const variables as snake_case uppercase.

Shadowing

It's possible to redefine a let variable into the same scope and the previously declared one gets "shadowed",
it's useful to reassign to the variable a computed version of its previous value:

let x = 1
let x = x * 2
let x = x * 4

Data Types

Rust data types are subdivided into scalar types and compound types.

Scalar Types

Integer numbers

It is a number without a fractional component, they are divided into signed and unsigned.
Signed and unsigned refer to whether it’s possible for the number to be negative (prefix "i" for signed and "u" for unsigned).
If a number needs to have a sign (+,-,...) will be declared as signed.

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

An i8 can store numbers from -(27) to 27 - 1, which equals -128 to 127.
The isize and usize types depend on the architecture of the computer your program is running on, which is denoted in the table as “arch”: 64 bits if you’re on a 64-bit architecture and 32 bits if you’re on a 32-bit architecture.

Floating points numbers

Rust also has two primitive types for floating-point numbers, which are numbers with decimal points. Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision. All floating-point types are signed.

Boolean type

As in most other programming languages, a Boolean type in Rust has two possible values: true and false. Booleans are one byte in size. The Boolean type in Rust is specified using bool.

Character type

Characters are defined using char type, they use single quote for normal declaration and double quotes for string literals.

Compound Types

Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.

Tuple type

A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size.

We create a tuple by writing a comma-separated list of values inside parentheses. Each position in the tuple has a type, and the types of the different values in the tuple don’t have to be the same.

let tup: (i32, f64, u8) = (500, 6.4, 1);

The variable tup binds to the entire tuple because a tuple is considered a single compound element. To get the individual values out of a tuple, we can use pattern matching to destructure a tuple value, like this:

let tup = (500, 6.4, 1);
let (x, y, z) = tup;

We can also access a tuple element directly by using a period (.) followed by the index of the value we want to access. For example:

let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;

The tuple without any values has a special name, unit. This value and its corresponding type are both written () and represent an empty value or an empty return type.

Array Type

Another way to have a collection of multiple values is with an array. Unlike a tuple, every element of an array must have the same type. Unlike arrays in some other languages, arrays in Rust have a fixed length.

An array isn’t as flexible as the vector type, though. A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size.

It's possible to define the type of an array as:

let a: [i32; 5] = [1, 2, 3, 4, 5];

It's possible to access the first element as:

let first = a[0];

Functions

Rust code uses snake case as the conventional style for function and variable names, in which all letters are lowercase and underscores separate words.

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

Functions can return values to the code that calls them. We don’t name return values, but we must declare their type after an arrow (->). In Rust, the return value of the function is synonymous with the value of the final expression in the block of the body of a function. You can return early from a function by using the return keyword and specifying a value, but most functions return the last expression implicitly.

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

There are no function calls, macros, or even let statements in the five function—just the number 5 by itself. That’s a perfectly valid function in Rust.

Control Flow

If expressions

It's important to remember that the provided condition must always be a bool or it would throw error.

let number = 6;
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");
}

Inline shorthand:

let condition = true;
let number = if condition { 5 } else { 6 };

Returned values must be of the same type since the variable has a determined type.

Loop expressions

loop {
    println!("again!");
}

It's possible to stop the loop using break keyword that also accepts a value to be returned.

let mut counter = 0;

let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2;
    }
};

It's possible to give a label to the loop in order to disambiguate them and to be able to break it.

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

By default break keyword will stop the inner loop, by specifying the loop name it will break the specific one.

While expressions

let mut number = 3;
while number != 0 {
    println!("{number}!");
    number -= 1;
}

For expressions

let a = [10, 20, 30, 40, 50];
for element in a {
    println!("the value is: {element}");
}

Generating a loop from a reversed list of ordered numbers:

for number in (1..4).rev() {
    println!("{number}!");
}

Ownership

Ownership is Rust’s most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector.
A GC-enabled (garbage collector) programming language includes one or more garbage collectors (GC engines) that automatically free up memory space that has been allocated to objects no longer needed by the program.

Ownership is a set of rules that govern how a Rust program manages memory.

Stack & Heap

Both the stack and the heap are parts of memory available to your code to use at runtime, but they are structured in different ways.

  • Stack (last in, first out): Think of a stack of plates: when you add more plates, you put them on top of the pile, and when you need a plate, you take one off the top. Adding or removing plates from the middle or bottom wouldn’t work as well! Adding data is called pushing onto the stack, and removing data is called popping off the stack. All data stored on the stack must have a known, fixed size. Data with an unknown size at compile time or a size that might change must be stored on the heap instead.
  • Heap (less organized): When you put data on the heap, you request a certain amount of space. The 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. This process is called allocating on the heap.

Pushing to the stack is faster than allocating on the heap because the allocator never has to search for a place to store new data; that location is always at the top of the stack.
Comparatively, allocating space on the heap requires more work because the allocator must first find a big enough space to hold the data and then perform bookkeeping to prepare for the next allocation.

Accessing data in the heap is slower than accessing data on the stack because you have to follow a pointer to get there. Contemporary processors are faster if they jump around less in memory.

The main purpose of ownership is to manage heap data

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.

Scope

Similarly to other programming language variables have a scope that begins at the variable definition and ends at the end of the code block it is part of.

String type

Basic data types such as integer or float can be stored on the stack and popped off the stack when their scope is over.
Rust offers string literals, where a string is hardcoded into the program with the limit that they're immutable.
Rust offers a mutable type named String that manages data allocated on the heap and as such is able to store an amount of text that is unknown at compile time.
It's possible to create a String from a string literal using the from function:

let s = String::from("hello");

And append another string literal to it as:

let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!`

When dealing with String it's important to keep in mind that:

  • The memory must be requested from the memory allocator at runtime. String::from allocated it automatically.
  • It's necessary to return this memory to the allocator when we’re done with our String. When a variable goes out of scope, Rust calls automatically a special function. This function is called drop, and it’s where the author of String can put the code to return the memory. Rust calls drop automatically at the closing curly bracket.

Double Free Error

The following code will throw error because the reference of s1 is assigned to s2 and the the drop time will be dropped the memory reference 2 times.

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);

The solution is to use the built-in method "clone" to do a deep copy:

let s1 = String::from("hello");
let s2 = s1.clone();
Types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make.

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

References and Borrowing

A reference is like a pointer in that it’s an address we can follow to access the data stored at that address; that data is owned by some other variable. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.

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 string reference owned by main fn is borrowed to the calculate_length fn, since it's borrowed by another owner the calculate function is not able to edit it.
It's possible to edit a borrowed reference using a "mutable reference":

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);

will fail.

It's possible to use curly brackets to create a new scope, allowing for multiple mutable references, just not simultaneous ones:

let mut s = String::from("hello");
{
    let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);

Dangling References

In languages with pointers, it’s easy to erroneously create a dangling pointer—a pointer that references a location in memory that may have been given to someone else—by freeing some memory while preserving a pointer to that memory. In Rust, by contrast, the compiler guarantees that references will never be dangling references: if you have a reference to some data, the compiler will ensure that the data will not go out of scope before the reference to the data does.

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

The solution here is to return String directly.

Slice Type

Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection.

let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];

the hello variable contains the reference to the memory cells that contains the word "hello".

From the 3rd cell to the end:

let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];

Taking the whole reference:

let slice = &s[..];

Defining a string slice as function parameter:

fn first_word(s: &str) -> &str {

Slice of a integer array:

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];

Structs

Structs hold multiple related values together and differently by tuples they use named entities.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}
fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}

Defining a struct as mutable will make all fields mutable, isn't possible to make mutable only certain fields.
Similarly to Javascript, structs fields may be assigned as follow:

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

Similarly to Javascript, it's possible to use a spread operator:

let user2 = User {
    email: String::from("another@example.com"),
    ..user1
};

It's possible to assign to a struct field both owned and borrowed types,
it's important to keep in mind that a reference assigned to a struct may not always be valid in the case it goes out of scope while is still being used from the struct.
Lifetimes ensure that the data referenced by struct is valid for as long as the struct is.

Tuple Structs

Tuple structs are a way to create a struct without assigning names to struct fields.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Derive

It's possible to derive traits from other structs when we define a struct.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

It's necessary to derive the Debug trait in order to print out the string using dbg! or println! macros.

Method Syntax

Unlike functions, methods are defined within the context of a struct (or an enum or a trait object).
Their first parameter is always self, which represents the instance of the struct the method is being called on.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

It's possible to define a method with the same name of an attribute, they are discriminated on the basis of their invokation.
All functions defined within an impl block are called associated functions because they’re associated with the type named after the impl.
To call an associatied function, it's used :: syntax:

let sq = Rectangle::square(3);

-> Operator

In C and C++, two different operators are used for calling methods: you use . if you’re calling a method on the object directly and -> if you’re calling the method on a pointer to the object and need to dereference the pointer first. In other words, if object is a pointer, object->something() is similar to (*object).something().

Rust doesn’t have an equivalent to the -> operator; instead, Rust has a feature called automatic referencing and dereferencing. Calling methods is one of the few places in Rust that has this behavior.

Here’s how it works: when you call a method with object.something(), Rust automatically adds in &, &mut, or * so object matches the signature of the method. In other words, the following are the same:

p1.distance(&p2);
(&p1).distance(&p2);

Enums

Enums give you a way of saying a value is one of a possible set of values.

enum IpAddrKind {
    V4,
    V6,
}

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};
enum IpAddr {
    V4(String),
    V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

Rust Nullability

The problem with null values is that if you try to use a null value as a not-null value, you’ll get an error of some kind. Because this null or not-null property is pervasive, it’s extremely easy to make this kind of error.

However, the concept that null is trying to express is still a useful one: a null is a value that is currently invalid or absent for some reason.

The problem isn’t really with the concept but with the particular implementation. As such, Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent. This enum is Option, and it is defined by the standard library as follows:

enum Option<T> {
    None,
    Some(T),
}

Match Control Flow

Rust has an extremely powerful control flow construct called match that 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,
    }
}

There’s one other aspect of match we need to discuss: the arms’ patterns must cover all possibilities.

Packages & Crates

A crate is the smallest amount of code that the Rust compiler considers at a time.
Crates can contain modules, and modules may be defined in other files that get compiled with the crate.
A crate can come in one of two forms: a binary crate or a library crate.

  • Binary crates: programs compiled to a runnable executable, they must have a function called main that defines what happens when the executable runs.
  • Library crates: programs that don’t have a main function, and they don’t compile to an executable. They define functionalities intended to be shared with multiple projects.

The crate root is a source file that the Rust compiler starts from and makes up the root module of a binary crate.
A package is a bundle of one or more crates that provides a set of functionality. A package contains a Cargo.toml file that describes how to build those crates.
If a package contains src/main.rs and src/lib.rs, it has two crates: a binary and a library, both with the same name as the package. A package can have multiple binary crates by placing files in the src/bin directory: each file will be a separate binary crate.

To import and call a function it's necessary to know its path:

  • An absolute path is the full path starting from a crate root; for code from an external crate, the absolute path begins with the crate name, and for code from the current crate, it starts with the literal crate.
  • A relative path starts from the current module and uses identifiers in the current module.

A relative path exposes self and super keywords:

  • super => used to refer the parent module similarly to path ../ (super::deliver_order())
  • self => used to refer the current module similarly to path ./ (self::method

Closures (Callback Functions)

Similarly to javascript is possible to define callback functions as follow:

# inline returning
class.method(|| 5 + 3)
# nested nested returning
class.method(|| anotherMethod())
# multilines returning
class.method(|| {
    let five = 5
    let six = 6
    5+6
})

JavaScript

C/C++

Java

CI

Rust

GO

JavaScriptC/C++JavaCIRustGObooleanoopdcmd