5.3 Data Types

Rust is statically typed, meaning every variable’s type is known at compile time, and it is strongly typed, preventing implicit conversion to unrelated types (such as automatically converting an integer to a floating-point). This strong, static typing helps catch errors early and avoids subtle bugs caused by unintended type mismatches.

5.3.1 Scalar Types

Rust’s scalar types represent single, discrete values: integers, floating-point numbers, booleans, and characters.

Integers

Rust provides various integer types, distinguished by their size and whether they are signed or unsigned:

  • Fixed-size: i8, i16, i32, i64, i128 (signed), and u8, u16, u32, u64, u128 (unsigned).
  • Pointer-sized: isize (signed) and usize (unsigned). These match the target platform’s pointer width (32 or 64 bits).

By default, unsuffixed integer literals in Rust are 32-bit signed integers (i32).

isize and usize

These types have sizes dependent on the target architecture:

  • On a 32-bit system, isize and usize are 32 bits.
  • On a 64-bit system, isize and usize are 64 bits.

They are typically used for indexing collections. For example, Rust requires array indices to be usize, so if you have an i32 index, you must cast it to usize before indexing an array.

Floating-Point Numbers

Rust supports two floating-point types, both of which follow the IEEE 754 standard:

  • f32 (32-bit)
  • f64 (64-bit, and the default)

Modern CPUs typically handle 64-bit floating-point (double-precision) operations in hardware just as efficiently—or sometimes even more efficiently—than 32-bit floating-point operations, making f64 a common default choice.

Booleans and Characters

  • bool: Can be either true or false. Rust typically uses a full byte for a boolean for alignment reasons.
  • char: A four-byte Unicode scalar value. This is distinct from C’s char, which is usually one byte and may represent ASCII or other encodings.
Rust TypeSizeRangeEquivalent C TypeNotes
i88 bits-128 to 127int8_tSigned 8-bit integer
u88 bits0 to 255uint8_tUnsigned 8-bit integer
i1616 bits-32,768 to 32,767int16_tSigned 16-bit integer
u1616 bits0 to 65,535uint16_tUnsigned 16-bit integer
i3232 bits-2,147,483,648 to 2,147,483,647int32_tSigned 32-bit integer (default in Rust)
u3232 bits0 to 4,294,967,295uint32_tUnsigned 32-bit integer
i6464 bits-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807int64_tSigned 64-bit integer
u6464 bits0 to 18,446,744,073,709,551,615uint64_tUnsigned 64-bit integer
isizepointer-sized (32 or 64)Varies by architectureintptr_tSigned pointer-sized integer (for indexing)
usizepointer-sized (32 or 64)Varies by architectureuintptr_tUnsigned pointer-sized integer (for indexing)
f3232 bits (IEEE 754)~1.4E-45 to ~3.4E+38float32-bit floating point
f6464 bits (IEEE 754)~5E-324 to ~1.8E+308double64-bit floating point (default in Rust)
bool1 bytetrue or false_BoolBoolean
char4 bytesUnicode scalar value (0 to 0x10FFFF)None (C’s char=1B)Represents a single Unicode character

5.3.2 Primitive Compound Types: Tuple and Array

Rust provides tuple and array as primitive compound types, each useful in different scenarios. They bundle multiple values but differ in how they store data and enforce type constraints.

5.3.3 Tuple

A tuple is a fixed-size collection of elements, each of which can have a different type. This contrasts with C, which has no built-in anonymous tuple type (though C supports structs).

Tuple Type and Value Syntax

  • Type: (T1, T2, T3, ...)
  • Value: (v1, v2, v3, ...)
#![allow(unused)]
fn main() {
let tup: (i32, f64, char) = (500, 6.4, 'x');
}

Tuples have a size known at compile time and cannot change length.

Singleton Tuples and the Unit Type

  • Singleton tuple (x,): Note the trailing comma to distinguish it from (x).
  • Unit type (): A zero-length tuple, often used to indicate “no meaningful value.” Functions that return “nothing” actually return ().
#![allow(unused)]
fn main() {
let single = (5,);  // a single-element tuple
let unit: () = (); // the unit type
}

Accessing Tuple Elements

Tuple elements are accessed using zero-based indices:

#![allow(unused)]
fn main() {
let tup: (i32, f64, char) = (500, 6.4, 'x');
println!("{}", tup.0); // 500
println!("{}", tup.1); // 6.4
println!("{}", tup.2); // x
}

You cannot do runtime indexing (like tup[i]) because each element can have a different type.

Mutability and Initialization

A tuple is immutable by default. Declaring it as mut allows modifying its fields, but you still must initialize all fields at once:

#![allow(unused)]
fn main() {
let mut tup = (500, 6.4, 'x');
tup.0 = 600; // Valid, because 'tup' is mutable
}

Partial initialization (leaving some fields uninitialized) is not allowed.

Destructuring

You can destructure a tuple into individual variables:

#![allow(unused)]
fn main() {
let tup = (1, 2, 3);
let (a, b, c) = tup;
println!("a = {}, b = {}, c = {}", a, b, c);
}

We’ll explore variable bindings and destructuring further in the next sections.

Tuples vs. Structs

In C, you might define a struct to group multiple data fields. Rust also has structs (with named fields). Consider a tuple if:

  • You have a small set of elements (potentially of varied types).
  • You don’t need named fields.
  • The positional meaning is clear.

Use a struct if:

  • You need more complex data organization.
  • Named fields improve clarity.
  • You want additional methods or traits on your data type.

5.3.4 Array

An array in Rust is a fixed-size sequence of elements of the same type. Rust arrays are bounds-checked, which prevents out-of-bounds memory access.

Declaration and Initialization

#![allow(unused)]
fn main() {
let array: [i32; 3] = [1, 2, 3];
}
  • [Type; Length] indicates an array of Type with a fixed Length.

  • Rust allows compile-time expressions for initialization:

    #![allow(unused)]
    fn main() {
    let x = 5;
    let y = x * 2;
    let array: [i32; 3] = [x, y, x + y]; // [5, 10, 15]
    }
  • You can fill all elements with the same value:

    #![allow(unused)]
    fn main() {
    let zeros = [0; 5]; // [0, 0, 0, 0, 0]
    }

Type Inference and Initialization

Rust often infers the array’s type and length from the initializer:

#![allow(unused)]
fn main() {
let array = [1, 2, 3]; // Inferred as [i32; 3]
}

You may also use a suffix if needed:

#![allow(unused)]
fn main() {
let array = [1u8, 2, 3]; // Inferred as [u8; 3]
}

Accessing Array Elements

Arrays use zero-based indexing. Indices must be usize (a pointer-sized type):

#![allow(unused)]
fn main() {
let array: [i32; 3] = [1, 2, 3];
let index = 1;
let second = array[index];
println!("Second element is {}", second);
}

Rust checks array bounds at runtime. An out-of-bounds index causes a panic (a runtime error).

Multidimensional Arrays

You can nest arrays to create multidimensional arrays:

#![allow(unused)]
fn main() {
let matrix: [[i32; 3]; 2] = [
    [1, 2, 3],
    [4, 5, 6],
];
}

This is effectively an array of arrays.

Memory Layout and the Copy Trait

  • Arrays are stored contiguously, with no padding between elements.
  • If a type T implements the Copy trait (e.g., primitive numeric types), [T; N] also implements Copy. The entire array can then be duplicated by value without affecting the original.

When to Use Arrays

Use arrays when the size is fixed at compile time and you want efficient, stack-allocated, and bounds-checked storage. For resizable collections, Rust provides the Vec<T> type (vectors), which we’ll explore in a later chapter.

5.3.5 Stack vs. Heap Allocation

Rust’s primitive data types (scalars, tuples, arrays) typically reside on the stack when declared as local variables, because their size is known at compile time. This makes their allocation and deallocation straightforward and efficient. In contrast, types like Vec<T> or String store their elements on the heap, allowing dynamic resizing.

However, any type—primitive or otherwise—can end up on the heap if it is a field within a heap-allocated structure. For instance, a Vec<T> always stores its buffer on the heap, no matter which types T represents. We will cover these details in future chapters on ownership and collections.