Chapter 3 Variables and Mutability in Rust

๐Ÿš€ Variables


Variables are immutable by default

By default variables are immutable, to change this we will use mut in front of the definition

let mut number = 5;

Differences between constants and variables


To declare:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Shadowing


This concept exists (I think) in all programming world, but in Rust is exciting. When you want to shadow a variable, for example apples, you can overwrite that value using the same name that the original variable:

let apples = 5

let apples = apples + 3 // This will use apples = 5 + 3

Also it works inside of scopes

let apples = 5 // = 5
{
	let apples = apples * 2 // = 10
}
let apples = apples + 3 // = 8
Important

This is not the same that use mut, in this case you are really using a new variable, assigning the value of the previous one

Changing the type by shadowing

You can change the type of a variable thanks to shadowing

    let spaces = "   "; // = "    "
    let spaces = spaces.len(); // = 4

Shadowing spares us to have to write a new variable with different name, like spaces_str.


๐Ÿท๏ธ Datatypes


Rust is a statically typed language:

A statically typed programming language is one in which variables and data types are checked at compile time, that is, before the program runs. This means that errors are detected early on, which can help prevent common problems during code execution (Tecnobits, 2023)

let guess: u32 = "42".parse().expect("Not a number!");
// unsigned 32-bit number

Rust can show errors many times if we do not specify the type

Rust has 4 primary scalar types of variables

Integer Types

Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
Architecture-dependent isize usize

Integers can be unsigned or signed

The way that you can know how many numbers can you store inside of one of those is using the next equation

nminย , nmax=โˆ’(2nโˆ’1)ย ,ย 2nโˆ’1โˆ’1

Here n represents the number of bits. Unsigned variants is the same way, but instead to be from โˆ’(2nโˆ’1) is from 0

Also the isize or usize depends on the architecture of your computer.

Type of number literals that you can write

Number literals Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'
Note

You also can use underscore to separate numbers that could be hard to read, for example, instead of use 10000 you can write 10_000.

Default integer set by Rust is i32

Floating-point type

These are numbers with decimal point. There are with 32-bit and 64-bit, f32 and f64, respectively. Default is f64. This is always signed.

Boolean type

You can specify if you are using boolean specifying bool:

let c: bool = false;

Character type

This is the most primitive alphabetic type

fn main() {
    let c = 'z';
    let z: char = 'โ„ค'; // with explicit type annotation
    let heart_eyed_cat = '๐Ÿ˜ป';
}

You must use ' instead of ", which represents string values

Compound types

Rust has two types of this kind. Tuples and arrays

Tuples

They can have a different types of values inside:

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Destructuring

Also you can define values using each value of a tuple. This is called destructuring.

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

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

Access to an specific element in a tuple

Also we can access to an specific element in a tuple using . followed by the index of that element:

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

Arrays

Unlike a tuple, here we must have the same data type in the whole array:

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Arrays can be changed or modified in length, so they are useful if you need an specific number of elements available to use, for example, months of year:

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

Also you can specify the type and the amount of elements inside of the array:

let a: [i32; 5] = [1, 2, 3, 4, 5];
// each element is a signed 32-bit number

Similarly, you can set a specified number of elements with the same value inside of an array using a simple expression. For example:

let a = [3; 5];
// It is the same like: let a = [3, 3, 3, 3, 3]

Access to an specific element in an array

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

Rust protects you with panicks

In resume, when you try to access past to the end of an array, Rust will show something like this:

thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

In other programming languages this happens without checking the error, and the error access to an invalid memory.

๐Ÿ”ฎ Functions


The keyword to declare functions in rust is fn. The way that you can call a function is in this way:

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Parameters

We can define functions with parameters

Rust Documentation says

We can define functions to have parameters, which are special variables that are part of a functionโ€™s signature. When a function has parameters, you can provide it with concrete values for those parameters. Technically, the concrete values are called arguments, but in casual conversation, people tend to use the words parameter and argument interchangeably for either the variables in a functionโ€™s definition or the concrete values passed in when you call a function.

fn main() {
    another_function(5);
}

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

It is important to define the type of data that you want in parameters of a function

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Statements and expressions

Statements

For example, this is a statement:

fn main() {
    let y = 6;
}

Also, for example, you cannot do this:

fn main() {
	// This is wrong โŒ
    let x = (let y = 6);
}

In other languages like C, you can do x = y = 6, but here, in rust, you cannot do that. This is why the statement let y = 6 DOES NOT return anything

Expressions

For example, an expression returns a value, calling a function is an expression, for example, a macro from Rust is an expression. A scope created with curly brackets is an expression:

fn main() {
    let y = {
        let x = 3;
        // This is an expression
        x + 1
    };

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

This RETURNS a value. Also you will notice that expression do not use semicolons:

Quote

If you add a semicolon to the end of an expression, you turn it into a statement, and it will then not return a value

Function with return values

The way that you can return a value is very simple, first, you need to specify the data type to return in a function using ->:

fn five() -> i32 {
    5
}

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

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

As you can see, you can return values without using the keyword return, you just have to avoid the use of semicolon, as you saw in the previous point.

Another example to understand the return values is this:

fn main() {
    let x = plus_one(5);

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

fn plus_one(x: i32) -> i32 {
    x + 1
}

Note that we are not using semicolon, if we put a semicolon at the end of the expression x+1, this will turned in an statement, and this will throw an error.

๐Ÿ” Control Flow (Conditions and bucles)


If expression

This expression should be familiar to you, it exists in all programming languages and flow structures. In rust conditions if-else are some like this:

fn main() {
    if number > 5 {
        println!("Number {number} greater than 5");
    } else {
        println!("Number {number} smaller than 5");
    }
}

Each code block using curly brackets in an if condition are called arms, like match expression. else expression is optional same like other programming languages

You cannot do something like this:

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

This is because in Rust, you must be explicit in each condition that you make. In this case this must be a bool type.

Multiple conditions

fn main() {
    let number : i8 = 7;

    if number < 0 {
        println!("Number is negative");
    } else if number % 2 != 0 {
        println!("Number {number} is an odd number");
    } else if number == 0 {
        println!("Number is zero ");
    } else {
        println!("Number {number} is an even number");
    }
}

Conditions in let statement

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

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

The values in this type of if statements must be the same data type. This could be a common error:

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

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

Loops

There are three type of loops:

loop

This will create an infinite loop:

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

You can interrupt using ctrl+C or using a break statement inside of this. You can combine loops with other control flow structures like conditions or other loop structures.

Return Values with loop

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}
Warning

Note that after the break expression we passed the value returning to our result variable

Labels for loops

This is a way that you can name a loop to avoid ambiguities, for example, if you need to break one of two loops. See the next sample:

fn loops_example_second() {
    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; // Here we are specifing that if the first counter is 2, it will break the first loop and all loops inside
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

While Bucle

In rust you can make a while bucle using only loop and else-if. Something like this:

fn main() {
	let mut number = 3;

    // first bucle with loop
    loop {
        println!("{number} :D");
        if number == 1 {
            break;
        }

        number -= 1;
    }
    println!("HAPPY BIRTHDAY");
}

But in Rust already exists a bucle specialized in this kind of bucles, this is the while bucle:

fn main() {
	let mut number = 3;

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

    println!("LIFTOFF!!!");

}

Looping in a collection with for

You can loop a collection using a while bucle like this:

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

However this way to loop a collection is an error-prone and also is slower because the program each time that pass an element must evaluate the condition to stop the program. Fortunately, rust also has a loop known as for, in other programming languages is better known as foreach, and this kind of loop is better to loop a collection:

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

Machine code generated from for loops can be more efficient as well because the index doesnโ€™t need to be compared to the length of the array at every iteration.

For loop is one of the most used loops used in Rust

Countdown using for bucle:

fn main() {
	for counter in (1..4).rev() {
       println!("counter: {counter}");
    }
}