A Trip to Data Types in Rust

A Trip to Data Types in Rust

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

A Trip to Data Types in Rust

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

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.

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

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 unsigned varible 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?

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

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

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 programm 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, and f64 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

Boolean Type

Is there anything except true or false in the world?

Rust also has 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 primive 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 🙂

Resources