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), andu8
,u16
,u32
,u64
,u128
(unsigned). - Pointer-sized:
isize
(signed) andusize
(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
andusize
are 32 bits. - On a 64-bit system,
isize
andusize
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 eithertrue
orfalse
. Rust typically uses a full byte for a boolean for alignment reasons.char
: A four-byte Unicode scalar value. This is distinct from C’schar
, which is usually one byte and may represent ASCII or other encodings.
Rust Type | Size | Range | Equivalent C Type | Notes |
---|---|---|---|---|
i8 | 8 bits | -128 to 127 | int8_t | Signed 8-bit integer |
u8 | 8 bits | 0 to 255 | uint8_t | Unsigned 8-bit integer |
i16 | 16 bits | -32,768 to 32,767 | int16_t | Signed 16-bit integer |
u16 | 16 bits | 0 to 65,535 | uint16_t | Unsigned 16-bit integer |
i32 | 32 bits | -2,147,483,648 to 2,147,483,647 | int32_t | Signed 32-bit integer (default in Rust) |
u32 | 32 bits | 0 to 4,294,967,295 | uint32_t | Unsigned 32-bit integer |
i64 | 64 bits | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | int64_t | Signed 64-bit integer |
u64 | 64 bits | 0 to 18,446,744,073,709,551,615 | uint64_t | Unsigned 64-bit integer |
isize | pointer-sized (32 or 64) | Varies by architecture | intptr_t | Signed pointer-sized integer (for indexing) |
usize | pointer-sized (32 or 64) | Varies by architecture | uintptr_t | Unsigned pointer-sized integer (for indexing) |
f32 | 32 bits (IEEE 754) | ~1.4E-45 to ~3.4E+38 | float | 32-bit floating point |
f64 | 64 bits (IEEE 754) | ~5E-324 to ~1.8E+308 | double | 64-bit floating point (default in Rust) |
bool | 1 byte | true or false | _Bool | Boolean |
char | 4 bytes | Unicode 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 struct
s).
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 struct
s (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 ofType
with a fixedLength
. -
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 theCopy
trait (e.g., primitive numeric types),[T; N]
also implementsCopy
. 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.