Rust Data Types
Tags: webdev, beginners, programming, rust
Published: 2024-01-23 14:03:37

# Data types in Rust

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. In cases when many types are possible, we must add a type annotation.

In this lesson, we will cover the basic data types in Rust.

We will talk about:
- Scalar types
- Compound types
- Custom types

If you prefer a video version
{% youtube NyqJp5M3hRE %}

<!-- <iframe width="905" height="510" src="https://www.youtube.com/embed/NyqJp5M3hRE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> -->

## Scalar types

A scalar type represents a single value. Rust has four primary scalar types: 
- integers
- floating-point numbers
- Booleans
- characters

Let's see them one by one.

### Integer types

Rust offers a variety of integer types, differing in size and whether they are
- signed (capable of representing negative numbers) 
- unsigned (only representing non-negative numbers). 

The size of an integer type determines its range of values.

#### List of Integer Types
Below is a table listing all of Rust's integer types, along with their sizes in bits, whether they are signed, and their range of values:

| 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`  |


```rust
fn main() {
    // Signed integers
    let small_signed: i8 = -128; // Smallest value for i8
    let large_signed: i64 = 9_223_372_036_854_775_807; // Largest value for i64

    // Unsigned integers
    let small_unsigned: u8 = 0; // Smallest value for u8
    let large_unsigned: u128 = 340_282_366_920_938_463_463_374_607_431_768_211_455; // Largest value for u128

    println!("Small signed: {}", small_signed);
    println!("Large signed: {}", large_signed);
    println!("Small unsigned: {}", small_unsigned);
    println!("Large unsigned: {}", large_unsigned);
}
```

In this example:

We declare and initialize two signed integers (i8 and i64) and two unsigned integers (u8 and u128).
We assign them their minimum or maximum possible values, showcasing the range of each type.
Finally, we print these values to the console.

We have a similar example here:

[![Data Types in Rust - Rust programming tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dmbayf8um1nsvllc4ob0.png)](https://youtu.be/NyqJp5M3hRE)


#### Integer Literals

In Rust, integer literals can be written in different numeral systems:

| Numeral System | Description                            | Example      |
|----------------|----------------------------------------|--------------|
| Decimal        | Base-10, common form                   | `98_222`     |
| Hexadecimal    | Base-16, prefixed with `0x`            | `0xff`       |
| Octal          | Base-8, prefixed with `0o`             | `0o77`       |
| Binary         | Base-2, prefixed with `0b`             | `0b1111_0000`|
| Byte (u8 only) | ASCII characters, prefixed with `b`    | `b'A'`       |


Here's an example demonstrating how to use different integer types and literals in Rust:

```rust
fn main() {
    let decimal: i32 = 98_222;    // Decimal
    let hex: u32 = 0xff;          // Hexadecimal
    let octal: u8 = 0o77;         // Octal
    let binary: u8 = 0b1111_0000; // Binary
    let byte: u8 = b'A';          // Byte (u8 only)

    println!("Decimal: {}", decimal);
    println!("Hexadecimal: {}", hex);
    println!("Octal: {}", octal);
    println!("Binary: {}", binary);
    println!("Byte: {}", byte);
}

```

Here is a similar example:

[![Data Types in Rust - Rust programming tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cs4qoc7wjzywra7po944.png)](https://youtu.be/NyqJp5M3hRE)

### Floating-point types

Rust has two primary types for representing floating-point numbers: f32 and f64. 

The f32 is a single-precision float, while f64 is a double-precision float. 

The default type is f64 because it offers a good balance between precision and performance on modern CPUs. It's roughly the same speed as f32 but provides more precision.

This example shows how to declare floating-point variables and perform basic arithmetic operations.

```rust
fn main() {
    let x = 2.0; // f64, double-precision
    let y: f32 = 3.0; // f32, single-precision

    // Arithmetic operations
    let sum = x + y as f64; // Type casting f32 to f64
    let difference = x - y as f64;
    let product = x * y as f64;
    let quotient = x / y as f64;

    println!("Sum: {}", sum);
    println!("Difference: {}", difference);
    println!("Product: {}", product);
    println!("Quotient: {}", quotient);
}

```

Here is a similar example:

[![Data Types in Rust - Rust programming tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ezz7khly2enavdktrsck.png)](https://youtu.be/NyqJp5M3hRE)

### Boolean type

In Rust, the Boolean type is represented by bool. It is one byte in size and can only take two values: true and false. 

Booleans are often used in conditional statements to control the flow of a program.

```rust
fn main() {
    let t = true;
    let f: bool = false; // Explicit type annotation

    // Using Booleans in an if statement
    if t {
        println!("t is true");
    }

    if !f { // using ! to invert the Boolean value
        println!("f is false");
    }
}
```

In this example:

[![Data Types in Rust - Rust programming tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xj73f87csiuvqzgppxmt.png)](https://youtu.be/NyqJp5M3hRE)

### Character type

In Rust, the char type is four bytes in size and is used to represent a single Unicode Scalar Value. This means it can encode much more than just ASCII characters. 

It can represent a wide range of characters, including:
- accented letters
- characters from various languages (Chinese, Japanese, Korean, ...)
- symbols like emojis and even zero-width spaces.

#### Unicode Scalar Values

Unicode Scalar Values in Rust range from U+0000 to U+D7FF and U+E000 to U+10FFFF, making char capable of representing over a million different characters.

```rust
fn main() {
    let c = 'z'; // ASCII character
    let z = '𝕏'; // Unicode character (U+1D54F)
    let heart_eyed_cat = '😻'; // Emoji

    // Iterating over characters in a string
    for char in "Hello, 世界!" 🚀.chars() {
        println!("{}", char);
    }
}
```

In this example:

- We declare three char variables: c, z, and heart_eyed_cat, showcasing the ability of char to store various types of characters.
- We also demonstrate how to iterate over the characters of a string, which includes both ASCII and non-ASCII characters.

Rust's char type is versatile and essential for applications requiring handling diverse character sets, such as text processing in different languages.

Here is a similar example:

[![Data Types in Rust - Rust programming tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/trb9u1a4u6ade0pxpbpy.png)](https://youtu.be/NyqJp5M3hRE)

## Compound types

Compound types can group multiple values into one type. 

Rust has two primitive compound types: tuples and arrays.

### Tuple type

In Rust, a tuple is a versatile way to group together a number of values of varying types into one compound type. Tuples are particularly useful when you need to return multiple values from a function or when you want to pass around a group of values that might not be related enough to warrant a struct.

Characteristics
- Fixed Length: A tuple's size cannot change once declared. This means you cannot add or remove elements from a tuple after its creation.
- Heterogeneous: Tuples can contain elements of different types, making them more flexible than arrays for specific use cases.

```rust
fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);

    // Destructuring a tuple into individual variables
    let (x, y, z) = tup;
    println!("The value of y is: {}", y);

    // Accessing tuple elements directly by their index
    let five_hundred = tup.0;
    let six_point_four = tup.1;
    let one = tup.2;

    println!("Values: {}, {}, {}", five_hundred, six_point_four, one);
}
```

#### Destructuring and Indexing
In the example above, we demonstrate two common ways to work with tuples:

- Destructuring: This involves breaking a tuple down into its individual components. In the example, let (x, y, z) = tup; unpacks the tuple so each variable contains one of the tuple's values.
- Direct Access: You can also access individual elements of a tuple using a period (.) followed by the value index, such as tup.0. This is useful for quickly grabbing a single value from a tuple.

Tuples are a fundamental compound type in Rust, providing a straightforward way to aggregate a fixed number of items of potentially different types into a single, cohesive unit.

[![Data Types in Rust - Rust programming tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xckvennd09zv75i0q200.png)](https://youtu.be/NyqJp5M3hRE)

### Array type

In Rust, an array is a collection of elements of the same type with a fixed length. Arrays in Rust are immutable by default and their size cannot be altered after they are declared. This makes arrays suitable for scenarios where you need a constant-size collection of elements.

#### Characteristics and Usage
- Fixed Length: The size of an array is determined at compile time and cannot be changed. This offers predictability and efficiency in memory usage.
- Stack Allocation: Arrays are allocated on the stack rather than the heap, which can be more efficient for small collections or fixed-size data structures.
- Uniform Type: All elements in an array must be of the same type.

```rust
fn main() {
    let a = [1, 2, 3, 4, 5]; // Array of type [i32; 5]

    // Accessing elements
    let first = a[0]; // First element
    let second = a[1]; // Second element
    println!("First: {}, Second: {}", first, second);

    // Iterating over an array
    for element in a.iter() {
        println!("Value: {}", element);
    }
}
```

Accessing and Iterating
In the provided example:

- We create an array a with five integers.

- Elements are accessed using index notation, such as a[0] for the first element.
We use a for loop with .iter() to iterate over each element in the array, demonstrating how to access and manipulate array elements in a sequence.

- Arrays are a fundamental part of Rust's type system, ideal for when you need a simple, fixed-size list of elements. For dynamic collections where the size can change, Rust offers other types like vectors (Vec<T>). We will cover vectors in a later lesson.

Here is a similar example:

[![Data Types in Rust - Rust programming tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6b9km9jz7ck6bd2c65b2.png)](https://youtu.be/NyqJp5M3hRE)


## Custom types

Rust allows you to define your own data types. You can define custom data types using the `struct` and `enum` keywords.

### Struct type

A struct is a custom data type that lets you name and package together multiple related values that make up a meaningful group. Structs are similar to tuples, but with named fields. Structs are useful when you want to give a group of values a name and clarify your code's intent.

```rust
struct Person {
  name: String,
  age: u8,
}

fn main() {
  // Creating an instance of the struct
  let person = Person {
  name: String::from("Alice"),
  age: 30,
};

  // Accessing fields of the struct
  println!("Name: {}", person.name);
  println!("Age: {}", person.age);
}
```

In this example:
- We define a `Person` struct with two fields: `name` (of type `String`) and `age` (of type `u8`).
- We then create an instance of the `Person` struct, initializing the fields with specific values.
- The `println!` macro is used to display the values of the struct's fields.

[![Data Types in Rust - Rust programming tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zi051cfmx9s11z83kxez.png)](https://youtu.be/NyqJp5M3hRE)

### Enum type

Enums in Rust, short for enumerations, are powerful custom data type that allow you to define a type by enumerating its possible variants. 

They are handy for creating a type that can be one of a few different things, each potentially with different types and amounts of associated data.

Key Features of Enums:
- Variants: Enums can have multiple variants, and each variant can optionally carry different types and amounts of data.
- Pattern Matching: Enums are often used with Rust's match control flow construct, which provides a way to execute different code based on the variant of the enum.
- Common Use Cases: Enums are widely used for error handling (Result<T, E> enum), optional values (Option<T> enum), and state management.

Let's see this with an example. We also have a spoiler for you: one of my favorite features of Rust is the `match` statement, which we will cover in an upcoming lesson.

```rust
// Define an enum to represent the states of a traffic light
enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn main() {
    let light = TrafficLight::Red;

    match light {
        TrafficLight::Red => println!("Stop"),
        TrafficLight::Yellow => println!("Caution"),
        TrafficLight::Green => println!("Go"),
    }
}
```     

In this example, TrafficLight is an enum with three variants: Red, Yellow, and Green. 

We create an instance of TrafficLight::Red and then use a match statement to perform different actions based on the variant.

[![Data Types in Rust - Rust programming tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/az40adjvvs82j8nknjeu.png)](https://youtu.be/NyqJp5M3hRE)

## Recap

In this lesson, we covered the basic data types in Rust. 

We learned about:
- scalar types: integers, floating-point numbers, Booleans, and characters
- compound types: tuples and arrays
- custom types: structs and enums

## EXTRA: print the type of a variable

```rust

fn main() {
    // Arrays
    let a = [1, 2, 3, 4, 5]; // type is [i32; 5]

    //print the type of a
    print_type_of(&a);
}

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}
```


If you prefer a video version
{% youtube NyqJp5M3hRE %}

<!-- <iframe width="905" height="510" src="https://www.youtube.com/embed/NyqJp5M3hRE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> -->

You can keep in touch with me here: [Francesco Ciulla](https://www.francescociulla.com/)