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
orfalse
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:
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 |
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) and
f64` (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
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)
}