Make a strong  Rust Foundation - Data Types, Variables, Mutability, and Constants

Make a strong Rust Foundation - Data Types, Variables, Mutability, and Constants

Learn about Data Types, Variables, Mutability, and Constants in Rust.

Objective

There are some common concepts that appear in almost every programming language. Let's dive into them here to start your Rust journey right.

Before we begin

I will try my best to go into as much detail as possible. But don't worry if you find some things hard to grasp. You can always come back after learning more or building some practice projects.

GitHub repo with all the code

https://github.com/codetit4n/rust-school

Make sure to star/fork/watch it on GitHub.

Some system-level fundamentals you should know

Since we are getting started with rust, I won't go very deep into Rust's memory management in this article. I will cover that later with things like Stack, Heap, static memory, etc. But for now, you need to understand what memory is.

There are 2 types of memory:

  • Persistent memory: Think of your HDD/SSD. It is the persistent memory because things you put on it can persist even after you turn off your computer. Or you can think of a database where you put some data, which will persist. This is slower compared to volatile memory and is more abundant.

  • Volatile(temporary) memory: Think of your RAM(Random Access Memory). This is a memory that will be erased if you shut down your computer. This is faster than persistent memory and scarce. The important thing to note is that whenever a program is running it uses this memory.

Now, there are a lot of details that we can go into for memory but as a beginner, you should be familiar with at least this much.

Any memory in a computer will store only binary data (1s and 0s). However, anything can be represented in binary. Your program will determine what the binary represents.

Data types

There are some fundamental data types that every language provides that are universally useful. Data types tell rust what kind of data is being specified so it knows how to work with that data.

So, what are some commonly used data types in rust?

  • Boolean: true or false

  • Integer: 1, 2, 30, -2,-40, etc

  • Double/Float: 1.2, 10.4, etc

  • Characters: 'A', 'B', 'C', etc

  • String: "Hello Rust", "I am from the future", etc

Since 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. But sometimes we might have to tell the compiler explicitly. Now, let's learn about variables and we will revisit this after that.

Variables

Humans can't directly work with memory(at least not easily), so we need variables to make that easier for us. A variable is a way to assign data to a temporary/volatile memory location.

NOTE: Here, the variables will be using the temporary memory(i.e., RAM).

Mutability

Variables in Rust are by default immutable. Which means the values can't be changed once set. If you want to make a variable mutable you have to explicitly tell the Rust compiler. This is Rust's way of encouraging you to favor immutability. Consider this code snippet:

fn main() {
    let x = 1;
    println!("The value of x is: {x}");
}

This will simply print the value of x and you will get:

Here, by using the let statement we have declared a variable x and assigned it a value of 1. And after that, we are printing it. By default, this is an immutable variable. So, if I try to reassign it to another value:

fn main() {
    let x = 1;
    println!("The value of x is: {x}");
    x = 2; //will give error
}

It will give me an error:

NOTE: How beautifully the Rust compiler describes the error and possible fixes. This is another amazing reason to use Rust.

The reason for this error is that it cannot assign an immutable variable twice. Which is very self-explanatory.

But, I can fix this by making the variable x mutable like this:

fn main() {
    let mut y = 1; //mutable variable
    y = 4;
    println!("The value of y is: {y}");
}

Output:

Here, you can see it prints the correct values and does not give any errors. It is giving some warnings, but for now, we can ignore these warnings.

This is the way you can make your variable mutable by using mut.

Revisiting Data Types and using Type Annotations

Now, consider:

let x = 4;

Here, the compiler can infer the data type. But there are certain cases where we need to specify the data type explicitly. Before going into that let's learn how are the different Data Types represented in Rust:

The Data Types in Rust can be broadly classified into 2 categories:

  1. Scalar Types

  2. Compound Types

Scalar types: These are types that represent a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters. We will learn about Compound types in Part 2 (next article).

Integer types:

An integer is a number without a fractional component. There is a very good table given in the rust book:

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Signed includes negative numbers but unsigned do not.

Here, for example: If you want to represent a signed 8-bit number it will have the type i8. The unsigned version will be u8.

"Each signed variant can store numbers from -(2n - 1) to 2n - 1 - 1 inclusive, where n is the number of bits that variant uses. So an i8 can store numbers from -(27) to 27 - 1, which equals -128 to 127. Unsigned variants can store numbers from 0 to 2n - 1, so a u8 can store numbers from 0 to 28 - 1, which equals 0 to 255."

Side Note: In the computer system, the signed numbers are stored using a method called two's complement.

Additionally, 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.

Few interesting ways to store integers:

You can use visual separators like 1_000, which will mean 1000. Number literals that can be multiple numeric types allow a type suffix, such as 57u8, to designate the type.

So how do you know which type of integer to use? If you’re unsure, Rust’s defaults are generally good places to start: integer types default to i32.

Other types of numbers can also be represented like these:

Number LiteralsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte(u8 only)b'A'

You can find examples of these here: https://github.com/codetit4n/rust-school/blob/main/lesson-3/variables/src/main.rs

Understanding Integer Overflow:

Let’s say you have a variable of type u8 that can hold values between 0 and 255. If you try to change the variable to a value outside that range, such as 256, integer overflow will occur, which can result in one of two behaviors. Let's see those using a code example:

  1. If I compile this code using cargo run (debug mode):

     fn main(){
         let mut overflow = 255u8;
         overflow = overflow + 1;
         println!("Value of overflow is: {overflow}");
     }
    

    Here, you can see it "panics". We will learn about panics in detail later in the series. But for now, think of panic as a runtime error.

  2. But if I compile it using cargo run --release (release mode):

    You will see:

    Here, you can see it did not panic. Instead, it gave a wrong answer. This behavior in rust is called two’s complement wrapping.

    Since the range of a u8 is from 0 - 255. So, when we added 1 to 255 it wrapped it to the minimum value of 0. And if I have added 2 to the overflow variable it would have given 1.

Why did the Rust program not panic while compiling in release mode? This is because in release mode Rust does not include checks for integer overflow, which it does for the debug mode.

Now, there are ways to handle this wrapping behavior in Rust. But we won't be discussing them here. If you are curious you can check out: https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-overflow

Floating-Point Types

For representing decimal numbers we use this type. Example:

let floating_num = 2.5;

This will have a default type of f64. Which is a 64-bit floating point variable.

Side Note: Floating-point numbers are represented according to the IEEE-754 standard.

Using type annotations:

We can also use type annotations like this:

let floating_num: f32 = 2.5;

This is a f32 type variable. Note how we used the : and then gave the type. This is how you use type annotations in Rust.

Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively.

Boolean Type

Similarly, we can have boolean types like:

let boolean_var = true;
let another_boolean_var: bool = false;

Booleans are one byte in size.

The Character Type

Rust’s char type is the language’s most primitive alphabetic type. Examples:

let p = 'p';
let z: char = 'z';
let emoji = '😄';
println!("Values of p, z, emoji: {p}, {z}, {emoji}");

Note that we specify char literals with single quotes, as opposed to string literals, which use double quotes. Rust’s char type is four bytes in size and represents a Unicode Scalar Value, which means it can represent a lot more than just ASCII. "Unicode Scalar Values range from U+0000 to U+D7FF and U+E000 to U+10FFFF inclusive. However, a “character” isn’t really a concept in Unicode, so your human intuition for what a “character” is may not match up with what a char is in Rust." We'll discuss this later in detail in this series.

Numeric Operations

Rust supports the basic mathematical operations you’d expect for all the number types: addition, subtraction, multiplication, division, and remainder(modulo).

Examples:

// addition
let sum = 5 + 10;

// subtraction
let difference = 95.5 - 4.3;

// multiplication
let product = 4 * 30;

// division
let quotient = 5.0 / 3.0; // Results in 1.6666666666666667
let truncated = -5 / 3; // Results in -1

// remainder
let remainder = 43 % 5;

Note: Numeric operations in Rust are similar to other languages. So, for instance, if you do: let quotient = 5 / 3; it will result in 1 because both the numerator and denominator are integers, so the result will be an integer.

Constants (vs Variables)

As the name suggests constants should always remain constant and variables can vary. But, wait aren't immutable variables similar to constants, since both of them cannot change?

The main difference is that the immutable variables are by default immutable and can be made mutable using mut . But the constants are always immutable, their behaviors cannot be changed.

NOTE: One thing to remember is that in certain programming constructs, we always want predictable behaviors from our code. It is very useful since it makes our compiler do less work. And less work means better performance.

Now, let's declare some constants:

const TWO: u32 = 2;

This is how we declare constants using the const keyword.

Note that it is mandatory to type annotate constants.

Thanks!