What are Data Types?
As we discussed in the last article that we can store any data in variables. The variables that we were declaring till now did not have any data types mentioned and were being assigned data type according to the data they contain by the Rust compiler. This is necessary because every data type requires a specific amount of space which is assigned after the data type is known.
There are 2 basic types of data types, Scalar and Compound. We will go deep into the types a little later. In Rust the data types are statically typed, meaning that they are needed at compilation. So either we can mention the type explicitly which is called type annotation or the Rust compiler can infer the type itself in simple cases. For example, if we declare a simple number it can be inferred easily like let num = 22 , but if we have a more complex operation like taking a string and converting it into number then we would have to annotate the variable with the type. It will look like : let num : u32 = "22".parse().expect("Not a number!") .
For now don’t worry about the .parse() or .expect() , we focus on : u32 this is where we are giving the variable a type of unsigned 32 bit integer. We will discuss more about these types later in this article. As we saw in the above example we annotate a variable with its type using a : . We add the : after the variable name followed by the type we want the variable to be.
Scalar Types
Scalar Types are the types that represent a single value, like a number or a character. In Rust there are 4 types of scalar data types : Integers, Floating-point numbers, Booleans and Characters. Let’s start with the easier ones and then go to the confusing types.
Character Type
The Character Type represents a single Unicode Value like an ‘a’ or a ‘=’ or an emoji like ‘🦀’. All these single values are characters. In Rust we use the char keyword to declare a character type. It is 4 bytes in size. For example, if we want to declare a variable ‘ch’ with the character data type we will do it like : let ch: char = 'a'; . This will declare a character variable called ch containing the character ‘a’.
Boolean Type
Boolean type is very useful data type used to denote just two values, either true or false . It is 1 byte in size. It is mostly used in conditional statements to determine if some statement is true or false. We declare a boolean like : let flag: bool = true; . This will create a variable flag with the data type boolean and the values as true.
Integer Type
Now coming to the little complex data types, first we have Integer. Integer type is used to represent numbers without any fraction component in Rust. At first it sounds simple, we have a number either positive or negative and we assign it the type integer. But here is where the complexity and optimization in Rust comes in. Since the numbers are infinite and can get bigger to any size we cannot have a single type that is able to contain the least possible and the most possible number as it will be a waste of space. So that is why in Integers we have different types for different sizes. Furthermore we have 2 variants for all the sizes, that are : signed and unsigned.
Signed integers will contain both positive and negative numbers while the unsigned integers will only contain positive numbers. So we use unsigned when we are sure that there is no possible negative value for that variable otherwise we use signed.
Now that we know about why it is complex and why there are so many types let’s look at the list of all types and sizes of integers:
| Length | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| Architecture-dependant | isize | usize |
Here all the types are self explanatory but let me break it down. So we put an i followed by the size of integer for signed integer and an u followed by the size of the integer for unsigned integer. And as for the last one it depends on the architecture type of the machine that we are using, so if we are using a 32-bit machine then isize and usize will be 32 bit, and if we are using a 64-bit machine then isize and usize will be 64 bit in size.
Integer Overflow :
While we are at this topic we should also learn about a very common error that we will definitely encounter when using integers, it is called Integer Overflow. It is a condition when we try to assign a value to an integer variable that is out of range from its defined size. For example u8 can store values from 0-255, so if we try to assign 256 to a u8 variable it will throw an error.
Rust handles it in 2 ways. First is if we are in debug mode Rust will panic and throw an error that looks like :
❯ cargo run
Compiling programming-basics v0.1.0 (<path>/programming-basics)
error: this arithmetic operation will overflow
--> src/main.rs:3:17
|
3 | let m: u8 = n + 1;
| ^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default
error: could not compile `programming-basics` (bin "programming-basics") due to 1 previous error
As we can see it clearly tells us that the number n+1 is out of the range of u8 data type as in the code I assigned the value 255 to n and tried to make the variable m = n+1.
Second is if we are compiling the program in release mode, then these checks are ignored. Rust will silently just ‘wrap around’ the value, so like if the values was 256 then it will turn to 0 and if it was 257 it will turn to 1.
Floating-point Type
To put is plainly, floating-point numbers are the numbers that contain a decimal. Meaning they contain an integer with a fraction. There are just 2 types of floating-point variables : f32 and f64 , which are 32-bit and 64-bit respectively.
This is how we represent it in code : let fl: f32 = 2.22; . This will create a variable named fl and assign the value 2.22 to it.
Code Implementation
Now that we know about all the scalar data types, let us implement all of them and observe the output.
fn main() {
let mut num = 22;
println!("{num}");
num = 21;
println!("{num}");
let number: u32 = 1234;
let number2: i32 = -1234;
println!("I am an unsigned integer : {number}");
println!("I am a signed integer : {number2}");
let fl: f32 = 2.22;
println!("I am a floating point number : {fl}");
let ch: char = '🦀';
println!("I am a character : {ch}");
let t: bool = true;
println!("I am a boolean : {t}");
}
The output will be:
❯ cargo run
Compiling programming-basics v0.1.0 (<path>/programming-basics)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/programming-basics`
22
21
I am an unsigned integer : 1234
I am a signed integer : -1234
I am a floating point number : 2.22
I am a character : 🦀
I am a boolean : true
As we can see it ran perfectly and the output is as expected.
Compound Types
Compound Types are the data types that can group multiple values into once single type. There are 2 types of Compound Types in Rust : Tuples and Arrays.
Before diving deep into these types we need to address one thing, that is you might be expecting to see String here in the Compound Types but the thing is String in Rust is a dynamic heap allocated type that does not have a fixed size, hence it is not included in this article. It deserves its own article that we will have later in this series when we are covering collections.
Tuple Type
A Tuple in Rust is a way of grouping multiple values with different data types into one single compound type. Tuples have a fixed size and we cannot increase or decrease their size once declared. We create a tuple by writing the values separated by commas , inside parentheses() . Each value in a tuple can have its own type, they do not have to be all same or all different. By default Rust is able to detect the types in a tuple but it is still a good practise to annotate the types while declaring.
The syntax to create a tuple is as follows :
Without type annotation:
let tup = (22, false, 'a', 2.22);
With type annotation:
let tup: (i32, bool, char, f32) = (22, false, 'a', 2.22);
Now after creating a tuple, the question comes on how do we access these values. In Rust there are 2 ways to access these values :
- Destructuring : This way we destructure a tuple into multiple variables. It can be done using the following syntax :
let (a, b, c, d) = tup;. Considering the values in the above initiated tuple a will contain 22, b will contain false, c will contain ‘a’ and d will contain 2.22. - Indexing : If we don’t want to create multiple variables we can directly access the values using their index or position in the tuple. We access it by writing the tuple name followed by a
.and then the index we want to access. The indexes always start from 0 and not 1. So if we want to print the first element we will write :let value = tup.0;println!("First element of tuple is : {value}"). This will print the element at 0th index meaning 1st position.
Array Type
An Array is also a way to group multiple values into a single compound type. The catch here is that unlike tuples all the values in an array should have the same data type. We create an array by writing the values separated by commas, inside square brackets[] . Arrays in Rust have a fixed length and cannot be expanded or shrunk after declaration.
The syntax to create an array is as follows :
Without type annotation:
let arr = [1,2,3,4,5];
With type annotation:
let arr: [i32; 5] = [1,2,3,4,5];
Here without the type annotation is straight forward but with the annotation after we write the name of the array we follow it by a : and then square brackets[] , inside which we first write the type of all the values in the array(i32in our case) then a semicolon ; , followed by the number of values inside the array.
We can also create an array with the same values by writing the values followed by a semicolon and then the number of elements.
let arr = ['a'; 5] , here the value of ‘arr’ will be : ['a', 'a', 'a', 'a', 'a'] .
To access the elements of an array we have one method, that is using the indexes.
We access an element at an index by writing the array name followed by the index inside square brackets[] .
For example:
If we take an array ‘arr’.
let arr: [i32; 5] = [1,2,3,4,5];
Now we want to access and print the element at 3rd index, that is the 4th position, which contains the values 4.
let value = arr[3];
println!("The value at 4th position / 3rd index is : {value}");
This is going to print the value 4.
Code Implementation
Let’s implement and add these data types to our earlier code.
fn main() {
let mut num = 22;
println!("{num}");
num = 21;
println!("{num}");
let number: u32 = 1234;
let number2: i32 = -1234;
println!("I am an unsigned integer : {number}");
println!("I am a signed integer : {number2}");
let fl: f32 = 2.22;
println!("I am a floating point number : {fl}");
let ch: char = '🦀';
println!("I am a character : {ch}");
let t: bool = true;
println!("I am a boolean : {t}");
let tup: (i32, f32, char, bool) = (22, 2.22, 'a', false);
let (_a, b, _c, _d) = tup;
let val1 = tup.2;
println!("The value of b is : {b}");
println!("The value at 3rd position / 2nd index of tuple is : {val1}");
let arr: [char; 5] = ['a', 'b', 'c', 'd', 'e'];
let val2 = arr[2];
println!("The value at 3rd position / 2nd index is : {val2}");
}
The output will be:
❯ cargo run
Compiling programming-basics v0.1.0 (<path>/programming-basics)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/programming-basics`
22
21
I am an unsigned integer : 1234
I am a signed integer : -1234
I am a floating point number : 2.22
I am a character : 🦀
I am a boolean : true
The value of b is : 2.22
The value at 3rd position / 2nd index of tuple is : a
The value at 3rd position / 2nd index is : c
Here we can see that we correctly get the value in the tuple as well as the array.
Conclusion
So this article has been a big one and we learnt a lot. We learnt about Scalar and Compound types. We then went deeper and learnt which data types come under them like integer, floating-point, boolean, character, tuple and array. With this much information please don’t forget to keep practicing these types and come back to the article if you ever forget some. That is it for this one will see you all in the next article where we will explore one of the most important topic that we will cover that is “Functions”.
-Aditya
Top comments (0)