This post published on my blog before
Hi everyone. Before that, I wrote a post called Understanding Variables and Mutability in Rust.
Today we'll review Data Types in Rust. In this small story, we'll see scalar and compound data type subsets.
Before starting, I'll create a project with cargo;
cargo new data_types
cd data_types
Introduction
Rust is a statically typed programming language. This means the compiler must know the data type of the variable's value you assigned.
The compiler can understand what type we want to use based on the value. In some cases, many types can be possible. For example, you got string value but it actually should be a numerical value. You can parse it. So, in this case, we have two different data types. But we'll only one.
let user_age_from_input: i8 = "27".parse().expect("Not a number!");
If we didn't add the type annotation, we'll see an output like
error[E0282]: type annotations needed
--> src/main.rs:4:9
|
4 | let user_age_from_input = "27".parse().expect("Not a number!");
| ^^^^^^^^^^^^^^^^^^^ consider giving `user_age_from_input` a type
Scalar Types
Scalar types represent only one value. Currently, we have four primary scalar types in Rust. These are integers, floating-point numbers, booleans and characters. You may familiar with them from other programming languages.
Integer Types
While we're talking about the integers, we actually saying is a number without a fractional part.
You can declare integer variables using i
or u
symbols. These are representing signed and unsigned types.
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 |
Each type has an explicit size in Rust. When you define a signed variable, this also means that variable can store negative and positive values.
Unsigned values can't store negative values. If you try to assign a negative value to an unsigned variable you'll see an error.
let user_age_from_input: u8 = -8;
println!("My age is {}", user_age_from_input);
You'll see this output;
error[E0600]: cannot apply unary operator `-` to type `u8`
--> src/main.rs:4:35
|
4 | let user_age_from_input: u8 = -8;
| ^^ cannot apply unary operator `-`
|
= note: unsigned values cannot be negated
A negative value can be bind in run time. But this isn't our topic for now.
When To Use Unsigned Variable?
In short, when your numbers start from 0...to positive numbers.
When To Use Signed Variable?
In short, when you need to store negative or positive numbers in some processes such as calculations, use a signed variable.
Which Type Should I Use?
If you don't know which type you should use, don't worry. Default data type is i32
in Rust.
let my_age = 27; // i32 by default
Calculations of the Maximum Number for Variants
By default you use i32. But how many numbers you can store? If you can't say that you can calculate like that;
For Signed Variants
from -(2n-1) to 2(n-1)-1 where n is the number of bits that variant uses.
For Unsigned Variants
from 0 to 2(n-1)-1.
There are two types that depend on the kind of computer your program is running on 64 bits or 32 bits. These are isize
and usize
Integer Overflow
Let's assume you have a variable type that declared as u8. So, it can hold values between 0 and 255. If you try to change that variable's value with 256, integer overflow will occur.
If you compile your program in debug mode, Rust will check your program for integer overflow cases and your program will panic
at runtime.
When you're compiling your program in release mode with --release
flag, Rust won't check your program for integer overflow. Instead of that, Rust will perform two’s complement wrapping
In short, values greater than the maximum value the type can hold “wrap around” to the minimum of the values the type can hold. In the case of a
u8
, 256 becomes 0, 257 becomes 1, and so on. The program won’t panic, but the variable will have a value that probably isn’t what you were expecting it to have. Relying on integer overflow’s wrapping behavior is considered an error
Floating-Point Types
Rust has two primitive types for floating-point numbers. These are numbers with decimal points. While i32
is the default type in integers, f64
is the default type for floating-point numbers in Rust.
let amount = 35.50; // f64
let total: f32 = 400.45; //f32
Floating-point numbers are represented according to the IEEE-754 standard. The
f32
type is a single-precision float, andf64
has double precision.
Numeric Operations
Rust supports the basic mathematical operations like the other programming languages. Addition, subtraction, multiplication, division, and remainders are supported. This just an example;
// addition
let sum_example = 19 + 1;
// subtraction
let get_difference = 2020 - 1993;
// multiplication
let multiply_it = 10 * 2;
// division
let divide_it = 30 / 2;
// remainder
let get_mod = 10 % 2;
Boolean Type
Is there anything except true or false in the world?
Rust also has a boolean type like the other programming languages. There are two possible values in Rust. These are true
or false
.
let is_completed = false;
// explicit type annotation
let is_send: bool = true;
Character Type
We've been worked on numbers and bool types till now. Now, we'll see a different type called character.
let symbol = '₺';
println!("Symbol is {}", symbol); // Symbol is ₺
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. Accented letters; Turkish, Chinese, Japanese, Korean, and Emojis. In the next posts, we'll see the string types.
Compound Types
Compound types can group multiple values into one type. Rust has two primitive compound types. These are tuple
and arrays
.
Tuple Type
A tuple type is a way to grouping the values in various types into one compound type. When you declare a tuple type, you can't add a new value. They have fixed lengths. (Actually there are few tricks but there is no built-in way).
let user_tuple: (i32, f64, bool) = (2019, 1500.76, true);
// or
let user_tuple = (2019, 1500.76, true);
This was a simple tuple. We created 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. We can print-out it like that;
let user_tuple = (2019, 1500.76, true);
println!("User tuple is {:?}", user_tuple);
Destructing
You can destruct the values in tuples. If you're used JavaScript before, you may familiar with destructing.
let user_tuple = (2019, 1500.76, true);
let (register_year, balance, is_customer) = user_tuple;
println!("User's balance is: {}", balance);
Accessing Tuple Indexes
You can access a tuple element directly using a dot (.
).
let user_tuple = (2019, 1500.76, true);
println!("User's balance is: {}", user_tuple.1);
This could be annoying :)
Array Type
The other way of storing multiple values is by using arrays. You can use different types in tuples. Unlike a tuple, each value of an array must be the same. Rust's array behavior is different than some other programming languages. Because arrays have a fixed length in Rust. For example;
let users = ["Ali", "Ben", "Burak", "Enes"];
println!("Users: {:?}", users);
Arrays are useful when you want your data allocated on the stack rather than the heap or when you want to ensure you always have a fixed number of elements.
An array isn't like a vector type. A vector is a similar collection type with an array. Its provided by the standard library and it's allowed to grow or shrink in size.
This is a good use case example for arrays from Rust's official documentation
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
You don't need to add a new element to months array. Because there are 12 months in a year.
You would want to create arrays with types.
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
/*------type: ^^^--size--*/
println!("Numbers: {:?}", numbers);
or
let numbers = [1; 5];
/*------value: ^--size--*/
println!("Numbers: {:?}", numbers);
In this way, you can create an array that has 5 elements containing number 1.
Accessing Array Elements
As in other languages, you can access array items. The first element's index is zero. The index increases one by one.
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
println!("First element is: {}", numbers[0]);
If you try to access an invalid index, your program won't compile.
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
println!("Number is: {}", numbers[5]);
error: this operation will panic at runtime
--> src/main.rs:6:29
|
6 | println!("Number is: {:?}", numbers[5]);
| ^^^^^^^^^^ index out of bounds: the len is 5 but the index is 5
|
= note: `#[deny(unconditional_panic)]` on by default
You will see an error as the above output. Because Rust will check that the index you’ve specified is less than the array length. If the index is greater than or equal to the array length, Rust will panic.
That's all.
Thanks for reading :)
Top comments (2)
Nice, Ali! I was just covering the data types chapter from the Rust Programming Language book.
Out of curiosity, what led you down the path of exploring Rust?
Thanks :)
Actually, I was planning to learn Rust for a while. I decided to learn Rust about two weeks ago. If you're asking that what is motivated you, my friend is writing Rust codes.
When he told me something about Rust, I liked Rust. And, I started reading Rust's documentation. I liked Rust thanks to its official documentation.
I don't need any online course. I can understand everything. I never used statically typed programming languages before. I was excited. So, I started to learn. But I don't have a goal for now. It looks like Rust just a hobby for me for now.