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:
Scalar Types
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:
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 |
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 Literals | Example |
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_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:
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.
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!