Skip to main content

Data Types in Rust

As mentioned before in the introduction of this course, one of Rust's main objectives is to have a robust, compile-time type system. Just as the immutability of variables helps with safety, having types in Rust greatly aids in ensuring that data is flowing as it should throughout the program.

Rust has two kinds of primitive, or base, data types:

  • Scalar - single point types, such as numbers and booleans (true or false statements).
  • Compound - arrays and tuples.

Scalar Types

Scalar types represent a single value, such as a number or boolean. Rust has four core Scalar types, which you have most likely seen in other programming languages:

  • Integers
  • Floating-point numbers
  • Booleans
  • Characters

Integers & Floating-point Types

An integer in Rust is the same as in mathematics - a non-fractional, whole number that can be either positive or negative. There are two types of integers: unsigned (positive numbers) and signed (negative or positive numbers).

  • Signed integers are denoted by the i, followed by the length of the number: i32.
  • Unsigned integers start with u, followed by the length of the number: u32.

The numeric characters 32 that follow whether an integer is signed or unsigned denotes the length of the number. Take this table from the Rust Book, which states all possible integer variants:

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

The length or size of the integer is always explicitly declared. Rust has a set of defaults for inferring types, such as integers defaulting to the i32 type. usize and isize depend on your machine's architecture, meaning they are either 32, or 64 bit in size.

Here are some common ways to declare an integer, some formats less traditional than others:


// Defaults to i32.
let default = 10;

// Explicitly declare this variable as an unsigned, 64-bit integer:
let sixty_four_bit_int: u64 = 10;

// You can also declare integer literals like so:
let big_number = 65_550; // 65,550

// Another way to specify the type can be done with this syntax, with the type following the number:
let short_hand = 455u32;

// Declaring a hex literal is possible!
let hex = 0x001;

// You can also represent characters as unsigned 8-bit numbers. This will get very useful later on:
let word_as_bytes: u8 = b'F';

Lastly, as integers do have a size, integer overflow is possible if you exceed the limit of a given type. For example, the type u8 has a minimum of 0 and a maximum of 255, meaning 256 would cause a panic. As the Rust compiler doesn't check for overflows, there are some additional functions you can use to ensure that adding or subtracting is always safe and won't cause a runtime error. We'll learn more about those later on.

Floating point numbers

Rust has two floating number types: ' f32(32-bit) andf64` (64-bit). Floating point numbers, unlike integers, are fractional, meaning they contain decimal points to represent parts of whole numbers:

f32 has single precision accuracy, whereas f64 has double-precision accuracy. f64 is the default type for floating point numbers in Rust:


let floating_double: f64 = 1.11;

let floating_single: f32 = 1.4;

let also_floating_double = 1.45;

Operators

As in any other programming language, there are a set of operators that allow for basic mathematics to be performed:


let addition = 1 + 1; // 2
let subtraction = 10 - 5; // 5
let division = 8 / 4; // 2
let multiplication = 4 * 4; // 16

Likewise, Rust also supports bitwise operators. A full list of Rust's operators and symbols may be found here.

Boolean Types

A boolean value can only have two possible states: true or false. Declaring a boolean in Rust is simple:


let i_am_true = true;

// Usually, it's easy to infer a bool type; however good to show explicit type assignments.
let i_am_false: bool = false;

This opens up many possibilities for branching logic in our Rust programs, of which we'll cover later.

Character Type

The character, or char type in Rust is the basic building block for alphabetic values. It simply declares a single character, which is encased by single quotes. At its core, it represents a Unicode Scalar Value, supporting a plethora of characters as well as zero-width spaces:

// Both are valid Unicode characters!
let the_letter_a = 'A';
let the_moon = '🌒';

Compound Types

Scalar types define how variables can hold a single value at a time. Compound types can hold multiple values under the same, unifying type.

There are two primary kinds of compound types - tuples and arrays.

Tuples

A tuple is a way to combine multiple values and types into a single, compound type. It can be useful for describing a set of varying values which have some relationship to one another.

The syntax for writing a tuple is as follows. Note that the type annotations are optional:


let my_tuple: (u32, f32, char) = (5, 5.5, '🌒'); // The explicit type annotations here are optional,

There are a couple of ways to access the elements within a tuple. The first way merely involves fetching them in order, starting at 0:


let unsigned_32: u32 = my_tuple.0;
let floating_32: f32 = my_tuple.1;
let moon: char = my_tuple.2;

You may also use pattern matching to access these elements. The following syntax constructs a pattern out of the above:


let my_tuple: (u32, f32, char) = (5, 5.5, '🌒'); // The explicit type annotations here are optional,

let (unsigned_32, floating_32, moon) = my_tuple;

Tuples can have elements that are of varying types, as seen above. They can be useful in describing a set, fixed amount of elements, such as a set of coordinates:


let location: (i32, i32) = (10, 45);

Arrays

An array is a collection of multiple values. Unlike tuples, these values must be the same type. Arrays are like lists - and are useful for having a fixed amount of types and having data stored on the stack.

Arrays are typically used less often than vectors, which is a type of collection that includes a number of convenience methods for sorting and manipulating the values within the collection. Vectors can grow and shrink in size, as the data is managed by the heap, whereas arrays are fixed-length and stored on the stack.


let my_pets_ages: [i32; 3] = [4, 5, 3];

Notice in the type declaration, [i32; 3], that the first part denotes the type of each element within the array with each type being an i32. The second portion within the square brackets, 3, denotes the maximum amount of elements an array can hold. It's possible to do without this explicit declaration, as the compiler will interpret it as it stands.

To access specific elements within an array is done using square brackets, along with the index of the element you wish to access. As with tuples, all arrays start at index 0:


let course_modules = ["Module 1", "Module 2", "Module 3","Module 4", "Module 5"];
// Use the [ index ] syntax to access a particular element.
let module_one = course_modules[0];

Be forewarned - it's possible to access an element that doesn't exist, which would cause a runtime error.


error: this operation will panic at runtime
--> src/main.rs:32:18
|
32 | let module_six = course_modules[5];
| ^^^^^^^^^^^^^^^^^ index out of bounds: the length is 5 but the index is 5
|
= note: `#[deny(unconditional_panic)]` on by default

info

How can the Rust compiler know when a runtime error will occur? Because an array is fixed length, it knows the maximum index that can be accessed, in this case, 4. The compiler disallows it before the program is even compiled. However, if this index is a user-generated value, which can only exist at runtime, then a panic will occur and the program will stop. The fact that the program stops is a security measure as it prevents any possibly invalid memory from being accessed and exploited.

Try it yourself!

What is happening here?

We define an array a fixed-length list of values of the same type stored on the stack. We can successfully access and print the items in this list; however, the compiler prevents any out-of-bounds access. It also prevents any additional items from being appended, as the array has a fixed length in memory.

Next, we create an array of tuples, which would look like:


let tuple_array = [("hi", 3), ("hello", 1)];
for tuple in tuple_array {
println!("{:?}", tuple)
}